Cloning: Replicate Environments#
This tutorial shows how to use the clone API to replicate sub-sections of a USD scene. Cloning creates copies in the internal physics representation (not USD prims), optimized for large-scale parallel simulation.
Prerequisites#
Complete the Hello World tutorial.
Use a USD scene with a hierarchy suitable for cloning (e.g.
/World/envs/env0).
Key Concepts#
Two ways to replicate. ovphysx offers two cloning paths:
Direct ovphysx API (
PhysX.clone()/ovphysx_clone()) — the approach used in this tutorial and the supported 0.5 path for tensor-based multi-environment workloads. After an ovstage-backed scene is populated and attached,clone()copies a source subtree in the internal physics representation. No USD prims are created, so it stays fast and memory-light at large environment counts.ovstage duplication (
ovstage.Stage.clone()/ovstage_clone()) — apps that own the ovstageStagecan duplicate the source subtree and drain the clone delta withupdate_from_ovstage(). This creates the physics objects, but in ovphysx 0.5 TensorBindingsAPI does not discover objects created by the clone delta. Complete TensorBindingsAPI support for this path is scheduled for ovstage after 0.5.
See Scene Cloning for the full comparison.
Tensor binding limitation in 0.5. For clones that need tensor bindings, use
the direct clone() API and complete cloning before warmup_gpu() or the first
simulation step. Direct cloning invalidates existing tensor and contact
bindings; destroy and recreate them before use. Do not also drain a duplicate
ovstage clone delta for the same target paths.
Clone before warmup. All clone() calls must happen before GPU warmup and before the first step(). Cloning after warmup_gpu() or the first step reallocates DirectGPU buffers and would corrupt already-initialized solver state, so the runtime rejects it with OVPHYSX_API_INVALID_ARGUMENT (surfaced in Python as RuntimeError). If you must clone later, call reset_stage() first. See GPU Warmup and Determinism.
Grouping copies with env_ids. A single clone() call numbers its copies automatically. When one logical environment is assembled from several clone() calls (for example, first every environment’s robot, then every environment’s object), pass the optional per-target env_ids with the same ids in every call, so copies sharing an id land in the same runtime environment and can collide. Without env_ids, objects cloned by different calls never share an environment.
Code Language#
Python#
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause
#
# NOTE: This file is included verbatim in documentation via literalinclude.
"""
Clone sample demonstrating scene replication with the clone API.
This sample demonstrates:
1. Loading a USD scene with an environment hierarchy
2. Cloning the environment to create multiple copies
3. Running simulation with all clones
"""
from pathlib import Path
import numpy as np
from ovphysx import PhysX
from ovphysx.types import TensorType
def attach_scene(physx, usd_path, stage_name):
import ovstage
if not ovstage.population.available():
raise RuntimeError("ovstage population bridge is unavailable")
stage = ovstage.Stage(stage_name)
ordinal = 1
try:
ovstage.population.open_usd(stage, str(usd_path), ordinal=ordinal, domains=ovstage.PopulationDomain.PHYSICS)
physx.attach_ovstage(stage, read_ordinal=ordinal)
return stage
except Exception:
stage.destroy()
raise
def main():
# Initialize PhysX SDK
PhysX.set_cpu_mode(True)
physx = PhysX()
stage = None
pose_binding = None
try:
# Load USD scene containing /World/envs/env0
script_dir = Path(__file__).resolve().parent
usd_path = script_dir / ".." / "data" / "basic_simulation.usda"
print(f"Loading USD scene through ovstage: {usd_path}")
stage = attach_scene(physx, usd_path, "ovphysx-clone-sample")
physx.wait_all()
# Clone env0 to create env1, env2, env3
targets = ["/World/envs/env1", "/World/envs/env2", "/World/envs/env3"]
print(f"Cloning /World/envs/env0 to {len(targets)} targets...")
physx.clone("/World/envs/env0", targets)
physx.wait_all()
print(f" Created {len(targets)} clones successfully")
# Create a tensor binding to read rigid body poses across all environments
# The pattern matches the "table" rigid body in every cloned environment
pose_binding = physx.create_tensor_binding(
pattern="/World/envs/env*/table",
tensor_type=TensorType.RIGID_BODY_POSE,
)
print(f" Rigid body binding: count={pose_binding.count}, shape={pose_binding.shape}")
# Run simulation with all environments
print("Running 10 simulation steps...")
dt = 1.0 / 60.0
for i in range(10):
physx.step(dt)
physx.wait_all()
print(" All steps completed")
# Read poses from all environments after simulation
poses = np.zeros(pose_binding.shape, dtype=np.float32)
pose_binding.read(poses)
for env_idx in range(poses.shape[0]):
px, py, pz = poses[env_idx, 0:3]
print(f" env{env_idx}: pos=({px:.4f}, {py:.4f}, {pz:.4f})")
print("Clone sample completed successfully")
finally:
if pose_binding is not None:
pose_binding.destroy()
if stage is not None:
physx.detach_ovstage()
stage.destroy()
physx.release()
print("Cleanup complete")
if __name__ == "__main__":
main()
C#
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
//
// NOTE: This file is included verbatim in documentation via literalinclude.
#include <ovphysx/ovphysx.h>
#include <ovphysx/ovphysx_types.h>
#include "ovstage_sample.h"
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#define sleep_ms(ms) Sleep(ms)
#else
#include <unistd.h>
#define sleep_ms(ms) usleep((ms) * 1000)
#endif
// Compile-time check: fail compilation if C++ compiler is used
#ifdef __cplusplus
#error "This file must be compiled as C, not C++"
#endif
static int wait_op_success(ovphysx_handle_t handle, ovphysx_enqueue_result_t res, uint64_t timeout_ns) {
if (res.status != OVPHYSX_API_SUCCESS) {
return 0;
}
ovphysx_op_wait_result_t wait_result = {0};
ovphysx_result_t wait_res = ovphysx_wait_op(handle, res.op_index, timeout_ns, &wait_result);
int success = (wait_res.status == OVPHYSX_API_SUCCESS && wait_result.num_errors == 0);
ovphysx_destroy_wait_result(&wait_result);
return success;
}
static int run(void)
{
printf("=== ovphysx Clone Example (C API) ===\n\n");
ovphysx_result_t init_res = ovphysx_initialize();
if (init_res.status != OVPHYSX_API_SUCCESS) {
fprintf(stderr, "Failed to initialize ovphysx\n");
return 1;
}
// Initialize create args with defaults
ovphysx_create_args create_args = OVPHYSX_CREATE_ARGS_DEFAULT;
// Create PhysX instance
printf("Creating PhysX instance...\n");
ovphysx_handle_t handle = 0;
ovphysx_result_t create_res = ovphysx_create_instance(&create_args, &handle);
if (create_res.status != OVPHYSX_API_SUCCESS) {
fprintf(stderr, "Failed to create PhysX instance\n");
ovphysx_shutdown();
return 1;
}
printf(" [OK] PhysX instance created\n\n");
// Populate ovstage from USD and attach it to ovphysx
printf("Loading USD scene...\n");
ovphysx_sample_stage_attachment_t stage_attachment = {0};
if (!ovphysx_sample_attach_usd_with_ovstage(
handle, OVPHYSX_TEST_DATA "/basic_simulation.usda", &stage_attachment)) {
fprintf(stderr, "ovstage attach/update failed\n");
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
printf(" [OK] USD scene loaded\n\n");
// Clone the environment (source: env0, targets: env1, env2, env3)
printf("Cloning /World/envs/env0 to create env1, env2, env3...\n");
const char* clone_targets[] = {
"/World/envs/env1",
"/World/envs/env2",
"/World/envs/env3"
};
enum { NUM_TARGETS = 3 };
ovphysx_string_t target_strings[NUM_TARGETS];
for (uint32_t i = 0; i < NUM_TARGETS; ++i) {
target_strings[i] = ovphysx_cstr(clone_targets[i]);
}
ovphysx_enqueue_result_t clone_res = ovphysx_clone(
handle,
ovphysx_cstr("/World/envs/env0"),
target_strings,
NUM_TARGETS,
NULL, /* parent_transforms: co-locate on the source */
NULL); /* env_ids: automatic per-call numbering */
if (!wait_op_success(handle, clone_res, 10ULL * 1000 * 1000 * 1000)) {
fprintf(stderr, "Clone operation failed or timed out\n");
ovphysx_sample_destroy_stage(handle, &stage_attachment);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
printf(" [OK] Created 3 clones successfully\n\n");
// Run a few simulation steps to verify clones work correctly
printf("Running simulation with clones (10 steps)...\n");
for (int i = 0; i < 10; i++) {
ovphysx_enqueue_result_t step_res = ovphysx_step(handle, 1.0f/60.0f);
if (!wait_op_success(handle, step_res, 10ULL * 1000 * 1000 * 1000)) {
fprintf(stderr, "Failed to run simulation step %d\n", i);
ovphysx_sample_destroy_stage(handle, &stage_attachment);
ovphysx_destroy_instance(handle);
ovphysx_shutdown();
return 1;
}
}
printf(" [OK] All 10 simulation steps completed successfully\n\n");
printf("=== Clone Example Completed Successfully ===\n");
ovphysx_sample_destroy_stage(handle, &stage_attachment);
ovphysx_result_t destroy_res = ovphysx_destroy_instance(handle);
ovphysx_shutdown();
printf("Cleanup complete\n");
return 0;
}
int main(void) {
int rc = run();
return rc;
}
Result#
After this tutorial, you can replicate environments via the clone API and simulate all copies together.