Skip to content

Commit 1f42d03

Browse files
authored
teleop: route ssik through analytical IK path, not Jacobian fallback (#165)
_step_pose only recognized MuJoCoEAIKSolver as analytical. JACO 2 uses MuJoCoSSIKSolver (added in #159) which silently fell through to _step_pose_as_twist — the Jacobian-based path designed for mink. The Jacobian fallback can't track ft_guarded_move's running target fast enough; the result is a silent stall and a "ft_guarded_move:no_progress" outcome in ada_mj feed_bite's acquire_food step. Extract `_is_analytical_ik(ik)` helper that accepts both MuJoCoEAIKSolver and MuJoCoSSIKSolver. Both return IK solutions in O(1) and are safe to call per teleop tick; only mink genuinely needs the Jacobian fallback. Adds TestAnalyticalIKDispatch (4 tests) locking in the classification for eaik, ssik, mink, and None. Fixes #164
1 parent c6d1754 commit 1f42d03

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

src/mj_manipulator/teleop.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ class TeleopFrame:
106106
gripper_position: float
107107

108108

109+
def _is_analytical_ik(ik) -> bool:
110+
"""True iff ``ik`` returns IK solutions in O(1) and is safe to call
111+
every teleop tick.
112+
113+
The pose dispatch in :meth:`TeleopController._step_pose` uses this
114+
to choose between the fast analytical path (one ``ik.solve`` call
115+
per tick) and the Jacobian-based ``_step_pose_as_twist`` fallback.
116+
Numerical solvers (mink) must go through the fallback — calling
117+
them per tick is both too slow and returns solutions in wrappings
118+
incompatible with teleop tracking.
119+
120+
Lazy-imports the concrete solver classes so unrelated callers don't
121+
pay for eaik/ssik import unless teleop is exercised.
122+
"""
123+
from mj_manipulator.arms.eaik_solver import MuJoCoEAIKSolver
124+
from mj_manipulator.arms.ssik_solver import MuJoCoSSIKSolver
125+
126+
return isinstance(ik, (MuJoCoEAIKSolver, MuJoCoSSIKSolver))
127+
128+
109129
class TeleopController:
110130
"""Unified teleop controller with pose and twist inputs.
111131
@@ -582,15 +602,13 @@ def _get_collision_checker(self):
582602
def _step_pose(self, pose: np.ndarray) -> TeleopState:
583603
"""Pose tracking via IK (analytical) or resolved-rate (numerical).
584604
585-
Analytical IK (EAIK): use IK solutions directly — fast, exact.
605+
Analytical IK (EAIK or ssik): use IK solutions directly — fast, exact.
586606
Numerical IK (mink): convert pose error to twist and use
587607
CartesianController (Jacobian-based). Numerical IK is too slow
588608
for teleop and returns solutions in wrong wrappings.
589609
"""
590-
from mj_manipulator.arms.eaik_solver import MuJoCoEAIKSolver
591-
592610
ik = self._arm.ik_solver
593-
if ik is not None and isinstance(ik, MuJoCoEAIKSolver):
611+
if ik is not None and _is_analytical_ik(ik):
594612
q_current = self._arm.get_joint_positions()
595613
solutions = ik.solve(pose, q_init=q_current)
596614
if not solutions:

tests/test_teleop.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,38 @@ def test_collision_tracking_is_recorded(self):
427427
frames = ctrl.stop_recording()
428428

429429
assert len(frames) == 1 # TRACKING_COLLISION frames are recorded
430+
431+
432+
class TestAnalyticalIKDispatch:
433+
"""Regression: _step_pose must route ssik through the analytical path.
434+
435+
Pre-fix, only MuJoCoEAIKSolver was recognized as analytical. JACO 2
436+
(which uses MuJoCoSSIKSolver) silently fell through to the Jacobian
437+
fallback, which can't keep up with ft_guarded_move and reports
438+
"ft_guarded_move:no_progress" — see ada_mj feed_bite acquire_food.
439+
"""
440+
441+
def test_eaik_is_analytical(self):
442+
from mj_manipulator.arms.eaik_solver import MuJoCoEAIKSolver
443+
from mj_manipulator.teleop import _is_analytical_ik
444+
445+
assert _is_analytical_ik(MuJoCoEAIKSolver.__new__(MuJoCoEAIKSolver))
446+
447+
def test_ssik_is_analytical(self):
448+
pytest.importorskip("ssik")
449+
from mj_manipulator.arms.ssik_solver import MuJoCoSSIKSolver
450+
from mj_manipulator.teleop import _is_analytical_ik
451+
452+
assert _is_analytical_ik(MuJoCoSSIKSolver.__new__(MuJoCoSSIKSolver))
453+
454+
def test_mink_is_not_analytical(self):
455+
pytest.importorskip("mink")
456+
from mj_manipulator.arms.mink_solver import MinkIKSolver
457+
from mj_manipulator.teleop import _is_analytical_ik
458+
459+
assert not _is_analytical_ik(MinkIKSolver.__new__(MinkIKSolver))
460+
461+
def test_none_is_not_analytical(self):
462+
from mj_manipulator.teleop import _is_analytical_ik
463+
464+
assert not _is_analytical_ik(None)

0 commit comments

Comments
 (0)