Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/source/overview/sim/sim_manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,44 @@ The manager provides methods to add, retrieve and remove various simulation asse

For more details on simulation assets, please refer to their respective documentation pages.

### USD Import and Export

#### Importing USD Files

EmbodiChain supports importing USD files (`.usd`, `.usda`, `.usdc`) for both rigid objects and articulations. When importing USD files, you can choose whether to use the physical properties defined in the USD file or override them with configuration values:

```python
# Import rigid object with USD properties
rigid_cfg = RigidObjectCfg(
shape=MeshCfg(fpath=get_data_path("path/to/object.usd")),
use_usd_properties=True # Use properties from USD file
)
obj = sim.add_rigid_object(cfg=rigid_cfg)

# Import articulation with USD properties
robot_cfg = ArticulationCfg(
fpath=get_data_path("path/to/robot.usd"),
use_usd_properties=True # Use joint drive properties from USD
)
robot = sim.add_articulation(cfg=robot_cfg)
```

#### Exporting to USD

You can export the current simulation scene to a USD file using the `export_usd()` method:

```python
# Export the entire scene to USD
sim.export_usd("my_scene.usda")
```

This exports all objects, articulations, robots, and their current states to a USD file, which can be:
- Reimported into EmbodiChain with preserved properties
- Opened in USD-compatible tools (e.g., USD Viewer, Omniverse)
- Used as assets for other simulations

See `scripts/tutorials/sim/export_usd.py` for a complete example.

## Simulation Loop

