From 27e2d13d564f6ee1db9202e3f8f63f1410120f84 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:27:11 +0100 Subject: [PATCH 1/2] Refresh articulation controller target and zero index selection --- .../nodes/OgnIsaacArticulationController.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationController.py b/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationController.py index 1a4a748eb5..aa34e22315 100644 --- a/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationController.py +++ b/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationController.py @@ -224,18 +224,18 @@ def compute(db: Any) -> bool: """ state = db.per_instance_state try: - if not state.initialized: - if len(db.inputs.robotPath) != 0: - state.prim_path = db.inputs.robotPath - else: - if len(db.inputs.targetPrim) == 0: - db.log_error("No robot prim found for the articulation controller") - return False - else: - state.prim_path = db.inputs.targetPrim[0].GetString() - - # initialize the controller handle for the robot + if len(db.inputs.robotPath) != 0: + prim_path = db.inputs.robotPath + else: + if len(db.inputs.targetPrim) == 0: + db.log_error("No robot prim found for the articulation controller") + return False + prim_path = db.inputs.targetPrim[0].GetString() + + if not state.initialized or state.prim_path != prim_path: + state.prim_path = prim_path state.initialize_controller() + state.joint_picked = False # pick the joints that are being commanded, this can be different at every step joint_names = db.inputs.jointNames @@ -244,7 +244,7 @@ def compute(db: Any) -> bool: state.joint_picked = False joint_indices = db.inputs.jointIndices - if np.asarray(joint_indices).any() and not np.array_equal(joint_indices, state.joint_indices): + if np.size(joint_indices) > 0 and not np.array_equal(joint_indices, state.joint_indices): state.joint_indices = np.array(joint_indices) state.joint_picked = False From d8364be0eb9db5612dade4030015b3029e48660d Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 16:28:16 +0100 Subject: [PATCH 2/2] Add articulation controller input refresh regressions --- ...t_articulation_controller_input_refresh.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 source/extensions/isaacsim.core.nodes/python/tests/test_articulation_controller_input_refresh.py diff --git a/source/extensions/isaacsim.core.nodes/python/tests/test_articulation_controller_input_refresh.py b/source/extensions/isaacsim.core.nodes/python/tests/test_articulation_controller_input_refresh.py new file mode 100644 index 0000000000..e599ebf3d6 --- /dev/null +++ b/source/extensions/isaacsim.core.nodes/python/tests/test_articulation_controller_input_refresh.py @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for articulation-controller robot and index input refresh.""" + +from types import MethodType, SimpleNamespace + +import numpy as np +import omni.kit.test +from isaacsim.core.nodes.ogn.python.nodes.OgnIsaacArticulationController import ( + OgnIsaacArticulationController, + OgnIsaacArticulationControllerInternalState, +) + + +class _FakeArticulation: + def __init__(self) -> None: + self.position_targets = [] + + def set_dof_position_targets(self, values, dof_indices=None) -> None: + self.position_targets.append((np.asarray(values), np.asarray(dof_indices))) + + def set_dof_velocity_targets(self, values, dof_indices=None) -> None: + pass + + def set_dof_efforts(self, values, dof_indices=None) -> None: + pass + + +class _FakeDb: + def __init__(self, state, *, robot_path: str, joint_indices=None, position_command=None) -> None: + self.per_instance_state = state + self.inputs = SimpleNamespace( + robotPath=robot_path, + targetPrim=[], + jointNames=[], + jointIndices=[] if joint_indices is None else joint_indices, + positionCommand=[] if position_command is None else position_command, + velocityCommand=[], + effortCommand=[], + ) + self.errors = [] + + def log_error(self, message) -> None: + self.errors.append(str(message)) + + +class TestArticulationControllerInputRefresh(omni.kit.test.AsyncTestCase): + """Verify controller target changes and explicit DOF zero are honored.""" + + @staticmethod + def _state() -> OgnIsaacArticulationControllerInternalState: + state = OgnIsaacArticulationControllerInternalState.__new__(OgnIsaacArticulationControllerInternalState) + state.initialized = True + state.prim_path = "/RobotA" + state.articulation = _FakeArticulation() + state.joint_names = None + state.joint_indices = None + state.joint_picked = False + state.command_error_message = None + state.node = None + return state + + async def test_explicit_zero_joint_index_remains_selected(self) -> None: + """jointIndices=[0] must not fall through to the all-DOFs path.""" + state = self._state() + state.prim_path = "/Robot" + db = _FakeDb(state, robot_path="/Robot", joint_indices=[0], position_command=[0.25]) + + self.assertTrue(OgnIsaacArticulationController.compute(db)) + self.assertEqual(db.errors, []) + self.assertEqual(len(state.articulation.position_targets), 1) + values, indices = state.articulation.position_targets[0] + self.assertTrue(np.array_equal(values, np.array([0.25]))) + self.assertTrue(np.array_equal(indices, np.array([0]))) + + async def test_robot_path_change_reinitializes_controller(self) -> None: + """Changing robotPath should rebuild the articulation handle for the new robot.""" + state = self._state() + state.joint_picked = True + initialized_paths = [] + + def initialize_controller(inner_state) -> None: + initialized_paths.append(inner_state.prim_path) + inner_state.articulation = _FakeArticulation() + inner_state.initialized = True + + state.initialize_controller = MethodType(initialize_controller, state) + db = _FakeDb(state, robot_path="/RobotB") + + self.assertTrue(OgnIsaacArticulationController.compute(db)) + self.assertEqual(db.errors, []) + self.assertEqual(initialized_paths, ["/RobotB"]) + self.assertEqual(state.prim_path, "/RobotB") + self.assertTrue(state.joint_picked)