Skip to content
Open
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
34 changes: 33 additions & 1 deletion docs/source/overview/sim/sim_articulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,41 @@ Articulations are configured using the {class}`~cfg.ArticulationCfg` dataclass.
| `body_scale` | `List[float]` | `[1.0, 1.0, 1.0]` | Scaling factors for the articulation links. |
| `disable_self_collisions` | `bool` | `True` | Whether to disable self-collisions. |
| `drive_props` | `JointDrivePropertiesCfg` | `...` | Default drive properties. |
| `attrs` | `RigidBodyAttributesCfg` | `...` | Rigid body attributes configuration. |
| `attrs` | `RigidBodyAttributesCfg` | `...` | Default rigid body attributes applied to all links. |
| `link_attrs` | `dict[str, LinkPhysicsOverrideCfg]` | `None` | Optional per-link overrides keyed by group name; each group matches link names via regex. |


### Per-link physics (`link_attrs`)

By default, `attrs` applies the same rigid-body physics to every link. Use `link_attrs` to
override specific links (matched by regex, same rules as joint drive dict keys):

```python
from embodichain.lab.sim.cfg import (
ArticulationCfg,
LinkPhysicsOverrideCfg,
RigidBodyAttributesCfg,
RigidBodyAttributesOverrideCfg,
)

art_cfg = ArticulationCfg(
fpath="path/to/robot.urdf",
attrs=RigidBodyAttributesCfg(static_friction=0.5),
link_attrs={
"eef": LinkPhysicsOverrideCfg(
link_names_expr=[".*(hand|finger|ee).*"],
attrs=RigidBodyAttributesOverrideCfg(
static_friction=0.95,
contact_offset=0.001,
),
),
},
)
```

At runtime, use `articulation.set_link_physical_attr(...)` and `get_link_physical_attr(...)`
for the same partial-override behavior.

### Drive Configuration