### Manual Update mode
Expand Down
18 changes: 18 additions & 0 deletions embodichain/lab/sim/objects/articulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,24 @@ def __init__(
self.default_joint_max_effort = self._data.qf_limits.clone()
self.default_joint_max_velocity = self._data.qvel_limits.clone()

# Write the USD properties back to cfg
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from joint drive properties, we also have link physics properties

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion, this commit will not include this change

usd_drive_pros = self.cfg.drive_pros
usd_drive_pros.stiffness = (
self.default_joint_stiffness[0].cpu().numpy().tolist()
)
usd_drive_pros.damping = (
self.default_joint_damping[0].cpu().numpy().tolist()
)
usd_drive_pros.friction = (
self.default_joint_friction[0].cpu().numpy().tolist()
)
usd_drive_pros.max_effort = (
self.default_joint_max_effort[0].cpu().numpy().tolist()
)
usd_drive_pros.max_velocity = (
self.default_joint_max_velocity[0].cpu().numpy().tolist()
)
Comment on lines +632 to +648
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JointDrivePropertiesCfg fields (stiffness, damping, friction, max_effort, max_velocity) are typed as Union[Dict[str, float], float]. However, .cpu().numpy().tolist() on a 1D tensor produces a list[float], which doesn't conform to either declared type. While this won't cause runtime issues in the current code path (since _set_default_joint_drive is only called in the not use_usd_properties branch), it could cause problems if code downstream of this point inspects the cfg and expects either a scalar or a dict. Consider updating the type annotation of JointDrivePropertiesCfg to include List[float] as a valid type, or documenting that after USD property readback, these fields contain per-joint lists.

Copilot uses AI. Check for mistakes.

Comment on lines 631 to +649
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When use_usd_properties=True, this code writes per-DOF arrays (lists) back into JointDrivePropertiesCfg fields (stiffness, damping, etc.). Those fields are typed/handled as float | Dict[str, float] elsewhere (e.g., _set_default_joint_drive expects a number or a name->value mapping), so setting them to list can break later drive setup or user overrides. Consider storing USD-derived values as a Dict[joint_name, float] (or keep them on the Articulation instance) rather than mutating cfg.drive_pros to a list.

Suggested change
# Write the USD properties back to cfg
usd_drive_pros = self.cfg.drive_pros
usd_drive_pros.stiffness = (
self.default_joint_stiffness[0].cpu().numpy().tolist()
)
usd_drive_pros.damping = (
self.default_joint_damping[0].cpu().numpy().tolist()
)
usd_drive_pros.friction = (
self.default_joint_friction[0].cpu().numpy().tolist()
)
usd_drive_pros.max_effort = (
self.default_joint_max_effort[0].cpu().numpy().tolist()
)
usd_drive_pros.max_velocity = (
self.default_joint_max_velocity[0].cpu().numpy().tolist()
)
# NOTE:
# We intentionally do not write these per-DOF USD properties back into
# `self.cfg.drive_pros`, because `JointDrivePropertiesCfg` fields such
# as `stiffness`, `damping`, etc. are expected to be either a scalar
# float or a Dict[str, float], not a list. The tensors above should
# be used directly wherever the full per-DOF information is required.

Copilot uses AI. Check for mistakes.
self.pk_chain = None
if self.cfg.build_pk_chain:
self.pk_chain = create_pk_chain(
Expand Down
12 changes: 10 additions & 2 deletions embodichain/lab/sim/objects/rigid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ def __init__(
for entity in entities:
entity.set_body_scale(*cfg.body_scale)
entity.set_physical_attr(cfg.attrs.attr())
else:
# Read current properties from USD-loaded entities and write back to cfg
# Use first entity as reference
first_entity: MeshObject = entities[0]

Comment on lines +219 to +223
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entities[0] is accessed when cfg.use_usd_properties is True, but load_mesh_objects_from_cfg() can return an empty list (e.g., USD import finds no rigid bodies and only logs an error). This will raise IndexError during object construction. Add a guard that validates entities is non-empty and raise/log a clear error before reading USD properties (or fall back to cfg defaults).

Copilot uses AI. Check for mistakes.
cfg.body_scale = tuple(first_entity.get_body_scale())
cfg.attrs = RigidBodyAttributesCfg().from_dict(
first_entity.get_physical_attr().as_dict()
)
Comment on lines +225 to +227
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RigidBodyAttributesCfg().from_dict(...) unnecessarily creates a throwaway instance before calling the classmethod from_dict. This should be RigidBodyAttributesCfg.from_dict(...) — calling the classmethod directly on the class rather than on a discarded instance.

Copilot uses AI. Check for mistakes.

if device.type == "cuda":
self._world.update(0.001)
Expand Down Expand Up @@ -872,8 +881,7 @@ def reset(self, env_ids: Sequence[int] | None = None) -> None:
local_env_ids = self._all_indices if env_ids is None else env_ids
num_instances = len(local_env_ids)

if not self.cfg.use_usd_properties:
self.set_attrs(self.cfg.attrs, env_ids=local_env_ids)
self.set_attrs(self.cfg.attrs, env_ids=local_env_ids)

pos = torch.as_tensor(
self.cfg.init_pos, dtype=torch.float32, device=self.device
Expand Down
17 changes: 17 additions & 0 deletions embodichain/lab/sim/sim_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,23 @@ def reset_objects_state(
if uid not in excluded_uids:
sensor.reset(env_ids)

def export_usd(self, fpath: str) -> bool:
"""Export the current simulation scene to a USD file.

Args:
fpath (str): The file path to save the USD file.

Returns:
bool: True if export is successful, False otherwise.
"""
try:
self._env.export_to_usd_file(fpath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should support exclusive uid list. (eg, remove ground plane from export)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussion, this commit will not include this change

logger.log_info(f"Simulation scene exported to USD file: {fpath}")
return True
except Exception as e:
logger.log_error(f"Failed to export simulation scene to USD: {e}")
return False

def destroy(self) -> None:
"""Destroy all simulated assets and release resources."""
# Clean up all gizmos before destroying the simulation
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dynamic = ["version"]
# Core install dependencies (kept from requirements.txt). Some VCS links are
# specified using PEP 508 direct references where present.
dependencies = [
"dexsim_engine==0.3.10",
"dexsim_engine==0.3.11",
"setuptools>=78.1.1",
"gymnasium>=0.29.1",
"langchain",
Expand Down
Loading
Loading