From 3171b0bd56b1789673b0d56d954b0bcb59ef1b8d Mon Sep 17 00:00:00 2001 From: ooctipus Date: Thu, 24 Sep 2026 19:27:21 -0700 Subject: [PATCH] [PR5A] Require explicit Newton construction at startup (#8005) ## Summary Newton startup now requires an explicitly supplied builder instead of silently discovering and importing the USD stage when none exists. - The cable/deformable examples and deformable tutorial declare `@configclass` scene cfgs. Randomized asset declarations are completed in `__post_init__`; `num_envs` and `env_spacing` are constructor arguments. `scene_cfg.class_type(scene_cfg)` constructs the scene after launch; `InteractiveScene` owns spawning and replication. Converter previews also use `InteractiveScene`. Explicit clone orchestration stays in tests and cloner documentation. - Preserved develop's packaged CLI examples and consolidated robot zoo; the deleted robot demos are not restored. No `AGENTS.md` changes. - Native tools can continue using `NewtonManager.set_builder(builder)` without a plan. The explicit stage-import API remains available. - `SimulationContext.reset()` and `play()` are unchanged. Empty simulations and native-only PhysX/OVPhysX callers do not need dummy plans. `create_empty.py` is unchanged from develop. - Previews import their declared assets through the scene lifecycle instead of relying on the fallback. - Fixed duplicate import of globally declared native deformables, exposed by using those previews' existing plans. The exclusion is limited to declared roots. No blanket initialization guards or unrelated empty-scene migrations remain. PR5B (#7995) handles geometry transport through SDP separately. ## Validation - Merged develop `4d7e1dbef`, preserving its example packaging and test consolidation. All 16 remaining CPU script checks and eight focused Newton manager checks passed. No new test functions remain in this PR; existing fixtures were migrated, and one particle-count assertion covers duplicate deformable import. - The packaged zoo and deformable examples passed headless Kit/PhysX launch, stepping and clean shutdown on GPU 1. The packaged cable example passed two CPU/Newton steps. The four-cube tutorial passed reset and three Newton GPU steps, including its controlled-corner checks. - Five seeded comparisons preserved randomized geometry, material values, ordering and random state after moving asset declarations into `__post_init__`. Constructor-only checks now return all requested cables / 12 deformables without external cfg mutation. Tutorial cfgs matched on PhysX, OVPhysX and Newton; all three import without loading USD before launch. - Formatting and changelog checks passed. Tutorial snippet markers were checked; documentation builds remain in CI. - Earlier lifecycle validation confirmed that explicit native builders and native-only PhysX/OVPhysX startup remained supported without dummy plans. ## Performance Pre-rebase matched warm sample after running each checkout once, physical GPU 1 (RTX 5090), `Isaac-Cartpole-Direct`, 4,096 environments, Newton MJWarp, no visualizer, seed 0. Develop: `1e520e9d5`; candidate: `ad0296a06`. Both used identical dependencies and verified checkout imports. Timings have not been rerun for the conflict-only rebase. Startup includes imports through the first step. Runtime excludes 50 warmup steps and measures 200 full environment steps, synchronizing GPU work at the window boundaries. | Metric | PR5A | develop | |---|---:|---:| | Startup | 5.995 s | 5.899 s | | Environment creation and initial reset | 4.607 s | 4.548 s | | Runtime step | 1.611 ms | 1.641 ms | | Aggregate environment FPS | 2,542,219 | 2,496,304 | Differences are approximately 2% or less; this single pair does not establish a speedup or regression. No stepping work was added. ## Checklist - [x] Followed contribution guidelines and ran formatting checks - [x] Updated documentation and package changelog fragments - [x] Extended existing regression coverage and migrated affected callers - [x] Backport to the active release branch --- docs/source/how-to/cloning.rst | 9 + docs/source/how-to/run_deformable_object.rst | 25 ++- examples/cables.py | 133 +++++++------ examples/deformables.py | 188 ++++++++---------- scripts/tools/convert_mjcf.py | 16 +- scripts/tools/convert_urdf.py | 16 +- .../01_assets/run_deformable_object.py | 116 +++++------ .../changelog.d/clone-lifecycle.skip | 1 + .../custom_coupling/test_manual_coupling.py | 131 ++++++------ .../test/deformable/test_deformable_object.py | 42 ++-- .../changelog.d/clone-lifecycle.major.rst | 12 ++ .../isaaclab_newton/cloner/replicate.py | 5 +- .../isaaclab_newton/physics/newton_manager.py | 12 +- .../test/assets/test_articulation.py | 181 ++++++++++++----- .../test/assets/test_cable_object.py | 6 + .../assets/test_newton_actuators_newton.py | 69 ++++++- .../test/assets/test_rigid_object.py | 62 ++++-- .../assets/test_rigid_object_collection.py | 67 +++++-- .../cloner/test_newton_builder_world_hook.py | 18 +- .../physics/test_newton_fabric_body_sync.py | 4 + .../test_newton_manager_abstraction.py | 6 +- .../test/physics/test_newton_solver_reset.py | 14 +- .../sensors/test_newton_raycast_sensor.py | 4 + .../test/sim/test_views_xform_prim_newton.py | 5 + 24 files changed, 670 insertions(+), 472 deletions(-) create mode 100644 source/isaaclab_contrib/changelog.d/clone-lifecycle.skip create mode 100644 source/isaaclab_newton/changelog.d/clone-lifecycle.major.rst diff --git a/docs/source/how-to/cloning.rst b/docs/source/how-to/cloning.rst index c1b462701b48..330065897549 100644 --- a/docs/source/how-to/cloning.rst +++ b/docs/source/how-to/cloning.rst @@ -121,6 +121,15 @@ visuals, PhysX's native replicator for rigid bodies and articulations, Newton's world system for its parallel pipeline. The same plan drives all of them, so user code never branches on the backend. +Newton startup requires a builder; it no longer imports the USD stage implicitly. +``InteractiveScene`` handles planning and replication internally; use it for maintained +demos, tutorials, and asset previews. The explicit cloner examples below are for tests +and code that teaches the cloner API. Native tools can instead supply a builder with +``NewtonManager.set_builder(builder)``. + +Require a plan where a consumer uses it, not merely because simulation initializes. +Empty PhysX simulations and tools that supply a native Newton builder need no dummy plan. + ClonePlan ~~~~~~~~~ diff --git a/docs/source/how-to/run_deformable_object.rst b/docs/source/how-to/run_deformable_object.rst index e37be5174e0d..749efc827cee 100644 --- a/docs/source/how-to/run_deformable_object.rst +++ b/docs/source/how-to/run_deformable_object.rst @@ -52,7 +52,7 @@ The tutorial corresponds to the ``run_deformable_object.py`` script in the ``scr .. literalinclude:: ../../../scripts/tutorials/01_assets/run_deformable_object.py :language: python - :emphasize-lines: 71-117, 146-151, 153-162, 167-175, 177-178, 184-189 + :emphasize-lines: 88-112, 135-151, 156-164, 173-178, 191-192 :linenos: @@ -62,10 +62,10 @@ The Code Explained Designing the scene ------------------- -Similar to the :ref:`tutorial-interact-rigid-object` tutorial, we populate the scene with a ground plane -and a light source. In addition, we add a deformable object to the scene using the :class:`assets.DeformableObject` -class. This class is responsible for spawning the prims at the input path and initializes their corresponding -deformable body physics handles. +We declare the ground plane, light, and deformable cube in a subclass of :class:`scene.InteractiveSceneCfg`. +:class:`scene.InteractiveScene` constructs the assets and handles replication internally. +``DeformableSceneCfg(num_envs=4, env_spacing=0.5)`` selects four environment origins on a centered grid +with 0.5 m spacing. In this tutorial, we create a cubical soft object using the spawn configuration similar to the deformable cube in the :ref:`Spawn Objects ` tutorial. The only difference is that now we wrap @@ -81,13 +81,13 @@ when the simulation is played. implementation. -As seen in the rigid body tutorial, we can spawn the deformable object into the scene in a similar fashion by creating -an instance of the :class:`assets.DeformableObject` class by passing the configuration object to its constructor. +The scene constructs the deformable objects from their cfg and owns their clone lifecycle. +The simulation loop accesses the resulting asset through ``scene["cube_object"]``. .. literalinclude:: ../../../scripts/tutorials/01_assets/run_deformable_object.py :language: python - :start-at: # Create separate groups called "env_0", "env_1", ... - :end-at: cube_object = DeformableObject(cfg=cfg) + :start-at: @configclass + :end-before: def run_simulator( Running the simulation loop --------------------------- @@ -104,7 +104,7 @@ are defined in the **simulation world frame** and are stored in the :attr:`asset We use the :attr:`assets.DeformableObject.data.default_nodal_state_w` attribute to get the default nodal state of the spawned object prims. This default state can be configured from the :attr:`assets.DeformableObjectCfg.init_state` -attribute, which we left as identity in this tutorial. +attribute, which places the cube 1 m above its environment origin in this tutorial. .. attention:: The initial state in the configuration :attr:`assets.DeformableObjectCfg` specifies the pose @@ -148,7 +148,7 @@ method. .. literalinclude:: ../../../scripts/tutorials/01_assets/run_deformable_object.py :language: python - :start-at: # update the kinematic target for cubes at index 0 and 3 + :start-at: # update the kinematic target for cubes at the positive and negative diagonal corners :end-at: cube_object.write_nodal_kinematic_target_to_sim_index(nodal_kinematic_target) Similar to the rigid object and articulation, we perform the :meth:`assets.DeformableObject.write_data_to_sim` method @@ -226,8 +226,7 @@ To stop the simulation, you can either close the window, or press ``Ctrl+C`` in This tutorial showed how to spawn deformable objects and wrap them in a :class:`DeformableObject` class to initialize their physics handles which allows setting and obtaining their state. We also saw how to apply kinematic commands to the deformable object to move the mesh nodes in a controlled manner. The ``deformables`` example provides a more advanced -example, including surface deformables, loading USD assets, and applying deformable materials. In the next tutorial, we will see how to create -a scene using the :class:`InteractiveScene` class. +example, including surface deformables, loading USD assets, and applying deformable materials. .. _PhysX documentation: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html .. _partial kinematic: https://nvidia-omniverse.github.io/PhysX/physx/5.4.1/docs/SoftBodies.html#kinematic-soft-bodies diff --git a/examples/cables.py b/examples/cables.py index 685d3266f3b8..e38093bc16ab 100644 --- a/examples/cables.py +++ b/examples/cables.py @@ -38,73 +38,74 @@ parser.error("--num_segments must be at least 2.") import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg, CableObjectCfg +from isaaclab.cloner import CloneCfg from isaaclab.physics import PhysicsCfg +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.utils import configclass if TYPE_CHECKING: from isaaclab.assets import CableObject + from isaaclab.scene import InteractiveScene -def design_scene(num_cables: int, num_segments: int, colorize: bool) -> dict[str, CableObject]: - """Spawn a ground plane, light, and randomly oriented cable pile. - - Args: - num_cables: Number of cables to spawn. - num_segments: Number of segments per cable. - colorize: Whether to give each cable a random visual material. - """ - from isaaclab.assets import CableObject, CableObjectCfg - - ground_cfg = sim_utils.GroundPlaneCfg() - ground_cfg.func("/World/defaultGroundPlane", ground_cfg) - light_cfg = sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) - light_cfg.func("/World/light", light_cfg) - - cable_length = 0.5 - segment_length = cable_length / num_segments - thickness = 0.01 - radius = 0.5 * thickness - target_stretch_stiffness = 5.0e5 # [N/m] per joint - target_bend_stiffness = 20.0 # [N.m/rad] per joint - stretch_modulus = target_stretch_stiffness * segment_length / (math.pi * radius**2) - bend_modulus = target_bend_stiffness * segment_length / (0.25 * math.pi * radius**4) - xy_jitter = 0.3 - z_spacing = 1.5 * thickness - z_base = 0.8 - positions = [(index * segment_length, 0.0, 0.0) for index in range(num_segments + 1)] - - print(f"[INFO]: Spawning {num_cables} cables...") - entities: dict[str, CableObject] = {} - for index in range(num_cables): - angle = random.uniform(0.0, 2.0 * math.pi) - position = ( - random.uniform(-xy_jitter, xy_jitter) - 0.5 * cable_length * math.cos(angle), - random.uniform(-xy_jitter, xy_jitter) - 0.5 * cable_length * math.sin(angle), - z_base + index * z_spacing, - ) - orientation = (0.0, 0.0, math.sin(0.5 * angle), math.cos(0.5 * angle)) - visual_material = None - if colorize: - visual_material = sim_utils.PreviewSurfaceCfg( - diffuse_color=(random.random(), random.random(), random.random()) +@configclass +class CablesSceneCfg(InteractiveSceneCfg): + """A randomized cable pile with ground and lighting.""" + + clone_cfg = CloneCfg(clone_template="/World/Env_{}") + + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + light = AssetBaseCfg( + prim_path="/World/light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + + def __post_init__(self): + """Configure cable geometry and sample the initial pile.""" + colorize = bool(args_cli.visualizer and "kit" in args_cli.visualizer) + cable_length = 0.5 + segment_length = cable_length / args_cli.num_segments + thickness = 0.01 + radius = 0.5 * thickness + target_stretch_stiffness = 5.0e5 # [N/m] per joint + target_bend_stiffness = 20.0 # [N.m/rad] per joint + stretch_modulus = target_stretch_stiffness * segment_length / (math.pi * radius**2) + bend_modulus = target_bend_stiffness * segment_length / (0.25 * math.pi * radius**4) + xy_jitter = 0.3 + z_spacing = 1.5 * thickness + z_base = 0.8 + positions = [(index * segment_length, 0.0, 0.0) for index in range(args_cli.num_segments + 1)] + + print(f"[INFO]: Spawning {args_cli.num_cables} cables...") + for index in range(args_cli.num_cables): + angle = random.uniform(0.0, 2.0 * math.pi) + position = ( + random.uniform(-xy_jitter, xy_jitter) - 0.5 * cable_length * math.cos(angle), + random.uniform(-xy_jitter, xy_jitter) - 0.5 * cable_length * math.sin(angle), + z_base + index * z_spacing, ) - cfg = CableObjectCfg( - prim_path=f"/World/Env_0/Cable{index:03d}", - spawn=sim_utils.CableCfg( - positions=positions, - visual_material=visual_material, - physics_material=sim_utils.CableMaterialCfg( - thickness=thickness, - density=100.0, - stretch_stiffness=stretch_modulus, - bend_stiffness=bend_modulus, + orientation = (0.0, 0.0, math.sin(0.5 * angle), math.cos(0.5 * angle)) + visual_material = None + if colorize: + visual_material = sim_utils.PreviewSurfaceCfg( + diffuse_color=(random.random(), random.random(), random.random()) + ) + cfg = CableObjectCfg( + prim_path=f"{{ENV_REGEX_NS}}/Cable{index:03d}", + spawn=sim_utils.CableCfg( + positions=positions, + visual_material=visual_material, + physics_material=sim_utils.CableMaterialCfg( + thickness=thickness, + density=100.0, + stretch_stiffness=stretch_modulus, + bend_stiffness=bend_modulus, + ), + collision_props=[sim_utils.UsdPhysicsCollisionCfg(collision_enabled=True)], ), - collision_props=[sim_utils.UsdPhysicsCollisionCfg(collision_enabled=True)], - ), - init_state=CableObjectCfg.InitialStateCfg(pos=position, rot=orientation), - ) - entities[f"cable_{index:03d}"] = CableObject(cfg=cfg) - - return entities + init_state=CableObjectCfg.InitialStateCfg(pos=position, rot=orientation), + ) + setattr(self, f"cable_{index:03d}", cfg) def reset_cables(entities: dict[str, CableObject]) -> None: @@ -114,7 +115,7 @@ def reset_cables(entities: dict[str, CableObject]) -> None: cable.write_segment_velocity_to_sim_index(segment_velocity=cable.data.default_segment_velocity_w) -def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, CableObject], max_steps: int = -1) -> None: +def run_simulator(sim: sim_utils.SimulationContext, scene: InteractiveScene, max_steps: int = -1) -> None: """Run the simulation and periodically restore the cable pile.""" sim_dt = sim.get_physics_dt() reset_steps = max(1, int(2.0 / sim_dt)) @@ -122,12 +123,11 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict[str, CableObj while (max_steps < 0 or count < max_steps) and sim.is_headless_or_exist_active_visualizer(): if count > 0 and count % reset_steps == 0: - reset_cables(entities) + reset_cables(scene.cable_objects) print("[INFO]: Resetting cable state...") sim.step(render=False) - for cable in entities.values(): - cable.update(sim_dt) + scene.update(sim_dt) if sim.is_rendering: sim.render() count += 1 @@ -141,11 +141,12 @@ def main() -> None: sim_cfg = sim_utils.SimulationCfg(dt=0.01, device=args_cli.device, physics=physics_cfg) sim = sim_utils.SimulationContext(sim_cfg) sim.set_camera_view(eye=(2.0, 2.0, 1.0), target=(0.0, 0.0, 0.25)) - colorize = bool(args_cli.visualizer and "kit" in args_cli.visualizer) - entities = design_scene(args_cli.num_cables, args_cli.num_segments, colorize) + + scene_cfg = CablesSceneCfg(num_envs=1, env_spacing=0.0) + scene = scene_cfg.class_type(scene_cfg) sim.reset() print("[INFO]: Setup complete...") - run_simulator(sim, entities, args_cli.max_steps) + run_simulator(sim, scene, args_cli.max_steps) if __name__ == "__main__": diff --git a/examples/deformables.py b/examples/deformables.py index 56b8ddda4fcb..f898fbd2964c 100644 --- a/examples/deformables.py +++ b/examples/deformables.py @@ -50,6 +50,9 @@ import tqdm import isaaclab.sim as sim_utils +from isaaclab.assets import AssetBaseCfg +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.utils import configclass from isaaclab.assets import DeformableObjectCfg # isort:skip from isaaclab.physics import PhysicsCfg # isort:skip @@ -77,145 +80,122 @@ ) -def define_origins(num_origins: int, radius: float = 2.0, center_height: float = 3.0) -> list[list[float]]: - """Defines origins distributed on the surface of a sphere, sampled according to a Fibonacci lattice. - - Args: - num_origins: Number of points to place. - radius: Radius of the sphere [m]. - center_height: Height of the sphere center above ground [m]. - """ - golden_ratio = (1 + np.sqrt(5)) / 2 - env_origins = torch.zeros(num_origins, 3) - for i in range(num_origins): - theta = 2 * np.pi * i / golden_ratio - phi = np.arccos(1 - 2 * (i + 0.5) / num_origins) - env_origins[i, 0] = radius * np.cos(theta) * np.sin(phi) - env_origins[i, 1] = radius * np.sin(theta) * np.sin(phi) - env_origins[i, 2] = radius * np.cos(phi) + center_height - return env_origins.tolist() - - -def design_scene() -> tuple[dict, list[list[float]]]: - """Designs the scene.""" - # Ground-plane - cfg_ground = sim_utils.GroundPlaneCfg() - cfg_ground.func("/World/defaultGroundPlane", cfg_ground) - - # spawn distant light - cfg_light = sim_utils.DomeLightCfg( - intensity=3000.0, - color=(0.75, 0.75, 0.75), - ) - cfg_light.func("/World/light", cfg_light) - - # spawn a red cone - cfg_sphere = sim_utils.MeshSphereCfg( +OBJECT_CFGS = { + "sphere": sim_utils.MeshSphereCfg( radius=0.4, deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=VolumeDeformableMaterialCfg(), - ) - cfg_cuboid = sim_utils.MeshCuboidCfg( + ), + "cuboid": sim_utils.MeshCuboidCfg( size=(0.6, 0.6, 0.6), deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=VolumeDeformableMaterialCfg(), - ) - cfg_cylinder = sim_utils.MeshCylinderCfg( + ), + "cylinder": sim_utils.MeshCylinderCfg( radius=0.25, height=0.5, deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=VolumeDeformableMaterialCfg(), - ) - cfg_capsule = sim_utils.MeshCapsuleCfg( + ), + "capsule": sim_utils.MeshCapsuleCfg( radius=0.35, height=0.5, deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=VolumeDeformableMaterialCfg(), - ) - cfg_cone = sim_utils.MeshConeCfg( + ), + "cone": sim_utils.MeshConeCfg( radius=0.35, height=0.75, deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=VolumeDeformableMaterialCfg(), - ) - cfg_cloth = sim_utils.MeshRectangleCfg( + ), + "cloth": sim_utils.MeshRectangleCfg( size=(1.5, 1.0), edge_refinement=21, deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=SurfaceDeformableMaterialCfg(), - ) - cfg_usd = sim_utils.UsdFileCfg( + ), + "usd": sim_utils.UsdFileCfg( usd_path=f"{ISAACLAB_NUCLEUS_DIR}/Objects/Teddy_Bear/teddy_bear.usd", deformable_props=DeformableBodyPropertiesCfg(), visual_material=sim_utils.PreviewSurfaceCfg(), physics_material=VolumeDeformableMaterialCfg(), scale=[0.05, 0.05, 0.05], + ), +} + + +@configclass +class DeformablesSceneCfg(InteractiveSceneCfg): + """Randomized deformable objects with ground and lighting.""" + + filter_collisions = False + + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + light = AssetBaseCfg( + prim_path="/World/light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) ) - # create a dictionary of all the objects to be spawned - objects_cfg = { - "sphere": cfg_sphere, - "cuboid": cfg_cuboid, - "cylinder": cfg_cylinder, - "capsule": cfg_capsule, - "cone": cfg_cone, - "cloth": cfg_cloth, - "usd": cfg_usd, - } - # Create separate groups of deformable objects - origins = define_origins(num_origins=12, radius=1.5, center_height=2.0) - print("[INFO]: Spawning objects...") - # Iterate over all the origins, spawn objects, and create a view for all the deformables - # note: since we manually spawned random deformable meshes above, we don't need to - # specify the spawn configuration for the deformable object - scene_entities = {} - for idx, origin in tqdm.tqdm(enumerate(origins), total=len(origins)): - # randomly select an object to spawn - obj_name = random.choice(list(objects_cfg.keys())) - obj_cfg = objects_cfg[obj_name] - # randomize the deformable material stiffness - if args_cli.physics == "newton_vbd" and obj_name == "cloth": - obj_cfg.physics_material.tri_ke = random.uniform(5e3, 5e4) - obj_cfg.physics_material.tri_ka = random.uniform(5e3, 5e4) - else: - youngs_modulus = random.uniform(5e5, 1e7) - poissons_ratio = random.uniform(0.25, 0.45) - if args_cli.physics == "newton_vbd": - obj_cfg.physics_material.k_mu = youngs_modulus / (2.0 * (1.0 + poissons_ratio)) - obj_cfg.physics_material.k_lambda = ( - youngs_modulus * poissons_ratio / ((1.0 + poissons_ratio) * (1.0 - 2.0 * poissons_ratio)) - ) + def __post_init__(self): + """Sample object shapes, positions, stiffnesses, and colors.""" + origins = define_origins(num_origins=12, radius=1.5, center_height=2.0) + print("[INFO]: Spawning objects...") + for idx, origin in tqdm.tqdm(enumerate(origins), total=len(origins)): + # randomly select an object to spawn + obj_name = random.choice(list(OBJECT_CFGS.keys())) + obj_cfg = OBJECT_CFGS[obj_name].copy() + # randomize the deformable material stiffness + if args_cli.physics == "newton_vbd" and obj_name == "cloth": + obj_cfg.physics_material.tri_ke = random.uniform(5e3, 5e4) + obj_cfg.physics_material.tri_ka = random.uniform(5e3, 5e4) else: - obj_cfg.physics_material.youngs_modulus = youngs_modulus - obj_cfg.physics_material.poissons_ratio = poissons_ratio - # randomize the color - obj_cfg.visual_material.diffuse_color = (random.random(), random.random(), random.random()) - # spawn the object, separate groups for surface and volume deformables - if obj_name in ["cloth"]: - prim_path = f"/World/Origin/Surface{idx:02d}" - cfg = DeformableObjectCfg( - prim_path=prim_path, - spawn=obj_cfg, - init_state=DeformableObjectCfg.InitialStateCfg(pos=origin), - ) - scene_entities[f"Surface{idx:02d}"] = cfg.class_type(cfg) - else: - prim_path = f"/World/Origin/Volume{idx:02d}" - cfg = DeformableObjectCfg( - prim_path=prim_path, - spawn=obj_cfg, - init_state=DeformableObjectCfg.InitialStateCfg(pos=origin), + youngs_modulus = random.uniform(5e5, 1e7) + poissons_ratio = random.uniform(0.25, 0.45) + if args_cli.physics == "newton_vbd": + obj_cfg.physics_material.k_mu = youngs_modulus / (2.0 * (1.0 + poissons_ratio)) + obj_cfg.physics_material.k_lambda = ( + youngs_modulus * poissons_ratio / ((1.0 + poissons_ratio) * (1.0 - 2.0 * poissons_ratio)) + ) + else: + obj_cfg.physics_material.youngs_modulus = youngs_modulus + obj_cfg.physics_material.poissons_ratio = poissons_ratio + # randomize the color + obj_cfg.visual_material.diffuse_color = (random.random(), random.random(), random.random()) + name = f"{'Surface' if obj_name == 'cloth' else 'Volume'}{idx:02d}" + setattr( + self, + name, + DeformableObjectCfg( + prim_path=f"/World/Origin/{name}", + spawn=obj_cfg, + init_state=DeformableObjectCfg.InitialStateCfg(pos=origin), + ), ) - scene_entities[f"Volume{idx:02d}"] = cfg.class_type(cfg) - # return the scene information - return scene_entities, origins + +def define_origins(num_origins: int, radius: float = 2.0, center_height: float = 3.0) -> list[list[float]]: + """Defines origins distributed on the surface of a sphere, sampled according to a Fibonacci lattice. + + Args: + num_origins: Number of points to place. + radius: Radius of the sphere [m]. + center_height: Height of the sphere center above ground [m]. + """ + golden_ratio = (1 + np.sqrt(5)) / 2 + env_origins = torch.zeros(num_origins, 3) + for i in range(num_origins): + theta = 2 * np.pi * i / golden_ratio + phi = np.arccos(1 - 2 * (i + 0.5) / num_origins) + env_origins[i, 0] = radius * np.cos(theta) * np.sin(phi) + env_origins[i, 1] = radius * np.sin(theta) * np.sin(phi) + env_origins[i, 2] = radius * np.cos(phi) + center_height + return env_origins.tolist() def run_simulator(sim: "sim_utils.SimulationContext", entities: dict[str, "DeformableObject"]): @@ -273,13 +253,13 @@ def main(): # Set main camera sim.set_camera_view([4.0, 4.0, 3.0], [0.5, 0.5, 0.0]) - # Design scene by adding assets to it - scene_entities, _ = design_scene() + scene_cfg = DeformablesSceneCfg(num_envs=1, env_spacing=0.0) + scene = scene_cfg.class_type(scene_cfg) # Play the simulator sim.reset() # Now we are ready! print("[INFO]: Setup complete...") - run_simulator(sim, scene_entities) + run_simulator(sim, scene.deformable_objects) print("[INFO]: Simulation complete...") diff --git a/scripts/tools/convert_mjcf.py b/scripts/tools/convert_mjcf.py index 414f784b358c..2c04490a1f83 100644 --- a/scripts/tools/convert_mjcf.py +++ b/scripts/tools/convert_mjcf.py @@ -100,8 +100,9 @@ import os # noqa: E402 import isaaclab.sim as sim_utils # noqa: E402 -from isaaclab import cloner # noqa: E402 +from isaaclab.assets import AssetBaseCfg # noqa: E402 from isaaclab.physics import PhysicsCfg # noqa: E402 +from isaaclab.scene import InteractiveSceneCfg # noqa: E402 from isaaclab.sim.converters import MjcfConverter, MjcfConverterCfg # noqa: E402 from isaaclab.utils.assets import check_file_path # noqa: E402 from isaaclab.utils.dict import print_dict # noqa: E402 @@ -128,13 +129,12 @@ def preview(usd_path: str, physics_cfg: PhysicsCfg) -> None: # shared scene data, so no backend-specific code is needed here. Physics is not stepped -- the # asset is shown in its imported pose until the visualizer window is closed. sim = sim_utils.SimulationContext(sim_utils.SimulationCfg(device=args_cli.device, physics=physics_cfg)) - plan = cloner.make_clone_plan((), 1, 0.0, global_paths=("/World/Light", "/World/ConvertedAsset")) - sim.set_clone_plan(plan) - light_cfg = sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) - light_cfg.func("/World/Light", light_cfg) - asset_cfg = sim_utils.UsdFileCfg(usd_path=usd_path) - asset_cfg.func("/World/ConvertedAsset", asset_cfg) - cloner.replicate(plan, replicate_physics=False) + scene_cfg = InteractiveSceneCfg(num_envs=1, env_spacing=0.0) + scene_cfg.light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + scene_cfg.asset = AssetBaseCfg(prim_path="/World/ConvertedAsset", spawn=sim_utils.UsdFileCfg(usd_path=usd_path)) + _scene = scene_cfg.class_type(scene_cfg) sim.reset() # Checked per visualizer rather than through ``SimulationContext.is_headless_or_exist_active_visualizer``: diff --git a/scripts/tools/convert_urdf.py b/scripts/tools/convert_urdf.py index 03e6f1cba5b3..901d987af83b 100644 --- a/scripts/tools/convert_urdf.py +++ b/scripts/tools/convert_urdf.py @@ -104,8 +104,9 @@ import os # noqa: E402 import isaaclab.sim as sim_utils # noqa: E402 -from isaaclab import cloner # noqa: E402 +from isaaclab.assets import AssetBaseCfg # noqa: E402 from isaaclab.physics import PhysicsCfg # noqa: E402 +from isaaclab.scene import InteractiveSceneCfg # noqa: E402 from isaaclab.sim.converters import UrdfConverter, UrdfConverterCfg # noqa: E402 from isaaclab.utils.assets import check_file_path # noqa: E402 from isaaclab.utils.dict import print_dict # noqa: E402 @@ -132,13 +133,12 @@ def preview(usd_path: str, physics_cfg: PhysicsCfg) -> None: # shared scene data, so no backend-specific code is needed here. Physics is not stepped -- the # asset is shown in its imported pose until the visualizer window is closed. sim = sim_utils.SimulationContext(sim_utils.SimulationCfg(device=args_cli.device, physics=physics_cfg)) - plan = cloner.make_clone_plan((), 1, 0.0, global_paths=("/World/Light", "/World/ConvertedAsset")) - sim.set_clone_plan(plan) - light_cfg = sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) - light_cfg.func("/World/Light", light_cfg) - asset_cfg = sim_utils.UsdFileCfg(usd_path=usd_path) - asset_cfg.func("/World/ConvertedAsset", asset_cfg) - cloner.replicate(plan, replicate_physics=False) + scene_cfg = InteractiveSceneCfg(num_envs=1, env_spacing=0.0) + scene_cfg.light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=3000.0, color=(0.75, 0.75, 0.75)) + ) + scene_cfg.asset = AssetBaseCfg(prim_path="/World/ConvertedAsset", spawn=sim_utils.UsdFileCfg(usd_path=usd_path)) + _scene = scene_cfg.class_type(scene_cfg) sim.reset() # Checked per visualizer rather than through ``SimulationContext.is_headless_or_exist_active_visualizer``: diff --git a/scripts/tutorials/01_assets/run_deformable_object.py b/scripts/tutorials/01_assets/run_deformable_object.py index 4b1b6d4d6cc1..c6ed3951e336 100644 --- a/scripts/tutorials/01_assets/run_deformable_object.py +++ b/scripts/tutorials/01_assets/run_deformable_object.py @@ -48,61 +48,58 @@ import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils +from isaaclab.assets import AssetBaseCfg, DeformableObjectCfg +from isaaclab.cloner import CloneCfg from isaaclab.physics import PhysicsCfg +from isaaclab.scene import InteractiveSceneCfg +from isaaclab.utils import configclass if TYPE_CHECKING: from isaaclab.assets import DeformableObject + from isaaclab.scene import InteractiveScene + + +youngs_modulus = 1e5 +poissons_ratio = 0.4 +density = 500.0 +if args_cli.backend == "newton_vbd": + from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg + from isaaclab_newton.sim.spawners.materials import NewtonDeformableBodyMaterialCfg + + deformable_props = NewtonDeformableBodyPropertiesCfg() + # Newton's VBD path skips the simulation mesh collider, so collision offsets do not apply + collision_props = None + physics_material = NewtonDeformableBodyMaterialCfg( + k_mu=youngs_modulus / (2.0 * (1.0 + poissons_ratio)), + k_lambda=youngs_modulus * poissons_ratio / ((1.0 + poissons_ratio) * (1.0 - 2.0 * poissons_ratio)), + density=density, + ) +else: + from isaaclab_physx.sim.schemas import PhysxCollisionCfg, PhysxDeformableBodyPropertiesCfg + from isaaclab_physx.sim.spawners.materials import PhysxDeformableBodyMaterialCfg + + deformable_props = PhysxDeformableBodyPropertiesCfg() + collision_props = [PhysxCollisionCfg(rest_offset=0.0, contact_offset=0.001)] + physics_material = PhysxDeformableBodyMaterialCfg( + poissons_ratio=poissons_ratio, youngs_modulus=youngs_modulus, density=density + ) -def design_scene(): - """Designs the scene.""" - from isaaclab.assets import DeformableObject, DeformableObjectCfg - - # Ground-plane - cfg = sim_utils.GroundPlaneCfg() - cfg.func("/World/defaultGroundPlane", cfg) - # Lights - cfg = sim_utils.DomeLightCfg(intensity=2000.0, color=(0.8, 0.8, 0.8)) - cfg.func("/World/Light", cfg) - - # Create a dictionary for the scene entities - scene_entities = {} - - # Create separate groups called "env_0", "env_1", ... - # Newton's scene loader requires the "env_\d+" naming convention to - # detect per-environment Xforms and replicate them as separate worlds. - origins = [[0.25, 0.25, 0.0], [-0.25, 0.25, 0.0], [0.25, -0.25, 0.0], [-0.25, -0.25, 0.0]] - for i, origin in enumerate(origins): - sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=origin) - - youngs_modulus = 1e5 - poissons_ratio = 0.4 - density = 500.0 - if args_cli.backend == "newton_vbd": - from isaaclab_newton.sim.schemas import NewtonDeformableBodyPropertiesCfg - from isaaclab_newton.sim.spawners.materials import NewtonDeformableBodyMaterialCfg - - deformable_props = NewtonDeformableBodyPropertiesCfg() - # Newton's VBD path skips the simulation mesh collider, so collision offsets do not apply - collision_props = None - physics_material = NewtonDeformableBodyMaterialCfg( - k_mu=youngs_modulus / (2.0 * (1.0 + poissons_ratio)), - k_lambda=youngs_modulus * poissons_ratio / ((1.0 + poissons_ratio) * (1.0 - 2.0 * poissons_ratio)), - density=density, - ) - else: - from isaaclab_physx.sim.schemas import PhysxCollisionCfg, PhysxDeformableBodyPropertiesCfg - from isaaclab_physx.sim.spawners.materials import PhysxDeformableBodyMaterialCfg - - deformable_props = PhysxDeformableBodyPropertiesCfg() - collision_props = [PhysxCollisionCfg(rest_offset=0.0, contact_offset=0.001)] - physics_material = PhysxDeformableBodyMaterialCfg( - poissons_ratio=poissons_ratio, youngs_modulus=youngs_modulus, density=density - ) +@configclass +class DeformableSceneCfg(InteractiveSceneCfg): + """Soft cubes on a shared ground plane.""" + + filter_collisions = False + clone_cfg = CloneCfg(clone_template="/World/env_{}") + + ground = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) + light = AssetBaseCfg( + prim_path="/World/Light", spawn=sim_utils.DomeLightCfg(intensity=2000.0, color=(0.8, 0.8, 0.8)) + ) # 3D Deformable Object - cfg = DeformableObjectCfg( - prim_path="/World/env_.*/Cube", + cube_object = DeformableObjectCfg( + prim_path="{ENV_REGEX_NS}/Cube", spawn=sim_utils.MeshCuboidCfg( size=(0.2, 0.2, 0.2), deformable_props=deformable_props, @@ -114,19 +111,11 @@ def design_scene(): debug_vis=True, ) - cube_object = DeformableObject(cfg=cfg) - scene_entities["cube_object"] = cube_object - - # return the scene information - return scene_entities, origins - -def run_simulator(sim: sim_utils.SimulationContext, entities: dict, origins: torch.Tensor): +def run_simulator(sim: sim_utils.SimulationContext, scene: "InteractiveScene"): """Runs the simulation loop.""" # Extract scene entities - # note: we only do this here for readability. In general, it is better to access the entities directly from - # the dictionary. This dictionary is replaced by the InteractiveScene class in the next tutorial. - cube_object: DeformableObject = entities["cube_object"] + cube_object: DeformableObject = scene["cube_object"] # Define simulation stepping sim_dt = sim.get_physics_dt() @@ -146,7 +135,7 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict, origins: tor # reset the nodal state of the object nodal_state = cube_object.data.default_nodal_state_w.torch.clone() # apply random pose to the object - pos_w = torch.rand(cube_object.num_instances, 3, device=sim.device) * 0.1 + origins + pos_w = torch.rand(cube_object.num_instances, 3, device=sim.device) * 0.1 + scene.env_origins quat_w = math_utils.random_orientation(cube_object.num_instances, device=sim.device) nodal_state[..., :3] = cube_object.transform_nodal_pos(nodal_state[..., :3], pos_w, quat_w) @@ -164,8 +153,8 @@ def run_simulator(sim: sim_utils.SimulationContext, entities: dict, origins: tor print("----------------------------------------") print("[INFO]: Resetting object state...") - # update the kinematic target for cubes at index 0 and 3 - kinematic_cubes = [0, 3] + # update the kinematic target for cubes at the positive and negative diagonal corners + kinematic_cubes = [1, 2] # we slightly move the cube in the z-direction by picking the vertex at index 0 nodal_kinematic_target[kinematic_cubes, 0, 2] += 0.2 * sim_dt # set vertex at index 0 to be kinematically constrained @@ -199,15 +188,14 @@ def main(): sim = sim_utils.SimulationContext(sim_cfg) # Set main camera sim.set_camera_view(eye=[2.0, 2.0, 2.0], target=[0.0, 0.0, 0.75]) - # Design scene - scene_entities, scene_origins = design_scene() - scene_origins = torch.tensor(scene_origins, device=sim.device) + scene_cfg = DeformableSceneCfg(num_envs=4, env_spacing=0.5) + scene = scene_cfg.class_type(scene_cfg) # Play the simulator sim.reset() # Now we are ready! print("[INFO]: Setup complete...") # Run the simulator - run_simulator(sim, scene_entities, scene_origins) + run_simulator(sim, scene) print("[INFO]: Simulation complete...") diff --git a/source/isaaclab_contrib/changelog.d/clone-lifecycle.skip b/source/isaaclab_contrib/changelog.d/clone-lifecycle.skip new file mode 100644 index 000000000000..382cd47a06cb --- /dev/null +++ b/source/isaaclab_contrib/changelog.d/clone-lifecycle.skip @@ -0,0 +1 @@ +Migrated standalone test scene builders to explicit clone plans. diff --git a/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py b/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py index 668e88ce4f4f..82f6c98e35de 100644 --- a/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py +++ b/source/isaaclab_contrib/test/custom_coupling/test_manual_coupling.py @@ -24,8 +24,9 @@ from isaaclab_newton.sim.spawners.materials import NewtonDeformableBodyMaterialCfg import isaaclab.sim as sim_utils -from isaaclab.assets import RigidObjectCfg +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg from isaaclab.assets.deformable_object import DeformableObjectCfg +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.sim import SimulationCfg, build_simulation_context from isaaclab_contrib.custom_coupling import CoupledMJWarpVBDSolverCfg @@ -77,48 +78,45 @@ def generate_robot_and_two_cubes( """Create one robot, one colliding cube, and one free cube.""" sim_utils.create_prim("/World/env_0", "Xform", translation=(0.0, 0.0, 0.0)) - cfg = sim_utils.GroundPlaneCfg() - cfg.func("/World/defaultGroundPlane", cfg) - + ground_cfg = AssetBaseCfg(prim_path="/World/defaultGroundPlane", spawn=sim_utils.GroundPlaneCfg()) robot_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/env_[^/]+/Robot") - robot = Articulation(robot_cfg) - - colliding_cube = DeformableObject( - cfg=DeformableObjectCfg( - prim_path="/World/env_[^/]+/cube_collide", - spawn=sim_utils.MeshCuboidCfg( - size=(0.05, 0.05, 0.05), - deformable_props=NewtonDeformableBodyPropertiesCfg(), - visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.8, 0.2)), - physics_material=NewtonDeformableBodyMaterialCfg( - density=500.0, - k_mu=1e5, - k_lambda=1e5, - particle_radius=0.005, - ), + colliding_cube_cfg = DeformableObjectCfg( + prim_path="/World/env_[^/]+/cube_collide", + spawn=sim_utils.MeshCuboidCfg( + size=(0.05, 0.05, 0.05), + deformable_props=NewtonDeformableBodyPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.8, 0.2)), + physics_material=NewtonDeformableBodyMaterialCfg( + density=500.0, + k_mu=1e5, + k_lambda=1e5, + particle_radius=0.005, ), - init_state=DeformableObjectCfg.InitialStateCfg(pos=colliding_cube_pos), - ) + ), + init_state=DeformableObjectCfg.InitialStateCfg(pos=colliding_cube_pos), ) - - free_cube = DeformableObject( - cfg=DeformableObjectCfg( - prim_path="/World/env_[^/]+/cube_free", - spawn=sim_utils.MeshCuboidCfg( - size=(0.05, 0.05, 0.05), - deformable_props=NewtonDeformableBodyPropertiesCfg(), - visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 0.2, 0.2)), - physics_material=NewtonDeformableBodyMaterialCfg( - density=500.0, - k_mu=1e4, - k_lambda=1e4, - particle_radius=0.005, - ), + free_cube_cfg = DeformableObjectCfg( + prim_path="/World/env_[^/]+/cube_free", + spawn=sim_utils.MeshCuboidCfg( + size=(0.05, 0.05, 0.05), + deformable_props=NewtonDeformableBodyPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 0.2, 0.2)), + physics_material=NewtonDeformableBodyMaterialCfg( + density=500.0, + k_mu=1e4, + k_lambda=1e4, + particle_radius=0.005, ), - init_state=DeformableObjectCfg.InitialStateCfg(pos=free_cube_pos), - ) + ), + init_state=DeformableObjectCfg.InitialStateCfg(pos=free_cube_pos), ) - + plan = clone_plan_from_env_0( + CloneCfg(clone_template="/World/env_{}"), (ground_cfg, robot_cfg, colliding_cube_cfg, free_cube_cfg), 1, 0.0 + ) + ground_cfg.class_type(ground_cfg) + robot = Articulation(robot_cfg) + colliding_cube, free_cube = DeformableObject(colliding_cube_cfg), DeformableObject(free_cube_cfg) + replicate(plan) return robot, colliding_cube, free_cube @@ -129,38 +127,37 @@ def generate_lateral_rigid_and_deformable_cubes( """Create rigid and deformable cubes for lateral contact.""" sim_utils.create_prim("/World/env_0", "Xform", translation=(0.0, 0.0, 0.0)) - rigid_cube = RigidObject( - cfg=RigidObjectCfg( - prim_path="/World/env_[^/]+/rigid_cube", - spawn=sim_utils.CuboidCfg( - size=(0.2, 0.2, 0.2), - rigid_props=sim_utils.UsdPhysicsRigidBodyCfg(), - mass_props=sim_utils.MassCfg(mass=0.05), - collision_props=sim_utils.UsdPhysicsCollisionCfg(), - visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.2, 0.8)), - ), - init_state=RigidObjectCfg.InitialStateCfg(pos=rigid_cube_pos), - ) + rigid_cube_cfg = RigidObjectCfg( + prim_path="/World/env_[^/]+/rigid_cube", + spawn=sim_utils.CuboidCfg( + size=(0.2, 0.2, 0.2), + rigid_props=sim_utils.UsdPhysicsRigidBodyCfg(), + mass_props=sim_utils.MassCfg(mass=0.05), + collision_props=sim_utils.UsdPhysicsCollisionCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.2, 0.2, 0.8)), + ), + init_state=RigidObjectCfg.InitialStateCfg(pos=rigid_cube_pos), ) - - deformable_cube = DeformableObject( - cfg=DeformableObjectCfg( - prim_path="/World/env_[^/]+/deformable_cube", - spawn=sim_utils.MeshCuboidCfg( - size=(0.08, 0.08, 0.08), - deformable_props=NewtonDeformableBodyPropertiesCfg(), - visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 0.2, 0.2)), - physics_material=NewtonDeformableBodyMaterialCfg( - density=1000.0, - k_mu=1e5, - k_lambda=1e5, - particle_radius=0.005, - ), + deformable_cube_cfg = DeformableObjectCfg( + prim_path="/World/env_[^/]+/deformable_cube", + spawn=sim_utils.MeshCuboidCfg( + size=(0.08, 0.08, 0.08), + deformable_props=NewtonDeformableBodyPropertiesCfg(), + visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.8, 0.2, 0.2)), + physics_material=NewtonDeformableBodyMaterialCfg( + density=1000.0, + k_mu=1e5, + k_lambda=1e5, + particle_radius=0.005, ), - init_state=DeformableObjectCfg.InitialStateCfg(pos=deformable_cube_pos), - ) + ), + init_state=DeformableObjectCfg.InitialStateCfg(pos=deformable_cube_pos), ) - + plan = clone_plan_from_env_0( + CloneCfg(clone_template="/World/env_{}"), (rigid_cube_cfg, deformable_cube_cfg), 1, 0.0 + ) + rigid_cube, deformable_cube = RigidObject(rigid_cube_cfg), DeformableObject(deformable_cube_cfg) + replicate(plan) return rigid_cube, deformable_cube diff --git a/source/isaaclab_contrib/test/deformable/test_deformable_object.py b/source/isaaclab_contrib/test/deformable/test_deformable_object.py index 7ee3460d5fc7..415187f240fb 100644 --- a/source/isaaclab_contrib/test/deformable/test_deformable_object.py +++ b/source/isaaclab_contrib/test/deformable/test_deformable_object.py @@ -16,6 +16,7 @@ """Rest everything follows.""" +import numpy as np import pytest import torch import warp as wp @@ -30,6 +31,7 @@ import isaaclab.sim as sim_utils import isaaclab.utils.math as math_utils from isaaclab.assets import DeformableObject, DeformableObjectCfg +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.sim import SimulationCfg, build_simulation_context NEWTON_VBD_CFG = SimulationCfg( @@ -47,24 +49,18 @@ def _newton_sim_context(device="cuda:0", gravity_enabled=True): return build_simulation_context(device=device, sim_cfg=NEWTON_VBD_CFG, auto_add_lighting=True) -def generate_cubes_scene( - num_cubes: int = 1, - height: float = 1.0, - device: str = "cuda:0", -) -> DeformableObject: +def generate_cubes_scene(num_cubes: int = 1, height: float = 1.0) -> DeformableObject: """Generate a scene with deformable tet-mesh cubes. Args: num_cubes: Number of cubes to generate. height: Height of the cubes. - device: Device to use for the simulation. Returns: The deformable object representing the cubes. """ - origins = torch.tensor([(i * 1.0, 0, height) for i in range(num_cubes)]).to(device) - for i, origin in enumerate(origins): - sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=origin) + origins = np.asarray([(i * 1.0, 0, height) for i in range(num_cubes)], dtype=np.float32) + sim_utils.create_prim("/World/env_0", "Xform", translation=origins[0]) cube_object_cfg = DeformableObjectCfg( prim_path="/World/env_[^/]+/Cube", @@ -83,28 +79,26 @@ def generate_cubes_scene( rot=(1.0, 0.0, 0.0, 0.0), ), ) + plan = clone_plan_from_env_0( + CloneCfg(clone_template="/World/env_{}"), (cube_object_cfg,), num_cubes, 1.0, positions=origins + ) cube_object = DeformableObject(cfg=cube_object_cfg) + replicate(plan) return cube_object -def generate_cloth_scene( - num_cloths: int = 1, - height: float = 1.0, - device: str = "cuda:0", -) -> DeformableObject: +def generate_cloth_scene(num_cloths: int = 1, height: float = 1.0) -> DeformableObject: """Generate a scene with surface deformable cloth squares. Args: num_cloths: Number of cloths to generate. height: Height of the cloths. - device: Device to use for the simulation. Returns: The deformable object representing the cloths. """ - origins = torch.tensor([(i * 1.0, 0, height) for i in range(num_cloths)]).to(device) - for i, origin in enumerate(origins): - sim_utils.create_prim(f"/World/env_{i}", "Xform", translation=origin) + origins = np.asarray([(i * 1.0, 0, height) for i in range(num_cloths)], dtype=np.float32) + sim_utils.create_prim("/World/env_0", "Xform", translation=origins[0]) cloth_object_cfg = DeformableObjectCfg( prim_path="/World/env_[^/]+/Cloth", @@ -120,7 +114,12 @@ def generate_cloth_scene( rot=(1.0, 0.0, 0.0, 0.0), ), ) - return DeformableObject(cfg=cloth_object_cfg) + plan = clone_plan_from_env_0( + CloneCfg(clone_template="/World/env_{}"), (cloth_object_cfg,), num_cloths, 1.0, positions=origins + ) + cloth_object = DeformableObject(cfg=cloth_object_cfg) + replicate(plan) + return cloth_object def generate_cuboid_and_cylinder_scene(height: float = 1.0) -> tuple[DeformableObject, DeformableObject]: @@ -156,7 +155,10 @@ def generate_cuboid_and_cylinder_scene(height: float = 1.0) -> tuple[DeformableO ), init_state=DeformableObjectCfg.InitialStateCfg(pos=(0.4, 0.0, height + 0.2)), ) - return DeformableObject(cfg=cuboid_cfg), DeformableObject(cfg=cylinder_cfg) + plan = clone_plan_from_env_0(CloneCfg(clone_template="/World/env_{}"), (cuboid_cfg, cylinder_cfg), 1, 0.0) + cuboid, cylinder = DeformableObject(cfg=cuboid_cfg), DeformableObject(cfg=cylinder_cfg) + replicate(plan) + return cuboid, cylinder @pytest.fixture diff --git a/source/isaaclab_newton/changelog.d/clone-lifecycle.major.rst b/source/isaaclab_newton/changelog.d/clone-lifecycle.major.rst new file mode 100644 index 000000000000..45971dd180b9 --- /dev/null +++ b/source/isaaclab_newton/changelog.d/clone-lifecycle.major.rst @@ -0,0 +1,12 @@ +Changed +^^^^^^^ + +* **Breaking:** Removed implicit USD-stage import when Newton started without a builder. + Use ``InteractiveScene`` to construct and replicate configured USD assets before initialization. + Native tools can continue supplying a builder + through ``NewtonManager.set_builder(builder)`` without declaring a clone plan. + +Fixed +^^^^^ + +* Prevented globally declared native deformables from being imported twice through clone plans. diff --git a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py index 3217d3088e44..87f9f3ebb820 100644 --- a/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py +++ b/source/isaaclab_newton/isaaclab_newton/cloner/replicate.py @@ -137,11 +137,12 @@ def _replicate_newton( ) ignore_paths = [] if patterns: - for source in sources: - for prim in Usd.PrimRange(stage.GetPrimAtPath(source)): + for root_path in (*sources, *plan.global_paths): + for prim in Usd.PrimRange(stage.GetPrimAtPath(root_path)): path = str(prim.GetPath()) if any(pattern.fullmatch(path) for pattern in patterns): ignore_paths.append(path) + global_ignore_paths.extend(ignore_paths) else: entries = discover_deformables_on_stage(stage, root_paths=(*sources, *plan.global_paths)) ignore_paths = list( diff --git a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py index 94007a30365c..447631773830 100644 --- a/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py +++ b/source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py @@ -1442,14 +1442,18 @@ def start_simulation(cls) -> None: This function finalizes the model and initializes the simulation state. Note: Collision pipeline is initialized later in initialize_solver() after we determine whether the solver needs external collision detection. + + Raises: + RuntimeError: If neither clone-plan replication nor :meth:`set_builder` supplied a builder. """ logger.debug(f"Builder: {cls._builder}") + if cls._builder is None: + raise RuntimeError( + "Newton simulation requires an explicitly supplied builder. Replicate a ClonePlan or call" + " NewtonManager.set_builder() before starting the simulation." + ) cls._drain_stale_cuda_error() - - # Create builder from USD stage if not provided - if cls._builder is None: - cls.instantiate_builder_from_stage() cls._register_builder_attributes(cls._builder) logger.info("Dispatching MODEL_INIT callbacks") diff --git a/source/isaaclab_newton/test/assets/test_articulation.py b/source/isaaclab_newton/test/assets/test_articulation.py index fac76841724c..f1e3b59ef992 100644 --- a/source/isaaclab_newton/test/assets/test_articulation.py +++ b/source/isaaclab_newton/test/assets/test_articulation.py @@ -58,8 +58,9 @@ ImplicitActuator, ImplicitActuatorCfg, ) -from isaaclab.assets import ArticulationCfg +from isaaclab.assets import ArticulationCfg, AssetBaseCfg from isaaclab.assets.articulation.ordering_resolvers import get_articulation_name_ordering +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.controllers import ( DifferentialIKController, DifferentialIKControllerCfg, @@ -399,7 +400,7 @@ def fix_reversed_joints(stage): def generate_articulation( - articulation_cfg: ArticulationCfg, num_articulations: int, device: str + articulation_cfg: ArticulationCfg, num_articulations: int, device: str, add_ground_plane: bool = False ) -> tuple[Articulation, torch.tensor]: """Generate an articulation from a configuration. @@ -410,19 +411,25 @@ def generate_articulation( articulation_cfg: Articulation configuration. num_articulations: Number of articulations to generate. device: Device to use for the tensors. + add_ground_plane: Whether the simulation context authored a shared ground plane. Returns: The articulation and environment translations. """ # Generate translations of 2.5 m in x for each articulation - translations = torch.zeros(num_articulations, 3, device=device) - translations[:, 0] = torch.arange(num_articulations) * 2.5 - - # Create Top-level Xforms, one for each articulation - for i in range(num_articulations): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=translations[i][:3]) - articulation = Articulation(articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot")) + translations = np.zeros((num_articulations, 3), dtype=np.float32) + translations[:, 0] = np.arange(num_articulations) * 2.5 + + sim_utils.create_prim("/World/Env_0", "Xform", translation=translations[0]) + articulation_cfg = articulation_cfg.replace(prim_path="/World/Env_[^/]*/Robot") + cfgs = [articulation_cfg] + if add_ground_plane: + cfgs.append(AssetBaseCfg(prim_path="/World/defaultGroundPlane")) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), cfgs, num_articulations, 2.5, positions=translations + ) + articulation = Articulation(articulation_cfg) # Fix reversed joints for known-broken USD assets (body0/body1 swapped) usd_path = getattr(articulation_cfg.spawn, "usd_path", "") @@ -431,7 +438,7 @@ def generate_articulation( fix_reversed_joints(omni.usd.get_context().get_stage()) - return articulation, translations + return articulation, torch.as_tensor(translations, device=device) # --------------------------------------------------------------------------- @@ -470,7 +477,9 @@ def _setup_franka_at_home_pose(sim, *, zero_actuator_pd: bool = False, disable_g cfg.actuators["panda_forearm"].damping = 0.0 cfg.spawn.rigid_props.disable_gravity = disable_gravity sim_utils.create_prim("/World/Env_0", "Xform", translation=(0.0, 0.0, 0.0)) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), (cfg,), 1, 0.0) robot = Articulation(cfg) + replicate(sim.get_clone_plan()) sim.reset() assert robot.is_initialized @@ -837,6 +846,7 @@ def test_write_joint_state_accepts_int64_selector(sim, device, gravity_enabled, """Write joint state with int64 selectors.""" articulation_cfg = generate_articulation_cfg(articulation_type="spatial_tendon_test_asset") articulation, _ = generate_articulation(articulation_cfg, 2, device=device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.num_joints >= 2 @@ -877,14 +887,16 @@ def test_mjwarp_ordering_resolver_matches_newton_backend_names(sim, device, grav the same order on a single-joint chain. """ fixture_path = Path(__file__).parent / "data" / "articulation_ordering_branching.usda" - articulation = Articulation( - ArticulationCfg( - prim_path="/World/Robot", - spawn=sim_utils.UsdFileCfg(usd_path=str(fixture_path)), - actuators={}, - ) + sim_utils.create_prim("/World/Env_0", "Xform") + articulation_cfg = ArticulationCfg( + prim_path="/World/Env_0/Robot", + spawn=sim_utils.UsdFileCfg(usd_path=str(fixture_path)), + actuators={}, ) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), (articulation_cfg,), 1, 0.0) + articulation = Articulation(articulation_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -927,16 +939,18 @@ def test_branching_fixture_physx_ordering_reorders_newton_to_bfs(sim, device, gr test data directory so the two backends assert against the same ground-truth asset. """ fixture_path = Path(__file__).parent / "data" / "articulation_ordering_branching.usda" - articulation = Articulation( - ArticulationCfg( - prim_path="/World/Robot", - spawn=sim_utils.UsdFileCfg(usd_path=str(fixture_path)), - actuators={}, - joint_ordering="physx", - body_ordering="physx", - ) + sim_utils.create_prim("/World/Env_0", "Xform") + articulation_cfg = ArticulationCfg( + prim_path="/World/Env_0/Robot", + spawn=sim_utils.UsdFileCfg(usd_path=str(fixture_path)), + actuators={}, + joint_ordering="physx", + body_ordering="physx", ) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), (articulation_cfg,), 1, 0.0) + articulation = Articulation(articulation_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -993,6 +1007,7 @@ def test_newton_native_actuator_gain_write_maps_public_joint_subset_to_backend( joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES)), ) articulation, _ = generate_articulation(articulation_cfg, 2, device=sim.device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.joint_ordering is not None assert articulation.newton_actuator_adapter is not None @@ -1044,6 +1059,7 @@ def test_newton_ordered_body_state_cache_invalidates_on_same_timestamp_root_writ ) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + replicate(sim.get_clone_plan()) sim.reset() sim.step() articulation.update(sim.cfg.dt) @@ -1092,6 +1108,7 @@ def test_newton_ordered_state_caches_invalidate_on_rebind( ) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized has_ordering = ordering_mode == "reversed" @@ -1307,6 +1324,7 @@ def test_newton_rebind_preserves_lab_owned_actuator_gains( if ordering_mode == "reversed": articulation_cfg = articulation_cfg.replace(joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES))) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -1381,6 +1399,7 @@ def test_newton_post_step_hook_publishes_ordered_state_and_deregisters( body_ordering=_ANYMAL_C_ROOT_PRESERVING_REVERSED_BODY_NAMES, ) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -1440,6 +1459,7 @@ def test_write_data_to_sim_gathers_joint_targets_only_when_ordering_active( if ordering_mode == "reversed": articulation_cfg = articulation_cfg.replace(joint_ordering=tuple(reversed(ANYMAL_C_PHYSX_JOINT_NAMES))) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -1488,14 +1508,16 @@ def test_set_body_inertial_properties_updates_inverses( ): """Selected inertial-property writes keep Newton inverse arrays current under body ordering.""" fixture_path = Path(__file__).parent / "data" / "articulation_ordering_branching.usda" - articulation = Articulation( - ArticulationCfg( - prim_path="/World/Robot", - spawn=sim_utils.UsdFileCfg(usd_path=str(fixture_path)), - actuators={}, - body_ordering="physx", - ) + sim_utils.create_prim("/World/Env_0", "Xform") + articulation_cfg = ArticulationCfg( + prim_path="/World/Env_0/Robot", + spawn=sim_utils.UsdFileCfg(usd_path=str(fixture_path)), + actuators={}, + body_ordering="physx", ) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), (articulation_cfg,), 1, 0.0) + articulation = Articulation(articulation_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.data.body_ordering is not None @@ -1561,12 +1583,15 @@ def test_initialization_floating_base_non_root(sim, num_articulations, device, a device: The device to run the simulation on """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type, stiffness=0.0, damping=0.0) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + articulation, _ = generate_articulation( + articulation_cfg, num_articulations, device=sim.device, add_ground_plane=True + ) # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized @@ -1604,7 +1629,8 @@ def test_gravity_vec_w_tracks_model_gravity(sim, num_articulations, device, add_ :class:`~isaaclab.envs.mdp.randomize_physics_scene_gravity`). """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type, stiffness=0.0, damping=0.0) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device, add_ground_plane=True) + replicate(sim.get_clone_plan()) sim.reset() # GRAVITY_VEC_W must share storage with Newton's per-env gravity array. @@ -1655,12 +1681,13 @@ def test_initialization_floating_base(sim, num_articulations, device, add_ground device: The device to run the simulation on """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type, stiffness=0.0, damping=0.0) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device, add_ground_plane=True) # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized @@ -1710,6 +1737,7 @@ def test_initialization_fixed_base(sim, num_articulations, device, articulation_ assert sys.getrefcount(articulation) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized @@ -1768,6 +1796,7 @@ def test_fixed_base_reports_body_velocities(sim, num_articulations, device, arti articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) # Play sim + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_fixed_base @@ -1814,12 +1843,15 @@ def test_initialization_fixed_base_single_joint(sim, num_articulations, device, device: The device to run the simulation on """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) - articulation, translations = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, translations = generate_articulation( + articulation_cfg, num_articulations, device=device, add_ground_plane=True + ) # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized @@ -1869,6 +1901,7 @@ def test_hand_with_tendons_initializes_and_targets_only_given_envs(sim, num_arti # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized assert articulation.is_fixed_base @@ -1910,7 +1943,7 @@ def test_fragment_fix_root_link_uses_base_manager(sim, device, add_ground_plane, articulation_cfg = deepcopy(generate_articulation_cfg(articulation_type=articulation_type)) articulation_cfg.spawn.articulation_props = [] articulation_cfg.spawn.fix_root_link = True - articulation, _ = generate_articulation(articulation_cfg, num_articulations=1, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations=1, device=device, add_ground_plane=True) root = sim_utils.get_first_matching_child_prim( "/World/Env_0/Robot", @@ -1920,6 +1953,7 @@ def test_fragment_fix_root_link_uses_base_manager(sim, device, add_ground_plane, assert root is not None and root.HasAPI(UsdPhysics.RigidBodyAPI) assert sim_utils.find_global_fixed_joint_prim("/World/Env_0/Robot", stage=sim.stage) is not None + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized assert articulation.is_fixed_base @@ -1947,12 +1981,15 @@ def test_initialization_floating_base_made_fixed_base( articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type).copy() # Fix root link by making it kinematic articulation_cfg.spawn.fix_root_link = True - articulation, translations = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, translations = generate_articulation( + articulation_cfg, num_articulations, device=device, add_ground_plane=True + ) # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized @@ -2001,12 +2038,15 @@ def test_initialization_fixed_base_made_floating_base( articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type).copy() # Unfix root link by making it non-kinematic articulation_cfg.spawn.fix_root_link = False - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) + articulation, _ = generate_articulation( + articulation_cfg, num_articulations, device=sim.device, add_ground_plane=True + ) # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized @@ -2040,6 +2080,7 @@ def test_out_of_range_default_joint_state(sim, device, articulation_type, state_ # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10 + replicate(sim.get_clone_plan()) with pytest.raises(ValueError): sim.reset() @@ -2063,9 +2104,10 @@ def test_joint_pos_limits(sim, num_articulations, device, add_ground_plane, arti """ # Create articulation articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device, add_ground_plane=True) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized @@ -2128,7 +2170,7 @@ def test_joint_effort_limits(sim, num_articulations, device, add_ground_plane, a """Validate joint effort limits via joint_effort_out_of_limit().""" # Create articulation articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device, add_ground_plane=True) # Minimal env wrapper exposing scene["robot"] class _Env: @@ -2138,6 +2180,7 @@ def __init__(self, art): env = _Env(articulation) robot_all = SceneEntityCfg(name="robot") + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -2173,6 +2216,7 @@ def test_external_force_buffer(sim, num_articulations, device, articulation_type articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) # play the simulator + replicate(sim.get_clone_plan()) sim.reset() # find bodies to apply the force @@ -2257,6 +2301,7 @@ def test_external_force_on_single_body(sim, num_articulations, device, articulat articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Find bodies to apply the force @@ -2317,6 +2362,7 @@ def test_external_force_on_single_body_at_position(sim, num_articulations, devic articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Find bodies to apply the force @@ -2411,6 +2457,7 @@ def test_external_force_on_multiple_bodies(sim, num_articulations, device, artic articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Find bodies to apply the force @@ -2473,6 +2520,7 @@ def test_external_force_on_multiple_bodies_at_position(sim, num_articulations, d articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Find bodies to apply the force @@ -2565,6 +2613,7 @@ def test_loading_gains_from_usd(sim, num_articulations, device, articulation_typ articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=sim.device) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Expected gains @@ -2626,10 +2675,11 @@ def test_setting_gains_from_cfg(sim, num_articulations, device, add_ground_plane """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=sim.device + articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=sim.device, add_ground_plane=True ) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Expected gains @@ -2678,6 +2728,7 @@ def test_setting_velocity_limit_implicit( device=device, ) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # read the values set into the simulation @@ -2724,6 +2775,7 @@ def test_setting_velocity_limit_explicit( device=device, ) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # collect limit init values @@ -2783,6 +2835,7 @@ def test_setting_effort_limit_implicit(sim, articulation_type, num_articulations device=device, ) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # obtain the physx effort limits @@ -2843,6 +2896,7 @@ def test_setting_effort_limit_explicit( device=device, ) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # usd default effort limit is set to 80 @@ -2886,6 +2940,7 @@ def test_reset(sim, num_articulations, device, articulation_type, monkeypatch): ) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Now we are ready! @@ -2937,10 +2992,11 @@ def test_apply_joint_command(sim, num_articulations, device, add_ground_plane, a """Test applying of joint position target functions correctly for a robotic arm.""" articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=device + articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=device, add_ground_plane=True ) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() for _ in range(100): @@ -2993,6 +3049,7 @@ def test_body_root_state(sim, num_articulations, device, with_offset, articulati # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(articulation) < 10, "Possible reference leak for articulation" # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if articulation is initialized assert articulation.is_initialized, "Articulation is not initialized" @@ -3124,6 +3181,7 @@ def test_write_root_state( env_idx = torch.tensor([x for x in range(num_articulations)], device=device, dtype=torch.int32) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Resolve root body index by name (ordering may differ across physics backends) @@ -3206,6 +3264,7 @@ def test_write_root_state_functions_data_consistency( articulation, env_pos = generate_articulation(articulation_cfg, num_articulations, device) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Resolve root body index by name (ordering may differ across physics backends) @@ -3300,6 +3359,7 @@ def test_setting_articulation_root_prim_path(sim, device, articulation_type, roo assert sys.getrefcount(articulation) < 10 if root_prim_path == "/torso": + replicate(sim.get_clone_plan()) sim.reset() assert articulation._is_initialized else: @@ -3328,6 +3388,7 @@ def test_write_joint_state_data_consistency(sim, num_articulations, device, grav env_idx = torch.tensor([x for x in range(num_articulations)]) # Play sim + replicate(sim.get_clone_plan()) sim.reset() limits = torch.zeros(num_articulations, articulation.num_joints, 2, device=device) @@ -3421,10 +3482,11 @@ def test_write_joint_frictions_to_sim(sim, num_articulations, device, add_ground """Test static joint friction writes propagate directly to the Newton model.""" articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation( - articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=device + articulation_cfg=articulation_cfg, num_articulations=num_articulations, device=device, add_ground_plane=True ) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() friction = torch.rand(num_articulations, articulation.num_joints, device=device) @@ -3445,6 +3507,7 @@ def test_write_joint_viscous_friction_to_sim(sim, device, articulation_type, sel articulation_cfg = generate_articulation_cfg(articulation_type) articulation_cfg.actuators["panda_shoulder"].viscous_friction = 0.25 articulation, _ = generate_articulation(articulation_cfg, 1, device) + replicate(sim.get_clone_plan()) sim.reset() shoulder_joint_ids = articulation.actuators["panda_shoulder"].joint_indices @@ -3518,6 +3581,7 @@ def test_body_q_consistent_after_root_write(num_articulations, device, articulat articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, env_pos = generate_articulation(articulation_cfg, num_articulations, device) + replicate(sim.get_clone_plan()) sim.reset() model = SimulationManager.get_model() @@ -3573,8 +3637,9 @@ def _patched_simulate(cls): def test_randomize_rigid_body_com(sim, num_articulations, device, add_ground_plane, articulation_type): """Test that randomize_rigid_body_com modifies CoM and affects simulation dynamics.""" articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device, add_ground_plane=True) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -3597,8 +3662,9 @@ def test_randomize_rigid_body_com(sim, num_articulations, device, add_ground_pla def test_randomize_rigid_body_collider_offsets(sim, num_articulations, device, add_ground_plane, articulation_type): """Test that Newton collider offset randomization (shape_margin, shape_gap) takes effect.""" articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device, add_ground_plane=True) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -3654,7 +3720,8 @@ def test_dynamics_accessor_shapes(sim, num_articulations, device, add_ground_pla rather than checking a determinant (a well-formed 9x9 Franka mass matrix has det ~1e-13). """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device, add_ground_plane=True) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized assert articulation.is_fixed_base == (articulation_type == "panda") @@ -3716,15 +3783,21 @@ def test_heterogeneous_scene_per_view_shapes(sim, device, add_ground_plane, arti # per-articulation shape gate without that pre-existing quirk. num_per_type = 1 - franka_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/Env_franka_[^/]*/Robot") - anymal_cfg = ANYMAL_C_CFG.replace(prim_path="/World/Env_anymal_[^/]*/Robot") + franka_cfg = FRANKA_PANDA_CFG.replace(prim_path="/World/Env_[^/]*/Franka") + anymal_cfg = ANYMAL_C_CFG.replace(prim_path="/World/Env_[^/]*/Anymal") + anymal_cfg.init_state.pos = (0.0, 5.0, anymal_cfg.init_state.pos[2]) - for i in range(num_per_type): - sim_utils.create_prim(f"/World/Env_franka_{i}", "Xform", translation=(2.5 * i, 0.0, 0.0)) - sim_utils.create_prim(f"/World/Env_anymal_{i}", "Xform", translation=(2.5 * i, 5.0, 0.0)) + sim_utils.create_prim("/World/Env_0", "Xform") + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (franka_cfg, anymal_cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + num_per_type, + 2.5, + ) franka = Articulation(franka_cfg) anymal = Articulation(anymal_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert franka.is_initialized and anymal.is_initialized assert franka.is_fixed_base and not anymal.is_fixed_base @@ -3817,6 +3890,7 @@ def test_get_jacobians_link_origin_contract(sim, num_articulations, device, arti """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -3898,7 +3972,8 @@ def test_get_gravity_compensation_forces_matches_jacobian_gravity( if ordering_mode == "reversed": joint_names = PANDA_JOINT_NAMES if articulation_type == "panda" else ANYMAL_C_PHYSX_JOINT_NAMES articulation_cfg = articulation_cfg.replace(joint_ordering=tuple(reversed(joint_names))) - articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device, add_ground_plane=True) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -3956,6 +4031,7 @@ def test_dynamics_accessors_refresh_after_manual_joint_write(sim, num_articulati """ articulation_cfg = generate_articulation_cfg(articulation_type=articulation_type) articulation, _ = generate_articulation(articulation_cfg, num_articulations, device=device) + replicate(sim.get_clone_plan()) sim.reset() sim.step() articulation.update(sim.cfg.dt) @@ -4029,6 +4105,7 @@ def test_get_gravity_compensation_forces_static_equilibrium(sim, num_articulatio ) articulation, _ = generate_articulation(cfg, num_articulations, device=device) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized diff --git a/source/isaaclab_newton/test/assets/test_cable_object.py b/source/isaaclab_newton/test/assets/test_cable_object.py index c2801445702a..04809799cecc 100644 --- a/source/isaaclab_newton/test/assets/test_cable_object.py +++ b/source/isaaclab_newton/test/assets/test_cable_object.py @@ -17,6 +17,7 @@ from isaaclab_newton.physics import NewtonCfg, VBDSolverCfg, XPBDSolverCfg from isaaclab_newton.physics import NewtonManager as SimulationManager +import isaaclab.cloner as cloner import isaaclab.sim as sim_utils from isaaclab.assets import CableObjectCfg, RigidObjectCfg from isaaclab.envs.mdp.events import reset_scene_to_default @@ -99,6 +100,11 @@ def test_cable_collides_with_ground(): init_state=CableObjectCfg.InitialStateCfg(pos=(0.0, 0.0, 0.8)), ) ) + plan = cloner.make_clone_plan( + (cable.cfg,), 1, 0.0, global_paths=("/World/Ground",), env_template="/World/Env_{}" + ) + sim.set_clone_plan(plan) + cloner.replicate(plan) sim.reset() contact_seen = False diff --git a/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py b/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py index bdf081d965a7..264ce94b4498 100644 --- a/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py +++ b/source/isaaclab_newton/test/assets/test_newton_actuators_newton.py @@ -37,6 +37,8 @@ from isaaclab.actuators import IdealPDActuatorCfg from isaaclab.actuators.newton import read_group_parameter from isaaclab.actuators.newton.kernels import sync_torque_telemetry +from isaaclab.assets import AssetBaseCfg +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.sim import SimulationCfg, build_simulation_context from isaaclab.test.utils.actuator_equivalence import ( CARTPOLE_EXPLICIT_ACTUATORS, @@ -130,14 +132,21 @@ def _run_simulation( sim_cfg=sim_cfg, ) as sim: sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) + sim_utils.create_prim("/World/Env_0", "Xform") art_cfg = ANYMAL_C_CFG.replace( actuators=actuators, prim_path="/World/Env_[^/]*/Robot", joint_ordering=joint_ordering, ) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (art_cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + NUM_ENVS, + 3.0, + positions=np.asarray([(i * 3.0, 0.0, 0.0) for i in range(NUM_ENVS)]), + ) articulation = Articulation(art_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized @@ -345,8 +354,7 @@ def _run_anymal_and_cartpole(use_newton_actuators: bool, *, num_steps: int = NUM ) as sim: sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 6.0, 0, 0)) + sim_utils.create_prim("/World/Env_0", "Xform") anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Anymal") cartpole_cfg = CARTPOLE_CFG.replace( @@ -356,8 +364,16 @@ def _run_anymal_and_cartpole(use_newton_actuators: bool, *, num_steps: int = NUM # Stand the cartpole well clear of the anymal. cartpole_cfg.init_state = cartpole_cfg.init_state.replace(pos=(0.0, 3.0, 2.0)) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (anymal_cfg, cartpole_cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + NUM_ENVS, + 6.0, + positions=np.asarray([(i * 6.0, 0.0, 0.0) for i in range(NUM_ENVS)]), + ) anymal = Articulation(anymal_cfg) cartpole = Articulation(cartpole_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert anymal.is_initialized and cartpole.is_initialized @@ -450,13 +466,20 @@ def test_single_articulation(self): sim_cfg=sim_cfg, ) as sim: sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) + sim_utils.create_prim("/World/Env_0", "Xform") art_cfg = ANYMAL_C_CFG.replace( actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Robot", ) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (art_cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + NUM_ENVS, + 3.0, + positions=np.asarray([(i * 3.0, 0.0, 0.0) for i in range(NUM_ENVS)]), + ) anymal = Articulation(art_cfg) + replicate(sim.get_clone_plan()) sim.reset() adapter = SimulationManager._adapter @@ -511,8 +534,7 @@ def test_two_articulations(self): sim_cfg=sim_cfg, ) as sim: sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 6.0, 0, 0)) + sim_utils.create_prim("/World/Env_0", "Xform") anymal_cfg = ANYMAL_C_CFG.replace(actuators=IDEAL_PD_ACTUATORS, prim_path="/World/Env_[^/]*/Anymal") cartpole_cfg = CARTPOLE_CFG.replace( @@ -520,8 +542,16 @@ def test_two_articulations(self): prim_path="/World/Env_[^/]*/Cartpole", ) cartpole_cfg.init_state = cartpole_cfg.init_state.replace(pos=(0.0, 3.0, 2.0)) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (anymal_cfg, cartpole_cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + NUM_ENVS, + 6.0, + positions=np.asarray([(i * 6.0, 0.0, 0.0) for i in range(NUM_ENVS)]), + ) anymal = Articulation(anymal_cfg) cartpole = Articulation(cartpole_cfg) + replicate(sim.get_clone_plan()) sim.reset() self.assertIsNotNone(SimulationManager._adapter) @@ -638,7 +668,17 @@ def _make_sim_cfg(self, use_newton_actuators: bool) -> SimulationCfg: return SimulationCfg(dt=DT, physics=NEWTON_CFG, use_newton_actuators=use_newton_actuators) def _make_articulation(self) -> Articulation: - return Articulation(ANYMAL_C_CFG.replace(actuators=DELAYED_PD_ACTUATORS, prim_path="/World/Env_.*/Robot")) + cfg = ANYMAL_C_CFG.replace(actuators=DELAYED_PD_ACTUATORS, prim_path="/World/Env_.*/Robot") + plan = clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + self.NUM_ENVS, + 3.0, + positions=np.asarray([(i * 3.0, 0.0, 0.0) for i in range(self.NUM_ENVS)]), + ) + articulation = Articulation(cfg) + replicate(plan) + return articulation def _get_adapter(self, articulation): return SimulationManager._adapter @@ -691,14 +731,21 @@ def _run_authoring_introspection(actuator_cfgs: dict) -> dict: ) as sim: sim._app_control_on_stop_handle = None - for i in range(NUM_ENVS): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 3.0, 0, 0)) + sim_utils.create_prim("/World/Env_0", "Xform") art_cfg = ANYMAL_C_CFG.replace( actuators=actuator_cfgs, prim_path="/World/Env_[^/]*/Robot", ) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + (art_cfg, AssetBaseCfg(prim_path="/World/defaultGroundPlane")), + NUM_ENVS, + 3.0, + positions=np.asarray([(i * 3.0, 0.0, 0.0) for i in range(NUM_ENVS)]), + ) articulation = Articulation(art_cfg) + replicate(sim.get_clone_plan()) sim.reset() assert articulation.is_initialized diff --git a/source/isaaclab_newton/test/assets/test_rigid_object.py b/source/isaaclab_newton/test/assets/test_rigid_object.py index 6d05dd4577ba..01a4de488607 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object.py @@ -20,6 +20,7 @@ import sys from typing import Literal +import numpy as np import pytest import torch import warp as wp @@ -31,7 +32,8 @@ from newton import ModelFlags import isaaclab.sim as sim_utils -from isaaclab.assets import RigidObjectCfg +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.sim import SimulationCfg, build_simulation_context from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR, ISAACLAB_NUCLEUS_DIR from isaaclab.utils.math import ( @@ -70,6 +72,7 @@ def generate_cubes_scene( api: Literal["none", "rigid_body", "articulation_root"] = "rigid_body", kinematic_enabled: bool = False, device: str = "cuda:0", + add_ground_plane: bool = False, ) -> tuple[RigidObject, torch.Tensor]: """Generate a scene with the provided number of cubes. @@ -79,15 +82,14 @@ def generate_cubes_scene( api: The type of API that the cubes should have. kinematic_enabled: Whether the cubes are kinematic. device: Device to use for the simulation. + add_ground_plane: Whether the simulation context authored a shared ground plane. Returns: A tuple containing the rigid object representing the cubes and the origins of the cubes. """ - origins = torch.tensor([(i * 1.0, 0, height) for i in range(num_cubes)]).to(device) - # Create Top-level Xforms, one for each cube - for i, origin in enumerate(origins): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=origin) + origins = np.asarray([(i * 1.0, 0, height) for i in range(num_cubes)], dtype=np.float32) + sim_utils.create_prim("/World/Env_0", "Xform", translation=origins[0]) # Resolve spawn configuration if api == "none": @@ -116,9 +118,13 @@ def generate_cubes_scene( spawn=spawn_cfg, init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 0.0, height)), ) + cfgs = [cube_object_cfg] + if add_ground_plane: + cfgs.append(AssetBaseCfg(prim_path="/World/defaultGroundPlane")) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), cfgs, num_cubes, 1.0, positions=origins) cube_object = RigidObject(cfg=cube_object_cfg) - return cube_object, origins + return cube_object, torch.as_tensor(origins, device=device) @pytest.mark.isaacsim_ci @@ -135,6 +141,7 @@ def test_initialization(num_cubes, device): assert sys.getrefcount(cube_object) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if object is initialized @@ -166,6 +173,7 @@ def test_initialization_rejects_non_rigid_body_prims(api): # Check that the framework doesn't hold excessive strong references. assert sys.getrefcount(cube_object) < 10 + replicate(sim.get_clone_plan()) with pytest.raises(RuntimeError): sim.reset() @@ -182,9 +190,10 @@ def test_external_force_buffer(device): # Generate cubes scene with _newton_sim_context(device, add_ground_plane=True, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None - cube_object, origins = generate_cubes_scene(num_cubes=1, device=device) + cube_object, origins = generate_cubes_scene(num_cubes=1, device=device, add_ground_plane=True) # play the simulator + replicate(sim.get_clone_plan()) sim.reset() # find bodies to apply the force @@ -253,9 +262,10 @@ def test_external_force_on_single_body(num_cubes, device): # Generate cubes scene with _newton_sim_context(device, add_ground_plane=True, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None - cube_object, origins = generate_cubes_scene(num_cubes=num_cubes, device=device) + cube_object, origins = generate_cubes_scene(num_cubes=num_cubes, device=device, add_ground_plane=True) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Find bodies to apply the force @@ -328,9 +338,10 @@ def test_external_force_on_single_body_at_position(num_cubes, device): # Generate cubes scene with _newton_sim_context(device, add_ground_plane=True, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None - cube_object, origins = generate_cubes_scene(num_cubes=num_cubes, device=device) + cube_object, origins = generate_cubes_scene(num_cubes=num_cubes, device=device, add_ground_plane=True) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() # Find bodies to apply the force @@ -420,6 +431,7 @@ def test_set_rigid_object_state(num_cubes, device): cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, device=device) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() state_types = ["root_pos_w", "root_quat_w", "root_lin_vel_w", "root_ang_vel_w"] @@ -481,6 +493,7 @@ def test_reset_rigid_object(num_cubes, device): cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, device=device) # Play the simulator + replicate(sim.get_clone_plan()) sim.reset() for i in range(5): @@ -520,21 +533,22 @@ def test_rigid_body_set_mass(num_cubes, device): """Test that selected mass writes update inverse mass and inertia across static transitions.""" with _newton_sim_context(device, gravity_enabled=False, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None - for index in range(num_cubes): - sim_utils.create_prim(f"/World/Env_{index}", "Xform", translation=(float(index), 0.0, 1.0)) - cube_object = RigidObject( - RigidObjectCfg( - prim_path="/World/Env_[^/]*/Object", - spawn=sim_utils.CuboidCfg( - size=(0.2, 0.2, 0.2), - rigid_props=PhysxRigidBodyCfg(disable_gravity=True), - mass_props=sim_utils.MassCfg(mass=1.0), - collision_props=sim_utils.UsdPhysicsCollisionCfg(), - ), - ) + origins = np.asarray([(float(index), 0.0, 1.0) for index in range(num_cubes)], dtype=np.float32) + sim_utils.create_prim("/World/Env_0", "Xform", translation=origins[0]) + cfg = RigidObjectCfg( + prim_path="/World/Env_[^/]*/Object", + spawn=sim_utils.CuboidCfg( + size=(0.2, 0.2, 0.2), + rigid_props=PhysxRigidBodyCfg(disable_gravity=True), + mass_props=sim_utils.MassCfg(mass=1.0), + collision_props=sim_utils.UsdPhysicsCollisionCfg(), + ), ) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), (cfg,), num_cubes, 1.0, positions=origins) + cube_object = RigidObject(cfg) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Get masses before updating one environment. @@ -611,6 +625,7 @@ def test_gravity_vec_w(num_cubes, device, gravity_enabled): expected_g = (0.0, 0.0, 0.0) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check that gravity is set correctly @@ -644,6 +659,7 @@ def test_gravity_vec_w_tracks_model_gravity(num_cubes, device): with _newton_sim_context(device, gravity_enabled=True) as sim: sim._app_control_on_stop_handle = None cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() # GRAVITY_VEC_W must share storage with Newton's per-env gravity array. @@ -686,6 +702,7 @@ def test_body_root_state_properties(num_cubes, device, with_offset): cube_object, env_pos = generate_cubes_scene(num_cubes=num_cubes, height=0.0, device=device) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if cube_object is initialized @@ -798,6 +815,7 @@ def test_write_root_state(num_cubes, device, with_offset, state_location): env_idx = torch.tensor([x for x in range(num_cubes)], dtype=torch.int32, device=device) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if cube_object is initialized @@ -884,6 +902,7 @@ def test_write_state_functions_data_consistency(num_cubes, device, with_offset, cube_object, env_pos = generate_cubes_scene(num_cubes=num_cubes, height=0.0, device=device) # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if cube_object is initialized @@ -1015,6 +1034,7 @@ def _fk_reset_mask_dirty() -> bool: sim._app_control_on_stop_handle = None cube_object, _ = generate_cubes_scene(num_cubes=num_cubes, height=0.5, device=device) + replicate(sim.get_clone_plan()) sim.reset() assert cube_object.is_initialized diff --git a/source/isaaclab_newton/test/assets/test_rigid_object_collection.py b/source/isaaclab_newton/test/assets/test_rigid_object_collection.py index 2abff5889673..183aae9276f6 100644 --- a/source/isaaclab_newton/test/assets/test_rigid_object_collection.py +++ b/source/isaaclab_newton/test/assets/test_rigid_object_collection.py @@ -19,6 +19,7 @@ import sys +import numpy as np import pytest import torch import warp as wp @@ -29,7 +30,8 @@ from newton import ModelFlags import isaaclab.sim as sim_utils -from isaaclab.assets import RigidObjectCfg, RigidObjectCollectionCfg +from isaaclab.assets import AssetBaseCfg, RigidObjectCfg, RigidObjectCollectionCfg +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.sim import SimulationCfg, build_simulation_context from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR from isaaclab.utils.math import ( @@ -87,10 +89,8 @@ def generate_cubes_scene( A tuple containing the rigid object collection representing the cubes and the origins of the cubes. """ - origins = torch.tensor([(i * 3.0, 0, height) for i in range(num_envs)]).to(device) - # Create Top-level Xforms, one for each cube - for i, origin in enumerate(origins): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=origin) + origins = np.asarray([(i * 3.0, 0, height) for i in range(num_envs)], dtype=np.float32) + sim_utils.create_prim("/World/Env_0", "Xform", translation=origins[0]) # Resolve spawn configuration if has_api: @@ -110,13 +110,17 @@ def generate_cubes_scene( for i in range(num_cubes): cube_object_cfg = RigidObjectCfg( prim_path=f"/World/Env_[^/]*/Object_{i}", - spawn=spawn_cfg, + spawn=spawn_cfg.copy(), init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, 3 * i, height)), ) cube_config_dict[f"cube_{i}"] = cube_object_cfg + cfgs = list(cube_config_dict.values()) + if spawn_unrelated_sibling: + cfgs.append(AssetBaseCfg(prim_path="/World/Env_[^/]*/UnrelatedObject")) + clone_plan_from_env_0(CloneCfg(clone_template="/World/Env_{}"), cfgs, num_envs, 3.0, positions=origins) if spawn_unrelated_sibling: spawn_cfg.func( - "/World/Env_[^/]*/UnrelatedObject", + "/World/Env_0/UnrelatedObject", spawn_cfg, translation=(0.0, -3.0, height), ) @@ -124,7 +128,7 @@ def generate_cubes_scene( cube_object_collection_cfg = RigidObjectCollectionCfg(rigid_objects=cube_config_dict) cube_object_collection = RigidObjectCollection(cfg=cube_object_collection_cfg) - return cube_object_collection, origins + return cube_object_collection, torch.as_tensor(origins, device=device) @pytest.mark.parametrize("device", test_devices()) @@ -141,6 +145,7 @@ def test_initialization_ignores_unrelated_sibling_rigid_objects(device): spawn_unrelated_sibling=True, ) + replicate(sim.get_clone_plan()) sim.reset() assert object_collection.num_instances == num_envs @@ -160,6 +165,7 @@ def test_initialization(num_envs, num_cubes, device): assert sys.getrefcount(object_collection) < 10 # Play sim + replicate(sim.get_clone_plan()) sim.reset() # Check if object is initialized @@ -185,26 +191,33 @@ def test_set_body_inertial_properties_updates_inverses(device): num_cubes = 3 with _newton_sim_context(device, gravity_enabled=False, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None - for env_index in range(num_envs): - sim_utils.create_prim(f"/World/Env_{env_index}", "Xform", translation=(float(env_index), 0.0, 1.0)) + origins = np.asarray([(float(env_index), 0.0, 1.0) for env_index in range(num_envs)], dtype=np.float32) + sim_utils.create_prim("/World/Env_0", "Xform", translation=origins[0]) spawn_cfg = sim_utils.CuboidCfg( size=(0.2, 0.2, 0.2), rigid_props=PhysxRigidBodyCfg(disable_gravity=True), mass_props=sim_utils.MassCfg(mass=1.0), collision_props=sim_utils.UsdPhysicsCollisionCfg(), ) - object_collection = RigidObjectCollection( - RigidObjectCollectionCfg( - rigid_objects={ - f"cube_{body_index}": RigidObjectCfg( - prim_path=f"/World/Env_[^/]*/Object_{body_index}", - spawn=spawn_cfg, - init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, float(body_index), 0.0)), - ) - for body_index in range(num_cubes) - } - ) + cfg = RigidObjectCollectionCfg( + rigid_objects={ + f"cube_{body_index}": RigidObjectCfg( + prim_path=f"/World/Env_[^/]*/Object_{body_index}", + spawn=spawn_cfg.copy(), + init_state=RigidObjectCfg.InitialStateCfg(pos=(0.0, float(body_index), 0.0)), + ) + for body_index in range(num_cubes) + } + ) + clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), + cfg.rigid_objects.values(), + num_envs, + 1.0, + positions=origins, ) + object_collection = RigidObjectCollection(cfg) + replicate(sim.get_clone_plan()) sim.reset() env_mask = wp.array([True, False], dtype=wp.bool, device=device) @@ -254,6 +267,7 @@ def test_initialization_with_no_rigid_body(): assert sys.getrefcount(object_collection) < 10 # Play sim + replicate(sim.get_clone_plan()) with pytest.raises(RuntimeError): sim.reset() @@ -266,6 +280,7 @@ def test_external_force_buffer(device): num_envs = 2 num_cubes = 1 object_collection, origins = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() # find objects to apply the force @@ -320,6 +335,7 @@ def test_external_force_on_single_body(num_envs, num_cubes, device): with _newton_sim_context(device, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None object_collection, origins = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() # find objects to apply the force @@ -387,6 +403,7 @@ def test_external_force_on_single_body_at_position(num_envs, num_cubes, device): with _newton_sim_context(device, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None object_collection, origins = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() # find objects to apply the force @@ -470,6 +487,7 @@ def test_set_object_state(num_envs, num_cubes, device): with _newton_sim_context(device, gravity_enabled=False, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None object_collection, origins = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() state_types = ["body_link_pos_w", "body_link_quat_w", "body_com_lin_vel_w", "body_com_ang_vel_w"] @@ -537,6 +555,7 @@ def test_reset_object_collection(num_envs, num_cubes, device): with _newton_sim_context(device, gravity_enabled=True, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None object_collection, _ = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() for i in range(5): @@ -579,6 +598,7 @@ def test_gravity_vec_w(num_envs, num_cubes, device, gravity_enabled): # per-instance-per-body). expected_g = (0.0, 0.0, -9.81) if gravity_enabled else (0.0, 0.0, 0.0) + replicate(sim.get_clone_plan()) sim.reset() # Check if gravity vector is set correctly @@ -614,6 +634,7 @@ def test_gravity_vec_w_tracks_model_gravity(num_envs, num_cubes, device): with _newton_sim_context(device, gravity_enabled=True, auto_add_lighting=True) as sim: sim._app_control_on_stop_handle = None object_collection, _ = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, device=device) + replicate(sim.get_clone_plan()) sim.reset() # GRAVITY_VEC_W must share storage with Newton's per-env gravity array. @@ -651,6 +672,7 @@ def test_object_state_properties(num_envs, num_cubes, device, with_offset): sim._app_control_on_stop_handle = None cube_object, env_pos = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, height=0.0, device=device) + replicate(sim.get_clone_plan()) sim.reset() # check if cube_object is initialized @@ -746,6 +768,7 @@ def test_write_object_state(num_envs, num_cubes, device, with_offset, state_loca env_ids = torch.tensor([x for x in range(num_envs)], dtype=torch.int32) object_ids = torch.tensor([x for x in range(num_cubes)], dtype=torch.int32) + replicate(sim.get_clone_plan()) sim.reset() # Check if cube_object is initialized @@ -824,6 +847,7 @@ def test_write_object_state_functions_data_consistency(num_envs, num_cubes, devi env_ids = torch.tensor([x for x in range(num_envs)], dtype=torch.int32) object_ids = torch.tensor([x for x in range(num_cubes)], dtype=torch.int32) + replicate(sim.get_clone_plan()) sim.reset() # Check if cube_object is initialized @@ -974,6 +998,7 @@ def _fk_reset_mask_dirty() -> bool: sim._app_control_on_stop_handle = None cube_object, _ = generate_cubes_scene(num_envs=num_envs, num_cubes=num_cubes, height=0.5, device=device) + replicate(sim.get_clone_plan()) sim.reset() assert cube_object.is_initialized diff --git a/source/isaaclab_newton/test/cloner/test_newton_builder_world_hook.py b/source/isaaclab_newton/test/cloner/test_newton_builder_world_hook.py index f2b697b76574..e0a8ef5d960f 100644 --- a/source/isaaclab_newton/test/cloner/test_newton_builder_world_hook.py +++ b/source/isaaclab_newton/test/cloner/test_newton_builder_world_hook.py @@ -80,14 +80,23 @@ def test_copy_newton_clone_source_owns_mutable_geometry(monkeypatch): def test_explicit_global_import_uses_global_world( monkeypatch, load_visual_shapes, is_rendering, rgb_array, visual_shapes_required, expected ): - """Global imports honor visual requirements and keep colliders in Newton world -1.""" + """Global imports honor visual requirements and leave native deformables to world hooks.""" stage = Usd.Stage.CreateInMemory() UsdPhysics.Scene.Define(stage, "/physicsScene") UsdGeom.Xform.Define(stage, "/World") ground = UsdGeom.Cube.Define(stage, "/World/Ground") UsdPhysics.CollisionAPI.Apply(ground.GetPrim()) UsdLux.DistantLight.Define(stage, "/World/Light") - global_paths = ("/World/Ground", "/World/Light") + points = [(0.0, 0.0, 0.0), (0.1, 0.0, 0.0), (0.0, 0.1, 0.0), (0.0, 0.0, 0.1)] + native_mesh = UsdGeom.TetMesh.Define(stage, "/World/Native/sim") + native_mesh.CreatePointsAttr(points) + native_mesh.CreateTetVertexIndicesAttr([(0, 1, 2, 3)]) + global_paths = ("/World/Ground", "/World/Light", "/World/Native") + + def add_native_particles(builder, *_args): + builder.add_particles( + pos=points, vel=[(0.0, 0.0, 0.0)] * len(points), mass=[0.01] * len(points), radius=[0.005] * len(points) + ) builder = newton.ModelBuilder() add_usd = mock.Mock(wraps=builder.add_usd) @@ -110,9 +119,9 @@ def test_explicit_global_import_uses_global_world( visual_shapes_required=visual_shapes_required, ), ) - monkeypatch.setattr(replicate_module.NewtonManager, "_deformable_registry", ()) + monkeypatch.setattr(NewtonManager, "_deformable_registry", (SimpleNamespace(prim_path="/World/Native"),)) monkeypatch.setattr(replicate_module.NewtonManager, "_cl_inject_sites", mock.Mock(return_value=({}, {}, {}))) - monkeypatch.setattr(replicate_module.NewtonManager, "_per_world_builder_hooks", ()) + monkeypatch.setattr(NewtonManager, "_per_world_builder_hooks", (add_native_particles,)) monkeypatch.setattr(replicate_module, "replace_newton_builder_shape_colors", mock.Mock()) monkeypatch.setattr(NewtonManager, "_builder", None) monkeypatch.setattr(NewtonManager, "_cl_site_index_map", {}) @@ -139,4 +148,5 @@ def test_explicit_global_import_uses_global_world( ground_index = model.shape_label.index("/World/Ground") assert model.shape_world.numpy()[ground_index] == -1 assert model.world_count == 2 + assert model.particle_count == len(points) * model.world_count assert "/World/Light" not in model.shape_label # USD lights are not Newton physics entities. diff --git a/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py b/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py index 2123fd766a64..000a13bda17f 100644 --- a/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py +++ b/source/isaaclab_newton/test/physics/test_newton_fabric_body_sync.py @@ -28,6 +28,7 @@ from pxr import UsdGeom from usdrt import Gf, Rt +import isaaclab.cloner as cloner import isaaclab.sim as sim_utils from isaaclab.assets import AssetBaseCfg, CableObjectCfg, RigidObjectCfg from isaaclab.scene import InteractiveScene, InteractiveSceneCfg @@ -341,6 +342,9 @@ def test_periodic_cable_is_skipped_by_fabric_sync(): curve = UsdGeom.BasisCurves(sim_utils.get_current_stage().GetPrimAtPath("/World/Cable/geometry/mesh")) curve.GetWrapAttr().Set(UsdGeom.Tokens.periodic) + plan = cloner.make_clone_plan((), 1, 0.0, global_paths=("/World/Cable",)) + sim.set_clone_plan(plan) + cloner.replicate(plan) sim.reset() assert NewtonManager._cable_shape_ids is None diff --git a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py index da1ac15c6e30..a12c5ccf997e 100644 --- a/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py +++ b/source/isaaclab_newton/test/physics/test_newton_manager_abstraction.py @@ -1499,10 +1499,8 @@ def test_initialize_solver_populates_canonical_state( to MJCF; a ground-plane-only scene fails MJCF conversion. 3. Kamino's internal collision detector requires collidable geometry to construct its collision pipeline. - 4. Pre-populating ``NewtonManager._builder`` causes - :meth:`NewtonManager.start_simulation` to skip - :meth:`instantiate_builder_from_stage`, so the test does not depend on - USD asset packages. + 4. Supplying a native builder keeps initialization independent of USD asset + packages and clone planning. """ solver_cfg = solver_cfg_factory() sim_cfg = SimulationCfg( diff --git a/source/isaaclab_newton/test/physics/test_newton_solver_reset.py b/source/isaaclab_newton/test/physics/test_newton_solver_reset.py index db6f622e7863..9694220f48db 100644 --- a/source/isaaclab_newton/test/physics/test_newton_solver_reset.py +++ b/source/isaaclab_newton/test/physics/test_newton_solver_reset.py @@ -12,6 +12,7 @@ from unittest.mock import patch +import numpy as np import pytest import torch import warp as wp @@ -24,14 +25,14 @@ import isaaclab.sim as sim_utils from isaaclab.actuators import IdealPDActuatorCfg from isaaclab.assets import ArticulationCfg +from isaaclab.cloner import CloneCfg, clone_plan_from_env_0, replicate from isaaclab.sim import SimulationCfg, build_simulation_context from isaaclab.utils.assets import ISAAC_NUCLEUS_DIR def _generate_single_joint_articulations(num_articulations: int, device: str) -> Articulation: """Spawn ``num_articulations`` copies of the simple revolute articulation, one per env prim.""" - for i in range(num_articulations): - sim_utils.create_prim(f"/World/Env_{i}", "Xform", translation=(i * 2.5, 0.0, 0.0)) + sim_utils.create_prim("/World/Env_0", "Xform") articulation_cfg = ArticulationCfg( prim_path="/World/Env_[^/]*/Robot", spawn=sim_utils.UsdFileCfg( @@ -48,7 +49,14 @@ def _generate_single_joint_articulations(num_articulations: int, device: str) -> ), }, ) - return Articulation(articulation_cfg) + positions = np.zeros((num_articulations, 3), dtype=np.float32) + positions[:, 0] = np.arange(num_articulations) * 2.5 + plan = clone_plan_from_env_0( + CloneCfg(clone_template="/World/Env_{}"), (articulation_cfg,), num_articulations, 2.5, positions=positions + ) + articulation = Articulation(articulation_cfg) + replicate(plan) + return articulation @pytest.mark.parametrize("device", ["cuda:0"]) diff --git a/source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py b/source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py index 479c4ea31f78..9cc18abc5912 100644 --- a/source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py +++ b/source/isaaclab_newton/test/sensors/test_newton_raycast_sensor.py @@ -23,6 +23,7 @@ ) from newton import ShapeFlags +import isaaclab.cloner as cloner import isaaclab.sim as sim_utils from isaaclab.assets import RigidObject, RigidObjectCfg from isaaclab.scene import InteractiveScene, InteractiveSceneCfg @@ -190,6 +191,9 @@ def test_legacy_multi_mesh_tracks_ad_hoc_regex_target(sim): ) sensor = MultiMeshRayCaster(sensor_cfg) + plan = cloner.make_clone_plan((), 1, 0.0, global_paths=("/World/Origin_00/Obstacle",)) + sim.set_clone_plan(plan) + cloner.replicate(plan) sim.reset() sensor.update(sim.get_physics_dt(), force_recompute=True) diff --git a/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py b/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py index 2e3c65aaba49..831a154ed7bd 100644 --- a/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py +++ b/source/isaaclab_newton/test/sim/test_views_xform_prim_newton.py @@ -29,6 +29,7 @@ from pxr import Sdf +import isaaclab.cloner as cloner import isaaclab.sim as sim_utils from isaaclab.assets import RigidObjectCfg from isaaclab.scene import InteractiveScene, InteractiveSceneCfg @@ -146,6 +147,10 @@ def test_non_colliding_shapes_after_finalize(device): site_schemas.prependedItems = ["MjcSiteAPI"] site_prim.SetMetadata("apiSchemas", site_schemas) sim_utils.create_prim(VISUAL_PATH, prim_type="Cube", scale=(0.01, 0.01, 0.01)) + sim.require_visual_shapes() + plan = cloner.make_clone_plan((), 1, 0.0, global_paths=("/World/defaultGroundPlane", "/World/Robot")) + sim.set_clone_plan(plan) + cloner.replicate(plan) sim.reset() shape_labels = list(NewtonManager.get_model().shape_label)