From 10306f99cf93750b2cbbf22d3d3ef390ab093c3e Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Wed, 26 Aug 2026 17:21:16 +1000 Subject: [PATCH 1/2] fix(dhrobot): widen ikine_LM to forward all ETS.ikine_LM options DHRobot.ikine_LM had its own narrow signature that silently dropped method/k/kq/km, so DH robots couldn't use the wampler/sugihara variants or joint-limit/manipulability weighting that ETS.ikine_LM supports. Switch to Tep/q0 explicit + **ikargs forwarding, documented via a new shared |ikargs| substitution (matching the existing |BlockOptions| pattern) so the description stays in one place as more ikine_LM wrappers pick it up. Co-Authored-By: Claude Sonnet 5 --- docs/source/conf.py | 1 + src/roboticstoolbox/robot/DHRobot.py | 33 ++++++++++++++-------------- tests/test_DHRobot.py | 16 +++++++++----- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 42e18296c..d06799bd0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -198,6 +198,7 @@ :format: html .. |BlockOptions| replace:: :raw-html:`common Block options` .. |GraphicsBlockOptions| replace:: :raw-html:`common GraphicsBlock options` +.. |ikargs| replace:: additional keyword arguments accepted by the underlying numerical IK solver -- ``ilimit``, ``slimit``, ``tol``, ``mask``, ``joint_limits``, ``seed``, ``k``, ``method``, ``kq``, ``km`` -- see :meth:`~roboticstoolbox.ETS.ikine_LM` for details of each """ # -------- Suppress common noisy warnings ----------------------------------------# diff --git a/src/roboticstoolbox/robot/DHRobot.py b/src/roboticstoolbox/robot/DHRobot.py index 5a04c8490..d1c0133fb 100644 --- a/src/roboticstoolbox/robot/DHRobot.py +++ b/src/roboticstoolbox/robot/DHRobot.py @@ -2526,23 +2526,24 @@ def ikine_LM( self, Tep: np.ndarray | SE3, q0: ArrayLike | None = None, - ilimit: int = 30, - slimit: int = 100, - tol: float = 1e-6, - joint_limits: bool = False, - mask: ArrayLike | None = None, - seed: int | None = None, + **ikargs, ): - return self.ets().ikine_LM( - Tep=Tep, - q0=q0, - ilimit=ilimit, - slimit=slimit, - tol=tol, - joint_limits=joint_limits, - mask=mask, - seed=seed, - ) + """ + Numerical inverse kinematics by Levenberg-Marquadt optimization + + :param Tep: The desired end-effector pose + :param q0: The initial joint coordinate vector + :param ikargs: |ikargs| + :returns: An IKSolution containing joint coordinates ``q``, ``success`` flag, ``iterations``, ``searches``, ``residual`` error value, and ``reason`` string if applicable + :rtype: IKSolution + + This is a thin wrapper around :meth:`ETS.ikine_LM` -- see that + method for the full description of the Levenberg-Marquadt solver + and its ``method``/``kq``/``km`` variants. + + .. seealso:: :meth:`ikine_LM` (:class:`~roboticstoolbox.ETS`) + """ + return self.ets().ikine_LM(Tep=Tep, q0=q0, **ikargs) class SerialLink(DHRobot): diff --git a/tests/test_DHRobot.py b/tests/test_DHRobot.py index 6b2357fe3..ea058fc3d 100644 --- a/tests/test_DHRobot.py +++ b/tests/test_DHRobot.py @@ -1029,14 +1029,18 @@ def test_ikine_LM(self): self.assertTrue(sol.success) self.assertAlmostEqual(np.linalg.norm(T - puma.fkine(sol.q)), 0, places=4) - # def test_ikine_LMS(self): - # puma = rp.models.DH.Puma560() + def test_ikine_LM_ikargs_forwarding(self): + # DHRobot.ikine_LM forwards **ikargs straight through to + # ETS.ikine_LM -- confirm a non-default method/k actually reaches + # the underlying solver (regression: DHRobot.ikine_LM used to have + # its own narrow signature that silently dropped method/k/kq/km). + puma = rp.models.DH.Puma560() - # T = puma.fkine(puma.qn) + T = puma.fkine(puma.qn) - # sol = puma.ikine_LM(T) - # self.assertTrue(sol.success) - # self.assertAlmostEqual(np.linalg.norm(T - puma.fkine(sol.q)), 0, places=6) + sol = puma.ikine_LM(T, method="sugihara", k=0.0001, tol=1e-10, seed=0) + self.assertTrue(sol.success) + self.assertAlmostEqual(np.linalg.norm(T - puma.fkine(sol.q)), 0, places=4) # def test_ikine_unc(self): # puma = rp.models.DH.Puma560() From c1e4a7e58d614e33ecb4c1e530ba54d3b1374641 Mon Sep 17 00:00:00 2001 From: Peter Corke Date: Wed, 26 Aug 2026 17:21:42 +1000 Subject: [PATCH 2/2] fix(examples): replace nonexistent ikine_LMS/ikine_min calls Both examples/readme.py and examples/ikine_evaluate.py called robot.ikine_LMS(...), which hasn't existed since the LM variants were merged into ikine_LM(method=...). ikine_evaluate.py also called the never-existent robot.ikine_min(...), and its timeit benchmark strings passed q0 positionally, which lands in the end= parameter instead. Fixed all three, replacing ikine_LMS with ikine_LM(method="sugihara", k=0.0001) -- the default k=1.0 doesn't converge for this method -- and ikine_min with ikine_LM(joint_limits=False/True), which is what those benchmark rows were actually trying to compare. Co-Authored-By: Claude Sonnet 5 --- examples/ikine_evaluate.py | 28 +++++++++++----------------- examples/readme.py | 4 +++- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/examples/ikine_evaluate.py b/examples/ikine_evaluate.py index d4c4c2f80..8888a8051 100644 --- a/examples/ikine_evaluate.py +++ b/examples/ikine_evaluate.py @@ -31,31 +31,25 @@ # - the statement to execute for timeit ikfuncs = [ ( - robot.ikine_LM, # Levenberg-Marquadt + lambda T, q0: robot.ikine_LM(T, q0=q0), # Levenberg-Marquadt "ikine_LM", - "sol = robot.ikine_LM(T, q0)", + "sol = robot.ikine_LM(T, q0=q0)", ), ( - robot.ikine_LMS, # Levenberg-Marquadt (Sugihara) - "ikine_LMS", - "sol = robot.ikine_LMS(T, q0)", + lambda T, q0: robot.ikine_LM(T, q0=q0, method="sugihara", k=0.0001), + "ikine_LM (sugihara)", + "sol = robot.ikine_LM(T, q0=q0, method='sugihara', k=0.0001)", ), ( - robot.ikine_min, # numerical solution with no constraints - "ikine_min(qlim=False)", - "sol = robot.ikine_min(T, q0)", + lambda T, q0: robot.ikine_LM(T, q0=q0, joint_limits=False), + "ikine_LM(qlim=False)", + "sol = robot.ikine_LM(T, q0=q0, joint_limits=False)", ), ( - lambda T, q0: robot.ikine_min( - T, q0, qlim=True - ), # numerical solution with constraints - "ikine_min(qlim=True)", - "sol = robot.ikine_min(T, q0, qlim=True)", + lambda T, q0: robot.ikine_LM(T, q0=q0, joint_limits=True), + "ikine_LM(qlim=True)", + "sol = robot.ikine_LM(T, q0=q0, joint_limits=True)", ), - # (robot.ikine_mmc, #numerical solution with no constraints - # "ikine_min(qlim=False)", - # "sol = robot.ikine_min(T, q0)" - # ), ] if hasattr(robot, "ikine_a"): a = ( diff --git a/examples/readme.py b/examples/readme.py index ddfc1f790..86563ff1c 100644 --- a/examples/readme.py +++ b/examples/readme.py @@ -11,7 +11,9 @@ # IK T = SE3(0.7, 0.2, 0.1) * SE3.OA([0, 1, 0], [0, 0, -1]) -sol = robot.ikine_LMS(T) # solve IK, ignore additional outputs +sol = robot.ikine_LM( + T, method="sugihara", k=0.0001 +) # solve IK, ignore additional outputs print(sol.q) # display joint angles # FK shows that desired end-effector pose was achieved print(robot.fkine(sol.q))