# 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](hello_world.md) 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 ovstage `Stage` can duplicate the source subtree and drain the clone delta with `update_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](../developer_guide.md#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](../developer_guide.md#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

```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

```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.
