Skip to content

Commit 9e230b0

Browse files
committed
Add Kamino closed-loop articulation support to Newton backend
1 parent 517d2e2 commit 9e230b0

9 files changed

Lines changed: 716 additions & 36 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Added
2+
^^^^^
3+
4+
* Added :class:`~isaaclab_newton.assets.articulation.closed_loop_view.ClosedLoopView`
5+
to simulate closed kinematic chains (parallel linkages) in maximal coordinates with
6+
the Kamino solver.
7+
* Added ``max_contacts_per_world`` to :class:`~isaaclab_newton.physics.KaminoSolverCfg`
8+
to bound per-world contact allocation for the Kamino solver.
9+
10+
Fixed
11+
^^^^^
12+
13+
* Fixed contact-sensor forces for the Kamino solver by routing contact aggregation
14+
through a unified, solver-agnostic path shared with the MuJoCo-Warp backend.

source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation.py

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ def write_data_to_sim(self):
311311
# Standard Lab actuator path
312312
self._apply_actuator_model()
313313
self.data._sim_bind_joint_effort.assign(self._joint_effort_target_sim)
314+
self.data._sim_bind_joint_act.assign(self._joint_act_target_sim)
314315
if self._has_implicit_actuators:
315316
self.data._sim_bind_joint_position_target.assign(self._joint_pos_target_sim)
316317
self.data._sim_bind_joint_velocity_target.assign(self._joint_vel_target_sim)
@@ -510,7 +511,7 @@ def write_root_link_pose_to_sim_index(
510511
if self.data._root_state_w is not None:
511512
self.data._root_state_w.timestamp = -1.0
512513
self.data._fk_timestamp = -1.0 # Forces a kinematic update to get the latest body link poses.
513-
SimulationManager.invalidate_fk(env_ids=env_ids, articulation_ids=self._root_view.articulation_ids)
514+
SimulationManager.invalidate_fk(env_ids=env_ids, articulation_ids=self._get_root_view_articulation_ids())
514515
if self.data._body_com_pose_w is not None:
515516
self.data._body_com_pose_w.timestamp = -1.0
516517
if self.data._body_state_w is not None:
@@ -567,7 +568,7 @@ def write_root_link_pose_to_sim_mask(
567568
if self.data._root_state_w is not None:
568569
self.data._root_state_w.timestamp = -1.0
569570
self.data._fk_timestamp = -1.0 # Forces a kinematic update to get the latest body link poses.
570-
SimulationManager.invalidate_fk(env_mask=env_mask, articulation_ids=self._root_view.articulation_ids)
571+
SimulationManager.invalidate_fk(env_mask=env_mask, articulation_ids=self._get_root_view_articulation_ids())
571572
if self.data._body_com_pose_w is not None:
572573
self.data._body_com_pose_w.timestamp = -1.0
573574
if self.data._body_state_w is not None:
@@ -632,7 +633,7 @@ def write_root_com_pose_to_sim_index(
632633
if self.data._root_state_w is not None:
633634
self.data._root_state_w.timestamp = -1.0
634635
self.data._fk_timestamp = -1.0 # Forces a kinematic update to get the latest body link poses.
635-
SimulationManager.invalidate_fk(env_ids=env_ids, articulation_ids=self._root_view.articulation_ids)
636+
SimulationManager.invalidate_fk(env_ids=env_ids, articulation_ids=self._get_root_view_articulation_ids())
636637
if self.data._body_com_pose_w is not None:
637638
self.data._body_com_pose_w.timestamp = -1.0
638639
if self.data._body_state_w is not None:
@@ -693,7 +694,7 @@ def write_root_com_pose_to_sim_mask(
693694
if self.data._root_state_w is not None:
694695
self.data._root_state_w.timestamp = -1.0
695696
self.data._fk_timestamp = -1.0 # Forces a kinematic update to get the latest body link poses.
696-
SimulationManager.invalidate_fk(env_mask=env_mask, articulation_ids=self._root_view.articulation_ids)
697+
SimulationManager.invalidate_fk(env_mask=env_mask, articulation_ids=self._get_root_view_articulation_ids())
697698
if self.data._body_com_pose_w is not None:
698699
self.data._body_com_pose_w.timestamp = -1.0
699700
if self.data._body_state_w is not None:
@@ -1124,7 +1125,7 @@ def write_joint_position_to_sim_index(
11241125
)
11251126
# Invalidate FK timestamp so body poses are recomputed on next access.
11261127
self.data._fk_timestamp = -1.0
1127-
SimulationManager.invalidate_fk(env_ids=env_ids, articulation_ids=self._root_view.articulation_ids)
1128+
SimulationManager.invalidate_fk(env_ids=env_ids, articulation_ids=self._get_root_view_articulation_ids())
11281129
# Need to invalidate the buffer to trigger the update with the new root pose.
11291130
# Only invalidate if the buffer has been accessed (not None).
11301131
if self.data._body_link_vel_w is not None:
@@ -1182,7 +1183,7 @@ def write_joint_position_to_sim_mask(
11821183
)
11831184
# Invalidate FK timestamp so body poses are recomputed on next access.
11841185
self.data._fk_timestamp = -1.0
1185-
SimulationManager.invalidate_fk(env_mask=env_mask, articulation_ids=self._root_view.articulation_ids)
1186+
SimulationManager.invalidate_fk(env_mask=env_mask, articulation_ids=self._get_root_view_articulation_ids())
11861187
# Need to invalidate the buffer to trigger the update with the new root pose.
11871188
# Only invalidate if the buffer has been accessed (not None).
11881189
if self.data._body_link_vel_w is not None:
@@ -3501,8 +3502,59 @@ def has_articulation_root_api(prim) -> bool:
35013502

35023503
asset_prim, root_expr = resolve_matching_prims_from_source(self.cfg.prim_path)[0]
35033504
walk_root = asset_prim.GetPath().pathString
3504-
root_prims = get_all_matching_child_prims(walk_root, has_articulation_root_api, expected_num_matches=1)
3505-
root_prim_path_expr = root_expr + root_prims[0].GetPath().pathString[len(walk_root) :]
3505+
3506+
# Find all articulation root prims in the first environment.
3507+
first_env_root_prims = get_all_matching_child_prims(
3508+
walk_root,
3509+
predicate=has_articulation_root_api,
3510+
traverse_instance_prims=False,
3511+
)
3512+
from isaaclab_newton.cloner.newton_replicate import _prim_has_closed_kinematic_loops
3513+
3514+
has_closed_loops = _prim_has_closed_kinematic_loops(asset_prim)
3515+
if len(first_env_root_prims) == 0 or has_closed_loops:
3516+
# Closed-loop robots (e.g. DR Legs) have cyclic joint graphs that cannot
3517+
# form a tree-structured Newton ``ArticulationView``. Use ``ClosedLoopView``
3518+
# which provides strided views into the global Newton model arrays instead.
3519+
from .closed_loop_view import ClosedLoopView
3520+
3521+
if has_closed_loops and len(first_env_root_prims) > 0:
3522+
logger.info(
3523+
"Closed kinematic loops detected under '%s'; using ClosedLoopView despite"
3524+
" ArticulationRootAPI presence.",
3525+
walk_root,
3526+
)
3527+
else:
3528+
logger.info(
3529+
"No ArticulationRootAPI found under '%s'. Using ClosedLoopView for closed-loop robot support.",
3530+
walk_root,
3531+
)
3532+
self._root_view = ClosedLoopView(
3533+
SimulationManager.get_model(),
3534+
self.cfg.prim_path.replace(".*", "*"),
3535+
)
3536+
SimulationManager.get_physics_sim_view().append(self._root_view)
3537+
self._data = ArticulationData(self.root_view, self.device)
3538+
self._physics_ready_handle = SimulationManager.register_callback(
3539+
lambda _: self._data._create_simulation_bindings(),
3540+
PhysicsEvent.PHYSICS_READY,
3541+
name=f"articulation_rebind_{self.cfg.prim_path}",
3542+
)
3543+
self._create_buffers()
3544+
self._process_cfg()
3545+
self._process_actuators_cfg()
3546+
self._process_tendons()
3547+
# Let the articulation data know that it is fully instantiated and ready to use.
3548+
self.data.is_primed = True
3549+
return
3550+
if len(first_env_root_prims) > 1:
3551+
raise RuntimeError(
3552+
f"Failed to find a single articulation when resolving '{walk_root}'."
3553+
f" Found multiple '{first_env_root_prims}' under '{walk_root}'."
3554+
" Please ensure that there is only one articulation in the prim path tree."
3555+
)
3556+
3557+
root_prim_path_expr = root_expr + first_env_root_prims[0].GetPath().pathString[len(walk_root) :]
35063558
# -- articulation
35073559
self._root_view = ArticulationView(
35083560
SimulationManager.get_model(),
@@ -3538,6 +3590,17 @@ def has_articulation_root_api(prim) -> bool:
35383590
# Let the articulation data know that it is fully instantiated and ready to use.
35393591
self.data.is_primed = True
35403592

3593+
def _get_root_view_articulation_ids(self) -> wp.array | None:
3594+
"""Return the root view's ``articulation_ids``, or ``None`` for closed-loop assets.
3595+
3596+
:class:`ClosedLoopView` exposes an empty ``(world_count, 0)`` array, so returning
3597+
``None`` makes the reset machinery scope by ``env_ids`` / ``env_mask`` instead.
3598+
"""
3599+
art_ids = getattr(self._root_view, "articulation_ids", None)
3600+
if art_ids is None or art_ids.ndim < 2 or art_ids.shape[1] == 0:
3601+
return None
3602+
return art_ids
3603+
35413604
def _clear_callbacks(self) -> None:
35423605
"""Clears all registered callbacks, including the physics-ready rebind handle."""
35433606
super()._clear_callbacks()
@@ -3572,6 +3635,7 @@ def _create_buffers(self):
35723635
self._joint_pos_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device)
35733636
self._joint_vel_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device)
35743637
self._joint_effort_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device)
3638+
self._joint_act_target_sim = wp.zeros_like(self.data.joint_pos_target.warp, device=self.device)
35753639

35763640
# soft joint position limits (recommended not to be too close to limits).
35773641
wp.launch(
@@ -3975,6 +4039,9 @@ def _apply_actuator_model(self):
39754039
gear_ratio = actuator.gear_ratio
39764040
else:
39774041
gear_ratio = None
4042+
target_torque_buf = (
4043+
self._joint_act_target_sim if actuator.route_torque_to == "joint_act" else self._joint_effort_target_sim
4044+
)
39784045
wp.launch(
39794046
articulation_kernels.update_targets,
39804047
dim=(self.num_instances, joint_indices.shape[0]),
@@ -3987,7 +4054,7 @@ def _apply_actuator_model(self):
39874054
outputs=[
39884055
self._joint_pos_target_sim,
39894056
self._joint_vel_target_sim,
3990-
self._joint_effort_target_sim,
4057+
target_torque_buf,
39914058
],
39924059
device=self.device,
39934060
)

source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,20 @@ def applied_torque(self) -> ProxyArray:
315315
"""
316316
return self._applied_torque_ta
317317

318+
@property
319+
def joint_act_target(self) -> ProxyArray:
320+
"""Per-DOF feedforward actuation written to Newton's ``Control.joint_act``.
321+
322+
Shape is (num_instances, num_joints), dtype = wp.float32. In torch this resolves to
323+
(num_instances, num_joints).
324+
325+
Populated each ``write_data_to_sim`` from the per-actuator computed torques whose
326+
:attr:`~isaaclab.actuators.ActuatorBase.route_torque_to` is set to ``"joint_act"``.
327+
For the Kamino solver this becomes ``pd_tau_j_ff`` and feeds the implicit joint
328+
dynamic-constraint row.
329+
"""
330+
return self._joint_act_target_ta
331+
318332
"""
319333
Joint properties
320334
"""
@@ -1806,6 +1820,7 @@ def _pin_proxy_arrays(self) -> None:
18061820
self._joint_pos_limits_upper_ta = ProxyArray(self._sim_bind_joint_pos_limits_upper)
18071821
self._joint_vel_limits_ta = ProxyArray(self._sim_bind_joint_vel_limits_sim)
18081822
self._joint_effort_limits_ta = ProxyArray(self._sim_bind_joint_effort_limits_sim)
1823+
self._joint_act_target_ta = ProxyArray(self._sim_bind_joint_act)
18091824
self._body_mass_ta = ProxyArray(self._sim_bind_body_mass)
18101825
self._body_inertia_ta = ProxyArray(self._sim_bind_body_inertia)
18111826
self._body_com_pos_b_ta = ProxyArray(self._sim_bind_body_com_pos_b)
@@ -1837,6 +1852,7 @@ def _pin_proxy_arrays(self) -> None:
18371852
self._joint_pos_limits_upper_ta = ProxyArray(self._sim_bind_joint_pos_limits_upper)
18381853
self._joint_vel_limits_ta = ProxyArray(self._sim_bind_joint_vel_limits_sim)
18391854
self._joint_effort_limits_ta = ProxyArray(self._sim_bind_joint_effort_limits_sim)
1855+
self._joint_act_target_ta = ProxyArray(self._sim_bind_joint_act)
18401856
self._soft_joint_pos_limits_ta = ProxyArray(self._soft_joint_pos_limits)
18411857
self._soft_joint_vel_limits_ta = ProxyArray(self._soft_joint_vel_limits)
18421858
self._gear_ratio_ta = ProxyArray(self._gear_ratio)

0 commit comments

Comments
 (0)