diff --git a/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationState.py b/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationState.py index 0afdb851fc..4d2a9af118 100644 --- a/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationState.py +++ b/source/extensions/isaacsim.core.nodes/python/nodes/OgnIsaacArticulationState.py @@ -42,6 +42,11 @@ def __init__(self) -> None: def initialize_articulation(self) -> None: """Create the articulation handle for the selected robot prim and mark the state initialized.""" self._articulation = Articulation(self.robot_prim) + self.dof_names = None + self.dof_indices = None + self._dof_names = [] + self._dof_indices = None + self._link_indices = None self.initialized = True def pick_dofs(self, dof_names: Any, dof_indices: Any) -> None: @@ -129,17 +134,16 @@ def compute(db: Any) -> bool: """ state = db.per_instance_state try: - if not state.initialized: - if len(db.inputs.robotPath) != 0: - state.robot_prim = db.inputs.robotPath - else: - if not len(db.inputs.targetPrim): - db.log_error("No robot prim found for the articulation state") - return False - else: - state.robot_prim = db.inputs.targetPrim[0].GetString() - - # initialize the articulation handle for the robot + if len(db.inputs.robotPath) != 0: + robot_prim = db.inputs.robotPath + else: + if not len(db.inputs.targetPrim): + db.log_error("No robot prim found for the articulation state") + return False + robot_prim = db.inputs.targetPrim[0].GetString() + + if not state.initialized or state.robot_prim != robot_prim: + state.robot_prim = robot_prim state.initialize_articulation() # pick the articulation DOFs to be queried, they can be different at every step diff --git a/source/extensions/isaacsim.core.nodes/python/tests/test_articulation_state_retarget.py b/source/extensions/isaacsim.core.nodes/python/tests/test_articulation_state_retarget.py new file mode 100644 index 0000000000..36341c2a35 --- /dev/null +++ b/source/extensions/isaacsim.core.nodes/python/tests/test_articulation_state_retarget.py @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Regression coverage for IsaacArticulationState robot retargeting.""" + +from types import SimpleNamespace + +import numpy as np +import omni.kit.test +from isaacsim.core.nodes.ogn.python.nodes.OgnIsaacArticulationState import OgnIsaacArticulationState + + +class _FakeState: + def __init__(self) -> None: + self.initialized = True + self.robot_prim = "/RobotA" + self.dof_names = [] + self.dof_indices = np.array([], dtype=np.int64) + self.initialized_paths = [] + self.pick_calls = 0 + + def initialize_articulation(self) -> None: + self.initialized_paths.append(self.robot_prim) + self.dof_names = None + self.dof_indices = None + self.initialized = True + + def pick_dofs(self, dof_names, dof_indices) -> None: + self.pick_calls += 1 + self.dof_names = dof_names + self.dof_indices = dof_indices + + def get_dof_names(self): + return [] + + def get_articulation_state(self): + empty = np.array([], dtype=np.float32) + return empty, empty, empty, empty, empty + + +class TestArticulationStateRetarget(omni.kit.test.AsyncTestCase): + """Verify changing the robot input refreshes the cached articulation state.""" + + async def test_robot_path_change_reinitializes_articulation_and_selection(self) -> None: + """A new robotPath should rebuild the handle and re-resolve DOF selection.""" + state = _FakeState() + db = SimpleNamespace( + per_instance_state=state, + inputs=SimpleNamespace(robotPath="/RobotB", targetPrim=[], jointNames=[], jointIndices=[]), + outputs=SimpleNamespace(), + log_error=lambda _message: None, + log_warn=lambda _message: None, + ) + + self.assertTrue(OgnIsaacArticulationState.compute(db)) + self.assertEqual(state.robot_prim, "/RobotB") + self.assertEqual(state.initialized_paths, ["/RobotB"]) + self.assertEqual(state.pick_calls, 1)