Population Example#

The Python and C projects populate a small USD scene, inspect values and metadata, sample an animated value, and recover from a failed load.

  • Python: examples/python/population — run uv run main.py.

  • C: examples/c/population — configure with CMake, build, and run population from the build directory. See Getting Started in C for package setup.

The adjacent scene.usda contains /World/Body, a Cube with PhysicsRigidBodyAPI. Its properties include a temperature, a token, two points, an empty token, and bounds. The prim and temperature property have custom metadata. /World has a translation of 10 along X.

Loading the Scene#

Create the runtime stage and load the physics domain. Later Python snippets run inside the same context managers; keep the C stage alive until the example ends.

from pathlib import Path

import ovstage
from ovstage import OrdinalRange, PopulationDomain, population

scene = str(Path(__file__).with_name("scene.usda"))
with ovstage.Stage("example.population") as stage, ovstage.PathDictionary(stage) as paths:
    # Select physics data, including Body's rigid-body API and its ancestors.
    population.open_usd(stage, scene, ordinal=1, domains=PopulationDomain.PHYSICS)
    stage.advance_write_floor(ordinal=1).wait()
    # Continue the following examples inside these context managers.

waitPop() checks population completion; waitOp() waits for runtime operations and releases them. check() and checkOvx() stop on errors.

const std::string scene = argc > 1 ? argv[1] : "scene.usda";
const ovx_string_t scenePath{scene.c_str(), scene.size()};
ovstage_instance_desc_t instanceDesc{};
instanceDesc.name = "example.population";
ovstage_instance_t* stage = nullptr;
check(nullptr, ovstage_create_instance(&instanceDesc, &stage), "create_instance");
auto* dict = getPathDictionary(stage);

// Select physics data and wait for population before making it readable.
waitPop(stage, ovstage_population_open_usd_from_file(
    stage, scenePath, 1, OVSTAGE_POPULATION_DEFAULT_TIME,
    OVSTAGE_POPULATION_DOMAIN_PHYSICS), "open_usd");
ovstage_write_floor_desc_t watermark{};
watermark.scope = OVSTAGE_SCOPE_ALL;
watermark.ordinal = 1;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
// Keep stage and dict alive until all examples finish.

Selecting Values and Metadata#

Select the Cube’s properties and metadata, then populate at ordinal 2. The complete program first populates the physics domain at ordinal 1.

# Select only /World/Body. is_under_path("/World") also includes its descendants.
body_condition = population.PrimPredicate.has_path("/World/Body")

# Request the values used below, plus metadata on those properties.
selector = population.Selector(
    prim_predicate=body_condition,
    property_predicate=population.PropertyPredicate.has_name(
        "size", "temperature", "state", "points", "emptyName", "emptyArray", "extent"
    ),
    prim_metadata_paths=["customData:label"],
    property_metadata_paths=["customData:unit"],
)

desc = population.Desc(
    domains=PopulationDomain.NONE,
    selectors=[selector],
    stage_metadata_paths=["timeCodesPerSecond"],
)
population.open_usd_with_desc(
    stage, scene, ordinal=2, time=population.DEFAULT_TIME, descs=[desc]
)
stage.advance_write_floor(ordinal=2).wait()
const ovx_string_t bodyPath[] = {literal_to_ovx_string("/World/Body")};
// Match this exact prim; is_under_path would select a subtree instead.
auto bodyCondition = ovstage_population_prim_predicate_has_path(bodyPath, 1);

const ovx_string_t primMetadata[] = {literal_to_ovx_string("customData:label")};
const ovx_string_t propertyMetadata[] = {literal_to_ovx_string("customData:unit")};
ovstage_population_selector_t selector{};
selector.prim_predicate = bodyCondition;
const ovx_string_t properties[] = {
    literal_to_ovx_string("size"), literal_to_ovx_string("temperature"), literal_to_ovx_string("state"),
    literal_to_ovx_string("points"), literal_to_ovx_string("emptyName"),
    literal_to_ovx_string("emptyArray"), literal_to_ovx_string("extent")};
selector.property_predicate = ovstage_population_property_predicate_has_name(properties, 7);
selector.prim_metadata_paths = primMetadata;
selector.prim_metadata_path_count = 1;
selector.property_metadata_paths = propertyMetadata;
selector.property_metadata_path_count = 1;

const ovx_string_t stageMetadata[] = {literal_to_ovx_string("timeCodesPerSecond")};
ovstage_population_desc_t desc{};
desc.domains = OVSTAGE_POPULATION_DOMAIN_NONE;
desc.selectors = &selector;
desc.selector_count = 1;
desc.stage_metadata_paths = stageMetadata;
desc.stage_metadata_path_count = 1;
// Population copies the description and the referenced arrays during submission.
waitPop(stage, ovstage_population_open_usd_from_file_with_desc(
    stage, scenePath, 2, OVSTAGE_POPULATION_DEFAULT_TIME, &desc, 1), "open with description");
watermark.ordinal = 2;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");

Sampling Animation#

The scene uses 24 time codes per second. Its temperature changes from 20 at time code 0 to 30 at time code 24. Sample one second and read the result:

# One second is USD time code 24 in this scene. The ordinal is independent.
population.update_from_usd_time(stage, time=1.0, ordinal=3)
stage.advance_write_floor(ordinal=3).wait()
with paths.create_path_list_from_strings(["/World/Body"]) as selected:
    with stage.query_from_path_list(selected) as query:
        with stage.read_attributes(query, [paths.intern_token("temperature")], OrdinalRange.latest(3)) as read:
            read.wait()
            for group in read.groups():
                with group:
                    print(float(group.array(0).ravel()[0]))
                    # 30.0
// Seconds are converted using timeCodesPerSecond=24, independently of ordinal 3.
waitPop(stage, ovstage_population_apply_usd_time(stage, 3, 1.0), "sample one second");
watermark.ordinal = 3;
waitOp(stage, ovstage_advance_write_floor(stage, &watermark), "advance watermark");
{
    const ovx_string_t selected[] = {literal_to_ovx_string("/World/Body")};
    ovx_primpath_list_t pathList{};
    checkOvx(dict, path_dictionary_create_path_list_from_strings(dict, selected, 1, &pathList), "paths");
    ovstage_query_handle_t query{};
    check(stage, ovstage_query_from_path_list(stage, pathList, &query), "query");
    const ovx_string_t name = literal_to_ovx_string("temperature");
    ovx_token_t attribute{};
    checkOvx(dict, path_dictionary_create_tokens_from_strings(dict, &name, 1, &attribute), "attribute");
    ovstage_ordinal_range_t range{};
    range.end_ordinal = 3;
    ovstage_read_handle_t read{};
    waitOp(stage, ovstage_read_attributes(stage, query, &attribute, 1, range, &read), "read");
    ovstage_read_group_t group{};
    check(stage, ovstage_fetch_read_next(stage, read, OVSTAGE_TIMEOUT_INFINITE, &group), "fetch");
    const DLTensor& tensor = group.data.tensors[0];
    const auto* values = reinterpret_cast<const float*>(
        static_cast<const uint8_t*>(tensor.data) + tensor.byte_offset);
    std::printf("%.1f\n", static_cast<double>(values[0]));
    // 30.0
    // Release the borrowed data before releasing the read and query.
    ovstage_release_group(stage, &group);
    waitOp(stage, ovstage_release_read(stage, read), "release read");
    waitOp(stage, ovstage_release_query(stage, query), "release query");
    path_dictionary_release_path_list_reference(dict, pathList);
}

See Population (USD → ovstage) for the remaining snippets and their API contracts.