@@ -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 )
0 commit comments