Cloning Subtrees#
ovstage.Stage.clone() copies a subtree to one or more target paths.
For example, copy a robot subtree to create several simulation environments
in one call. The source is a prim path; each target becomes the
root of a copy.
Note
The clone API is pre-release and provisional (“Draft — API in flux”). Treat the exact symbols and argument ordering as subject to change. Clone copies runtime stage content, whether populated from USD or created through attribute writes. See Population (USD → ovstage) for loading USD content.
Cloning to Multiple Targets#
Like a write, clone carries an ordinal. It must clear the applicable floors
and satisfy the configured ordinal admission policy for target cells and prim lifecycle. Copied cells
are committed at that ordinal: the strict policy requires a newer ordinal for
later customization, even before sealing. NONDECREASING permits customization
at the same unsealed ordinal; FLOOR_ONLY skips retained-history admission.
Neither relaxes the clone’s operation floors. Its ordinal must exceed the global
floor and effective floors of source columns, even with no targets. Those source
checks are independent of target admission; fresh targets do not bypass them.
Target cells must also clear their operation floors and satisfy the configured
policy, including prim-lifecycle admission under the two history policies.
Advance the floor to make completed clones readable:
# The multi-environment pattern: stamp out N copies of a prototype
# (e.g. one scene/robot per RL environment) in a single call. Clone is
# an ordinal-keyed write (pick an ordinal above the current write
# floor) that copies the source subtree's attributes verbatim; the
# source must exist and each target must not. clone() blocks and
# raises OvstageError on failure.
stage.clone("/World/A", ["/World/A_env0", "/World/A_env1"], ordinal=3)
stage.advance_write_floor(ordinal=3).wait() # seal the clones so they're readable
print("cloned /World/A -> A_env0, A_env1")
// One call clones the subtree under a source prim to several new targets --
// the multi-environment pattern (N copies of a prototype, e.g. one scene
// per RL environment). Clone is an ordinal-keyed write: pick an ordinal
// above the write floor (ordinal 1 was sealed above, so clone at 2). The
// source must exist; each target must not already exist.
const ovx_string_t cloneTargets[] = { literal_to_ovx_string("/World/A_env0"),
literal_to_ovx_string("/World/A_env1") };
enq = ovstage_clone(stage, literal_to_ovx_string("/World/A"), cloneTargets, 2, /*ordinal*/ 2);
waitOp(stage, enq, "clone");
writeFloor.ordinal = 2; // seal the clones so they're readable
enq = ovstage_advance_write_floor(stage, &writeFloor);
waitOp(stage, enq, "advance_write_floor");
std::printf("cloned /World/A -> A_env0, A_env1\n");
Clone Rules#
The source must exist and every target must be new. A batch containing an existing target creates nothing.
Empty, invalid, or duplicate target paths fail at submission with
ErrorCode.INVALID_ARGUMENT. A property path cannot hold child prims.Nested targets can be accepted, but a target that also names a prim created by another target fails with
ErrorCode.OP_FAILEDbefore cloning. For example, targets/World/Tand/World/T/Innerconflict when the source contains a childInner.Relationship targets, path values, and attribute connections inside the source subtree are remapped into each copy. Paths outside it remain unchanged.
Every copied column has change records, including prim type, applied schemas, ancestor types, connection-only attributes, and each stored type of a mixed-type attribute name. If a name has both a value and a connection, it is tracked under the value type. Parent-child lists are not change-tracked. Watermarks on copied metadata also gate the clone.
Cloned prims attach to their remapped hierarchy parent. If that parent is absent, they attach to the nearest existing ancestor.
A new target root can still have existing descendants, for example after an older root was deleted. Cloning merges into those descendants. They keep attributes absent from the source, combine applied schemas and ancestor types, and retain their type when the source has none. A conflicting attribute or prim type, or a missing connection-only destination attribute, fails the clone before creation. If the source has both a value and connection under one name, the connection is copied only if the existing prim already has one. Delete existing descendants first when you need an exact copy.
These snippets demonstrate duplicate-target rejection. The runnable example
has already created /World/Proto/Rig with one prim at ordinal 2 and advanced
the watermark to 3. The repeated target is rejected before cloning starts:
import numpy as np
import ovstage
from ovstage import (AttributeSemantic, DLDataType, DLDataTypeCode, OrdinalRange, OvstageError,
PrimMode, WriteDesc, make_dltensor)
# A target path may appear only once in a clone request.
try:
stage.clone_async("/World/Proto/Rig", ["/World/Duplicate/Rig", "/World/Duplicate/Rig"],
ordinal=4).wait()
except OvstageError as error:
if error.code != ovstage.ErrorCode.INVALID_ARGUMENT:
raise
print("Duplicate targets rejected before cloning.")
# Duplicate targets rejected before cloning.
else:
raise RuntimeError("Expected duplicate targets to be rejected")
// A target path may appear only once in a clone request.
const ovx_string_t targets[] = { literal_to_ovx_string("/World/Duplicate/Rig"),
literal_to_ovx_string("/World/Duplicate/Rig") };
const auto operation = ovstage_clone(stage, literal_to_ovx_string("/World/Proto/Rig"), targets, 2, 4);
if (operation.status != OVSTAGE_ERROR_INVALID_ARGUMENT)
{
std::fprintf(stderr, "Expected duplicate targets to be rejected\n");
std::exit(EXIT_FAILURE);
}
// Submission failed, so there is no pending operation to wait for or release.
std::printf("Duplicate targets rejected before cloning.\n");
// Duplicate targets rejected before cloning.
Waiting and Results#
ovstage.Stage.clone() blocks and returns ovstage.CloneResult.
ovstage.Stage.clone_async() returns ovstage.CloneOperation.
Work can execute before you wait; wait() observes completion.
Use stage.clone_async(...).wait().result() to get the report.
The report contains prims_cloned across all targets, target_count,
source_prim_count including the source root, and ordinal.
prims_cloned sums the targets’ hierarchy subtree sizes after cloning,
including existing attached descendants. Nested targets can count a prim more
than once. For disjoint targets with no existing descendants, it equals
source_prim_count * target_count.
In C, wait with ovstage_wait_op(), fetch the report with
ovstage_fetch_clone_result(), then release operation tracking with
ovstage_release_op(). Fetching does not release the report; it remains
available until the operation is released.
These snippets submit a clone of that single-prim source, then wait and inspect its report. Python caches the report before releasing operation tracking; C fetches the report before releasing it:
# Submission returns an operation; wait when the copies are needed.
operation = stage.clone_async("/World/Proto/Rig", ["/World/Async/Rig"], ordinal=4)
# Independent application work can run here while the clone is pending.
report = operation.wait().result()
print("Async clone copied", report.prims_cloned, "prim to", report.target_count, "target")
# Async clone copied 1 prim to 1 target
# Advance the watermark before reading copied values.
check() checks API status; checkWait() checks operation errors.
const ovx_string_t target = literal_to_ovx_string("/World/Async/Rig");
const auto operation = ovstage_clone(stage, literal_to_ovx_string("/World/Proto/Rig"), &target, 1, 4);
check(stage, operation.status, "submit clone");
// Independent application work can run here while the clone is pending.
ovstage_op_wait_result_t wait{};
check(stage, ovstage_wait_op(stage, operation.op_index, OVSTAGE_TIMEOUT_INFINITE, &wait), "wait clone");
checkWait(stage, wait, "clone completion");
ovstage_clone_result_t report{};
check(stage, ovstage_fetch_clone_result(stage, operation.op_index, OVSTAGE_TIMEOUT_INFINITE, &report),
"fetch clone report");
std::printf("Async clone copied %llu prim to %llu target\n",
static_cast<unsigned long long>(report.prims_cloned), static_cast<unsigned long long>(report.target_count));
// Async clone copied 1 prim to 1 target
check(stage, ovstage_release_op(stage, operation.op_index), "release clone operation");
// Advance the watermark before reading copied values.
Clone Failures#
A failed wait reports ErrorCode.OP_FAILED and a message. Fetching the report
of a failed operation exposes the more specific code:
ErrorCode.NOT_FOUND— missing source.ErrorCode.PRIM_NOT_FOUND— a target already exists.ErrorCode.WRITE_FLOOR_VIOLATION— the ordinal conflicts with an affected watermark or retained target-cell/prim-lifecycle change under the configured admission policy. Execution failures can surface asOP_FAILEDfrom wait; inspect the diagnostic as described in Error Handling and Diagnostics.
Python’s CloneOperation.wait() and result() release native operation
tracking on completion. To inspect a specific failure code, call
ovstage.Stage.fetch_clone_result() before that release. After a failed
wait(), the native report is no longer available. Successful reports are
cached by CloneOperation. C callers can wait and fetch before releasing.
These snippets attempt to clone a missing source and fetch the specific
NOT_FOUND failure before releasing the operation. Fetching with the default
infinite timeout waits for completion, so a separate wait is unnecessary:
operation = stage.clone_async("/World/Missing", ["/World/MissingCopy"], ordinal=4)
if not operation.ok:
operation.wait() # Raise a submission error before trying to fetch a report.
try:
# Fetch waits for completion and preserves the clone-specific failure code.
# Do not call operation.wait() first: it releases the native report.
stage.fetch_clone_result(operation)
except OvstageError as error:
if error.code != ovstage.ErrorCode.NOT_FOUND:
raise
print("Clone source was not found.")
# Clone source was not found.
else:
raise RuntimeError("Expected a missing-source clone failure")
finally:
stage.release_op(operation.op_id) # Fetching the report does not release it.
const ovx_string_t target = literal_to_ovx_string("/World/MissingCopy");
const auto operation = ovstage_clone(stage, literal_to_ovx_string("/World/Missing"), &target, 1, 4);
check(stage, operation.status, "submit clone");
// Fetch waits for completion and returns the clone-specific failure code.
ovstage_clone_result_t report{};
const auto status = ovstage_fetch_clone_result(stage, operation.op_index, OVSTAGE_TIMEOUT_INFINITE, &report);
check(stage, ovstage_release_op(stage, operation.op_index), "release failed clone operation");
if (status != OVSTAGE_ERROR_NOT_FOUND)
{
std::fprintf(stderr, "Expected a missing-source clone failure\n");
std::exit(EXIT_FAILURE);
}
std::printf("Clone source was not found.\n");
// Clone source was not found.
Reading the Completion Report#
These snippets use a separate setup: /World/A contains one prim with a
value written at ordinal 1, and the watermark is 1. Python shows both the
blocking report and the asynchronous operation’s cached result; C fetches before
releasing operation tracking:
# A blocking clone returns its completion report: the targets' scene
# hierarchy subtree sizes after cloning, including each target root,
# summed over all targets, alongside the inputs it was given.
report = stage.clone("/World/A", ["/World/A_env2", "/World/A_env3"], ordinal=2)
assert isinstance(report, CloneResult)
assert report.prims_cloned == 2 # one source prim, two targets
assert report.target_count == 2
assert report.source_prim_count == 1
assert report.ordinal == 2
# The asynchronous form: wait() returns the operation, result() its report.
op = stage.clone_async("/World/A", ["/World/A_env4", "/World/A_env5"], ordinal=2)
assert op.wait().result().prims_cloned == 2
const ovx_string_t targets[] = { str("/World/A_env0"), str("/World/A_env1") };
const ovstage_enqueue_result_t enq = ovstage_clone(stage_, str("/World/A"), targets, 2, 2);
ASSERT_EQ(enq.status, OVSTAGE_OK);
ovstage_op_wait_result_t wait{};
ASSERT_EQ(ovstage_wait_op(stage_, enq.op_index, OVSTAGE_TIMEOUT_INFINITE, &wait), OVSTAGE_OK);
ASSERT_EQ(wait.error_op_id_count, 0u);
// The completion report is per-op state: fetch it after the wait and before release_op.
ovstage_clone_result_t report{};
ASSERT_EQ(ovstage_fetch_clone_result(stage_, enq.op_index, OVSTAGE_TIMEOUT_INFINITE, &report), OVSTAGE_OK);
EXPECT_EQ(report.prims_cloned, 2u); // one source prim, two targets
EXPECT_EQ(report.target_count, 2u);
EXPECT_EQ(report.source_prim_count, 1u);
EXPECT_EQ(report.ordinal, 2u);
ovstage_release_op(stage_, enq.op_index);
Further Reading#
Asynchronous Operations — driving the clone enqueue to completion.
Population (USD → ovstage) — ingesting USD content before cloning.
Runtime Loop — a full populate/read/update loop.