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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ trajectory so breaking changes can land in any minor release until the 1.0 stabi

### Added

- **`SimulationCfg` rigid-solver options** (ROADMAP M3.7) — eight optional fields exposing
Genesis `RigidOptions`: `enable_self_collision`, `enable_joint_limit`, `max_collision_pairs`
(contact); `solver_iterations`, `ls_iterations`, `solver_tolerance`, `integrator` (solver);
`constraint_timeconst` (constraint stiffness/damping). All default `None` ("use Genesis
default"); `SimulationCfg.rigid_options_kwargs()` maps the set ones to `RigidOptions` kwargs
and `InteractiveScene` passes `rigid_options` to `gs.Scene` **only when at least one is set**
— so existing configs are byte-for-byte unchanged. `integrator` is a string resolved to
`gs.integrator.<name>` in the scene (keeps `configs` Genesis-free, invariant #5). (Genesis
exposes no continuous-collision-detection / CCD knob, so it is not surfaced.) Tested in
`tests/test_configs.py`.

- **`ForceTorqueSensor`** (ROADMAP M3.5) — a joint force-torque sensor reporting each
selected joint's internal reaction force/torque from Genesis `get_dofs_force` (the
total internal DoF force, distinct from the commanded `applied_torque`). Select joints
Expand Down
38 changes: 38 additions & 0 deletions src/genelab/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,44 @@ class SimulationCfg:
# raise ``decimation`` if that's not what you want.
render_fps: int | None = 60

# --- Genesis rigid-solver options (ROADMAP M3.7) ---------------------------------------
# All optional; ``None`` = "use the Genesis default". Mapped to ``gs.options.RigidOptions``
# by :class:`~genelab.scene.InteractiveScene`, which only passes ``rigid_options`` to
# ``gs.Scene`` when at least one is set — so an unset config keeps the historic behaviour
# byte-for-byte. (Genesis exposes no continuous-collision-detection / CCD knob.)
enable_self_collision: bool | None = None # RigidOptions.enable_self_collision
enable_joint_limit: bool | None = None # RigidOptions.enable_joint_limit
max_collision_pairs: int | None = None # RigidOptions.max_collision_pairs
solver_iterations: int | None = None # RigidOptions.iterations (constraint solver)
ls_iterations: int | None = None # RigidOptions.ls_iterations (line search)
solver_tolerance: float | None = None # RigidOptions.tolerance
constraint_timeconst: float | None = (
None # RigidOptions.constraint_timeconst (stiffness/damping)
)
integrator: str | None = (
None # gs.integrator.<name>: Euler / implicitfast / approximate_implicitfast
)

def rigid_options_kwargs(self) -> dict[str, Any]:
"""Map the *set* rigid-solver fields to ``gs.options.RigidOptions`` kwargs.

Returns only the fields the user set (skips ``None``), keyed by the Genesis
``RigidOptions`` parameter names. ``integrator`` stays a string here — the scene
resolves it to ``gs.integrator.<name>`` so ``configs`` stays Genesis-free (invariant
#5). An empty dict means the scene won't pass ``rigid_options`` at all.
"""
mapping: dict[str, Any] = {
"enable_self_collision": self.enable_self_collision,
"enable_joint_limit": self.enable_joint_limit,
"max_collision_pairs": self.max_collision_pairs,
"iterations": self.solver_iterations,
"ls_iterations": self.ls_iterations,
"tolerance": self.solver_tolerance,
"constraint_timeconst": self.constraint_timeconst,
"integrator": self.integrator,
}
return {k: v for k, v in mapping.items() if v is not None}

@staticmethod
def play_retargeted_keys() -> tuple[str, ...]:
"""Override paths the CLI rewrites ``env.`` → ``play_env.`` in play mode.
Expand Down
13 changes: 13 additions & 0 deletions src/genelab/scene/interactive_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,17 @@ def build(self) -> None:
dt=float(self._sim_cfg.dt),
substeps=int(self._sim_cfg.substeps),
)
# Optional rigid-solver tuning (ROADMAP M3.7). The cfg keeps ``integrator`` as a
# string (so ``configs`` stays Genesis-free); resolve it to ``gs.integrator.<name>``
# here. ``rigid_options`` is only passed when the user set at least one field, so an
# untouched config leaves Genesis's defaults exactly as before.
rigid_kwargs = self._sim_cfg.rigid_options_kwargs()
if "integrator" in rigid_kwargs:
name = rigid_kwargs["integrator"]
integrator = getattr(gs.integrator, name, None)
if integrator is None:
raise ValueError(f"SimulationCfg.integrator={name!r} is not a gs.integrator member")
rigid_kwargs["integrator"] = integrator
# ``batch_render=True`` swaps in Genesis's BatchRenderer so attached cameras can
# emit per-env RGB-D tensors. Default ``None`` keeps the historic Rasterizer path.
renderer = (
Expand All @@ -186,6 +197,8 @@ def build(self) -> None:
renderer=renderer,
show_viewer=self._sim_cfg.vis,
)
if rigid_kwargs:
scene_kwargs["rigid_options"] = gs.options.RigidOptions(**rigid_kwargs)
if viewer_options is not None:
scene_kwargs["viewer_options"] = viewer_options
self._gs_scene = gs.Scene(**scene_kwargs)
Expand Down
34 changes: 34 additions & 0 deletions tests/test_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,37 @@ def test_play_retargeted_keys_target_real_simulation_fields() -> None:
def test_play_retargeted_keys_callable_on_class_and_instance() -> None:
"""Static method — same result whether called on the class or an instance."""
assert SimulationCfg.play_retargeted_keys() == SimulationCfg().play_retargeted_keys()


def test_rigid_options_kwargs_empty_by_default() -> None:
"""An untouched SimulationCfg yields no rigid_options kwargs (Genesis defaults preserved)."""
assert SimulationCfg().rigid_options_kwargs() == {}


def test_rigid_options_kwargs_maps_set_fields_to_genesis_names() -> None:
cfg = SimulationCfg(
enable_self_collision=False,
enable_joint_limit=True,
max_collision_pairs=200,
solver_iterations=80,
ls_iterations=20,
solver_tolerance=1e-6,
constraint_timeconst=0.02,
integrator="implicitfast",
)
assert cfg.rigid_options_kwargs() == {
"enable_self_collision": False,
"enable_joint_limit": True,
"max_collision_pairs": 200,
"iterations": 80, # solver_iterations → RigidOptions.iterations
"ls_iterations": 20,
"tolerance": 1e-6, # solver_tolerance → RigidOptions.tolerance
"constraint_timeconst": 0.02,
"integrator": "implicitfast", # stays a string; the scene resolves to gs.integrator
}


def test_rigid_options_kwargs_skips_none_only() -> None:
# enable_self_collision=False must survive (it's a real value, not "unset").
cfg = SimulationCfg(enable_self_collision=False, solver_iterations=50)
assert cfg.rigid_options_kwargs() == {"enable_self_collision": False, "iterations": 50}