The `drive_props` parameter controls the joint physics behavior. It is defined using the `JointDrivePropertiesCfg` class. For articulation object without internal drive force, like cabinet and drawer, better set `drive_type` to `"none"`.
Expand Down
148 changes: 147 additions & 1 deletion embodichain/lab/sim/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,106 @@ def from_dict(
return cfg


@configclass
class RigidBodyAttributesOverrideCfg:
"""Partial rigid-body attribute overrides for per-link physics configuration.

Fields set to ``None`` are not applied and retain values from the base
:class:`RigidBodyAttributesCfg`.
"""

mass: float | None = None
density: float | None = None
angular_damping: float | None = None
linear_damping: float | None = None
max_depenetration_velocity: float | None = None
sleep_threshold: float | None = None
min_position_iters: int | None = None
min_velocity_iters: int | None = None
max_linear_velocity: float | None = None
max_angular_velocity: float | None = None
enable_ccd: bool | None = None
contact_offset: float | None = None
rest_offset: float | None = None
enable_collision: bool | None = None
restitution: float | None = None
dynamic_friction: float | None = None
static_friction: float | None = None

def merge_with(self, base: RigidBodyAttributesCfg) -> PhysicalAttr:
"""Build a :class:`~dexsim.types.PhysicalAttr` from base values and overrides."""
merged = RigidBodyAttributesCfg()
for field_name in merged.__dataclass_fields__:
override_val = getattr(self, field_name)
if override_val is not None:
setattr(merged, field_name, override_val)
else:
setattr(merged, field_name, getattr(base, field_name))
return merged.attr()

@classmethod
def from_dict(
cls, init_dict: Dict[str, Union[str, float, int, bool]]
) -> RigidBodyAttributesOverrideCfg:
"""Initialize the configuration from a dictionary."""
cfg = cls()
for key, value in init_dict.items():
if hasattr(cfg, key):
setattr(cfg, key, value)
else:
logger.log_warning(
f"Key '{key}' not found in {cfg.__class__.__name__}."
)
return cfg


@configclass
class LinkPhysicsOverrideCfg:
"""Per-link physics override matched by regex on articulation link names."""

link_names_expr: list[str] = MISSING
"""Regex patterns matched against link names (full match)."""

attrs: RigidBodyAttributesOverrideCfg = RigidBodyAttributesOverrideCfg()
"""Partial attribute overrides applied on top of :attr:`ArticulationCfg.attrs`."""

replace_inertial: bool = False
"""Whether to recompute inertia when mass is overridden (DexSim flag)."""

@classmethod
def from_dict(cls, init_dict: Dict[str, Any]) -> LinkPhysicsOverrideCfg:
"""Initialize the configuration from a dictionary."""
cfg = cls()
for key, value in init_dict.items():
if key == "attrs" and isinstance(value, dict):
setattr(cfg, key, RigidBodyAttributesOverrideCfg.from_dict(value))
elif hasattr(cfg, key):
setattr(cfg, key, value)
else:
logger.log_warning(
f"Key '{key}' not found in {cfg.__class__.__name__}."
)
return cfg


def link_attrs_from_dict(
value: dict[str, Any],
) -> dict[str, LinkPhysicsOverrideCfg]:
"""Parse a ``link_attrs`` mapping from YAML/JSON-style dicts."""
link_attrs: dict[str, LinkPhysicsOverrideCfg] = {}
for group_name, group_cfg in value.items():
if isinstance(group_cfg, LinkPhysicsOverrideCfg):
link_attrs[group_name] = group_cfg
elif isinstance(group_cfg, dict):
link_attrs[group_name] = LinkPhysicsOverrideCfg.from_dict(group_cfg)
else:
raise TypeError(
f"link_attrs['{group_name}'] must be a dict or "
f"LinkPhysicsOverrideCfg, got {type(group_cfg)}."
)
return link_attrs


@configclass
class SoftbodyVoxelAttributesCfg:
# voxel config
Expand Down Expand Up @@ -1263,6 +1363,13 @@ class ArticulationCfg(ObjectBaseCfg):
The mass and density in attrs will only be used if specified.
"""

link_attrs: dict[str, LinkPhysicsOverrideCfg] | None = None
"""Named per-link physics override groups keyed by regex on link names.

Each group applies :attr:`LinkPhysicsOverrideCfg.attrs` on top of :attr:`attrs` for
matched links only. A link must not match more than one group.
"""

fix_base: bool = True
"""Whether to fix the base of the articulation.

Expand Down Expand Up @@ -1306,6 +1413,43 @@ class ArticulationCfg(ObjectBaseCfg):
Only effective for USD files, ignored for URDF files.
"""

@classmethod
def from_dict(
cls, init_dict: Dict[str, Union[str, float, tuple, dict]]
) -> ArticulationCfg:
"""Initialize the configuration from a dictionary."""
cfg = cls()
for key, value in init_dict.items():
if key == "link_attrs" and isinstance(value, dict):
cfg.link_attrs = link_attrs_from_dict(value)
elif hasattr(cfg, key):
attr = getattr(cfg, key)
if is_configclass(attr):
setattr(cfg, key, attr.from_dict(value))
else:
setattr(cfg, key, value)
else:
logger.log_warning(
f"Key '{key}' not found in {cfg.__class__.__name__}."
)

if cfg.init_local_pose is None:
from scipy.spatial.transform import Rotation as R

T = np.eye(4)
T[:3, 3] = np.array(cfg.init_pos)
T[:3, :3] = R.from_euler("xyz", np.deg2rad(cfg.init_rot)).as_matrix()
cfg.init_local_pose = T
else:
from scipy.spatial.transform import Rotation as R

cfg.init_pos = tuple(cfg.init_local_pose[:3, 3])
cfg.init_rot = tuple(
R.from_matrix(cfg.init_local_pose[:3, :3]).as_euler("xyz", degrees=True)
)

return cfg


@configclass
class RobotCfg(ArticulationCfg):
Expand Down Expand Up @@ -1354,7 +1498,9 @@ def from_dict(cls, init_dict: Dict[str, Union[str, float, tuple]]) -> RobotCfg:

cfg = cls() # Create a new instance of the class (cls)
for key, value in init_dict.items():
if hasattr(cfg, key):
if key == "link_attrs" and isinstance(value, dict):
cfg.link_attrs = link_attrs_from_dict(value)
elif hasattr(cfg, key):
attr = getattr(cfg, key)
if key == "urdf_cfg":
from embodichain.lab.sim.cfg import URDFCfg
Expand Down
89 changes: 88 additions & 1 deletion embodichain/lab/sim/objects/articulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@
from dexsim.engine import CudaArray, PhysicsScene

from embodichain.lab.sim import VisualMaterialInst, VisualMaterial
from embodichain.lab.sim.cfg import ArticulationCfg, JointDrivePropertiesCfg
from embodichain.lab.sim.cfg import (
ArticulationCfg,
JointDrivePropertiesCfg,
RigidBodyAttributesCfg,
RigidBodyAttributesOverrideCfg,
)
from dexsim.types import PhysicalAttr
from embodichain.utils.string import resolve_matching_names
from embodichain.lab.sim.common import BatchEntity
from embodichain.utils.math import (
matrix_from_quat,
Expand Down Expand Up @@ -1289,6 +1296,86 @@ def get_mass(
)
return mass_tensor

def get_link_physical_attr(
self,
link_names: str | Sequence[str] | None = None,
env_ids: Sequence[int] | None = None,
) -> list[PhysicalAttr]:
"""Get physical attributes for articulation links.

Args:
link_names: Link names or regex patterns. If None, all links are returned.
env_ids: Environment indices. If None, only env 0 is queried.

Returns:
List of :class:`~dexsim.types.PhysicalAttr`, one per (env, link) pair in
row-major order (env-major).
"""
if link_names is None:
matched_link_names = self.link_names
elif isinstance(link_names, str):
_, matched_link_names = resolve_matching_names(
keys=link_names, list_of_strings=self.link_names
)
else:
_, matched_link_names = resolve_matching_names(
keys=link_names, list_of_strings=self.link_names
)

local_env_ids = [0] if env_ids is None else list(env_ids)
attrs: list[PhysicalAttr] = []
for env_idx in local_env_ids:
for name in matched_link_names:
attrs.append(self._entities[env_idx].get_physical_attr(name))
return attrs

def set_link_physical_attr(
self,
attrs: RigidBodyAttributesCfg | RigidBodyAttributesOverrideCfg | PhysicalAttr,
link_names: str | Sequence[str] | None = None,
env_ids: Sequence[int] | None = None,
*,
base_attrs: RigidBodyAttributesCfg | None = None,
replace_inertial: bool = False,
) -> None:
"""Set physical attributes for selected articulation links.

Args:
attrs: Full, partial, or DexSim physical attributes to apply.
link_names: Link names or regex patterns. If None, all links are updated.
env_ids: Environment indices. If None, all environments are updated.
base_attrs: Base config used when ``attrs`` is a partial override.
replace_inertial: Recompute inertia when mass changes.
"""
if link_names is None:
matched_link_names = self.link_names
elif isinstance(link_names, str):
_, matched_link_names = resolve_matching_names(
keys=link_names, list_of_strings=self.link_names
)
else:
_, matched_link_names = resolve_matching_names(
keys=link_names, list_of_strings=self.link_names
)

if isinstance(attrs, RigidBodyAttributesOverrideCfg):
if base_attrs is None:
base_attrs = self.cfg.attrs
physical_attr = attrs.merge_with(base_attrs)
if attrs.mass is not None:
replace_inertial = True
elif isinstance(attrs, RigidBodyAttributesCfg):
physical_attr = attrs.attr()
else:
physical_attr = attrs

local_env_ids = self._all_indices if env_ids is None else env_ids
for env_idx in local_env_ids:
for name in matched_link_names:
self._entities[env_idx].set_physical_attr(
physical_attr, name, is_replace_inertial=replace_inertial
)

def set_joint_drive(
self,
stiffness: torch.Tensor | None = None,
Expand Down
Loading
Loading