roboticstoolbox-python 1.4.2__py3-none-any.whl → 1.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -202,7 +202,7 @@ def make_banner():
202
202
  from spatialmath import *
203
203
  from spatialmath.base import *
204
204
  from spatialmath.base import sym
205
- from roboticstoolbox import *"
205
+ from roboticstoolbox import *
206
206
 
207
207
  # useful variables
208
208
  from math import pi
@@ -51,8 +51,52 @@ static void _IK_loop(
51
51
 
52
52
  if (*E < tol)
53
53
  {
54
- for (int i = 0; i < ets->n; i++)
55
- q(i) = std::fmod(q(i) + PI, PI_x2) - PI;
54
+ // Preserve translations and coordinates already within their limits.
55
+ // Other revolute coordinates may move only by whole turns, so the
56
+ // converged end-effector pose is unchanged (see IKSolver._normalise_q).
57
+ int j = 0;
58
+ for (int i = 0; i < ets->m; i++)
59
+ {
60
+ ET *et = ets->ets[i];
61
+ if (!et->isjoint)
62
+ continue;
63
+
64
+ double lower = ets->qlim_l[j];
65
+ double upper = ets->qlim_h[j];
66
+ if (et->axis < 3 && !(lower <= q(j) && q(j) <= upper))
67
+ {
68
+ // Avoid rounding a principal angle across a nearby limit.
69
+ double angle = q(j);
70
+ if (!(angle >= -PI && angle < PI))
71
+ {
72
+ // Unlike Python's %, fmod can return a negative remainder.
73
+ angle = std::fmod(angle + PI, PI_x2);
74
+ if (angle < 0)
75
+ angle += PI_x2;
76
+ angle -= PI;
77
+ }
78
+ if (angle < lower)
79
+ angle += PI_x2 * std::ceil((lower - angle) / PI_x2);
80
+ else if (angle > upper)
81
+ angle -= PI_x2 * std::ceil((angle - upper) / PI_x2);
82
+ if (!(lower <= angle && angle <= upper))
83
+ {
84
+ // Recover rounded endpoints only if whole turns
85
+ // reconstruct the original coordinate exactly.
86
+ for (double bound : {lower, upper})
87
+ {
88
+ double turns = std::round((q(j) - bound) / PI_x2);
89
+ if (turns != 0 && bound + turns * PI_x2 == q(j))
90
+ {
91
+ angle = bound;
92
+ break;
93
+ }
94
+ }
95
+ }
96
+ q(j) = angle;
97
+ }
98
+ j++;
99
+ }
56
100
  *solution = reject_jl ? _check_lim(ets, q) : 1;
57
101
  break;
58
102
  }
@@ -315,4 +359,4 @@ extern "C"
315
359
  // return q;
316
360
  }
317
361
 
318
- } /* extern "C" */
362
+ } /* extern "C" */
@@ -27,6 +27,11 @@ class UR10(DHRobot):
27
27
 
28
28
  .. note::
29
29
  - SI units are used.
30
+ - The last link's inertia tensor has :math:`I_{xx} = 0` -- that's not
31
+ a bug in this model, it's what the manufacturer's own reference
32
+ (below) publishes. It makes the robot's inertia matrix near-singular
33
+ (``det(robot.inertia(q)) ~ 2e-6``), so dynamics work that needs a
34
+ well-conditioned inertia matrix should account for this.
30
35
 
31
36
  :References:
32
37
 
@@ -47,6 +47,9 @@ class Fetch(URDFRobot):
47
47
  from a URDF file. The model describes its kinematic and graphical
48
48
  characteristics.
49
49
 
50
+ The model is loaded via the `robot_descriptions
51
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
52
+
50
53
  .. runblock:: pycon
51
54
 
52
55
  >>> import roboticstoolbox as rtb
@@ -13,6 +13,9 @@ class Frankie(URDFRobot):
13
13
  from a URDF file. The model describes its kinematic and graphical
14
14
  characteristics.
15
15
 
16
+ The model is loaded via the `robot_descriptions
17
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
18
+
16
19
  .. runblock:: pycon
17
20
 
18
21
  >>> import roboticstoolbox as rtb
@@ -23,8 +26,14 @@ class Frankie(URDFRobot):
23
26
 
24
27
  - qz, zero joint angle configuration, 'L' shaped configuration
25
28
  - qr, vertical 'READY' configuration
26
- - qs, arm is stretched out in the x-direction
27
- - qn, arm is at a nominal non-singular configuration
29
+
30
+ Loaded via the `robot_descriptions
31
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package
32
+ (real inertial data, plain-mesh collision geometry), unlike
33
+ :class:`~roboticstoolbox.models.URDF.Panda.Panda`'s bundled default. See
34
+ the wiki's `Panda models
35
+ <https://github.com/petercorke/robotics-toolbox-python/wiki/Panda-models>`_
36
+ page for why the two differ.
28
37
 
29
38
  .. codeauthor:: Jesse Haviland
30
39
  .. sectionauthor:: Peter Corke
@@ -27,8 +27,16 @@ class FrankieOmni(Robot):
27
27
 
28
28
  - qz, zero joint angle configuration, 'L' shaped configuration
29
29
  - qr, vertical 'READY' configuration
30
- - qs, arm is stretched out in the x-direction
31
- - qn, arm is at a nominal non-singular configuration
30
+
31
+ The arm is loaded from the same bundled ``qut_frankie_description`` xacro
32
+ as :class:`~roboticstoolbox.models.URDF.Panda.Panda`'s default (no
33
+ inertial data), spliced by hand onto a separately-loaded
34
+ ``clearpath_ridgeback_description`` mobile base -- it has no
35
+ ``use_robot_descriptions``-style option, since the base+arm splice is
36
+ done manually rather than through :class:`URDFRobot`'s own loader. See
37
+ the wiki's `Panda models
38
+ <https://github.com/petercorke/robotics-toolbox-python/wiki/Panda-models>`_
39
+ page for the full picture.
32
40
 
33
41
  .. codeauthor:: Jesse Haviland
34
42
  .. sectionauthor:: Peter Corke
@@ -12,6 +12,9 @@ class Jaco(URDFRobot):
12
12
  from a URDF file. The model describes its kinematic and graphical
13
13
  characteristics.
14
14
 
15
+ The model is loaded via the `robot_descriptions
16
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
17
+
15
18
  .. runblock:: pycon
16
19
 
17
20
  >>> import roboticstoolbox as rtb
@@ -12,6 +12,9 @@ class PR2(URDFRobot):
12
12
  from a URDF file. The model describes its kinematic and graphical
13
13
  characteristics.
14
14
 
15
+ The model is loaded via the `robot_descriptions
16
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
17
+
15
18
  .. runblock:: pycon
16
19
 
17
20
  >>> import roboticstoolbox as rtb
@@ -23,20 +23,40 @@ class Panda(URDFRobot):
23
23
 
24
24
  - qz, zero joint angle configuration, 'L' shaped configuration
25
25
  - qr, vertical 'READY' configuration
26
- - qs, arm is stretched out in the x-direction
27
- - qn, arm is at a nominal non-singular configuration
26
+
27
+ :param use_robot_descriptions: if ``True``, load the Panda URDF from the
28
+ `robot_descriptions <https://github.com/robot-descriptions/robot_descriptions.py>`_
29
+ package instead of the toolbox's own bundled ``qut_frankie_description``
30
+ xacro (the default, ``False``). The bundled model has real collision
31
+ geometry (a hand-built capsule approximation) but no inertial
32
+ (mass/CoM/inertia) data at all -- ``rne()``/``inertia()``/``coriolis()``/
33
+ ``gravload()`` are all silently zero. The ``robot_descriptions`` model
34
+ has real inertial data, but its collision geometry is plain meshes,
35
+ which are roughly an order of magnitude slower to collision-check
36
+ against than the bundled model's capsules -- noticeable in a
37
+ real-time reactive-avoidance loop (see ``examples/neo.py``). See the
38
+ wiki's `Panda models <https://github.com/petercorke/robotics-toolbox-python/wiki/Panda-models>`_
39
+ page for the full comparison and rationale.
40
+ :type use_robot_descriptions: bool
28
41
 
29
42
  .. codeauthor:: Jesse Haviland
30
43
  .. sectionauthor:: Peter Corke
31
44
  """
32
45
 
33
- def __init__(self):
34
-
35
- super().__init__(
36
- "qut_frankie_description/robots/panda_arm_hand.urdf.xacro",
37
- manufacturer="Franka Emika",
38
- gripper_link_index=9,
39
- )
46
+ def __init__(self, use_robot_descriptions: bool = False):
47
+
48
+ if use_robot_descriptions:
49
+ super().__init__(
50
+ "panda",
51
+ manufacturer="Franka Emika",
52
+ gripper_link_index=9,
53
+ )
54
+ else:
55
+ super().__init__(
56
+ "qut_frankie_description/robots/panda_arm_hand.urdf.xacro",
57
+ manufacturer="Franka Emika",
58
+ gripper_link_index=9,
59
+ )
40
60
 
41
61
  self.grippers[0].tool = SE3(0, 0, 0.1034)
42
62
 
@@ -12,6 +12,9 @@ class UR10(URDFRobot):
12
12
  definition from a URDF file. The model describes its kinematic and
13
13
  graphical characteristics.
14
14
 
15
+ The model is loaded via the `robot_descriptions
16
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
17
+
15
18
  .. runblock:: pycon
16
19
 
17
20
  >>> import roboticstoolbox as rtb
@@ -12,6 +12,9 @@ class UR3(URDFRobot):
12
12
  definition from a URDF file. The model describes its kinematic and
13
13
  graphical characteristics.
14
14
 
15
+ The model is loaded via the `robot_descriptions
16
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
17
+
15
18
  .. runblock:: pycon
16
19
 
17
20
  >>> import roboticstoolbox as rtb
@@ -13,6 +13,9 @@ class UR5(URDFRobot):
13
13
  definition from a URDF file. The model describes its kinematic and
14
14
  graphical characteristics.
15
15
 
16
+ The model is loaded via the `robot_descriptions
17
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
18
+
16
19
  .. runblock:: pycon
17
20
 
18
21
  >>> import roboticstoolbox as rtb
@@ -30,7 +33,16 @@ class UR5(URDFRobot):
30
33
 
31
34
  def __init__(self):
32
35
 
33
- super().__init__("ur5", manufacturer="Universal Robotics", gripper_link_index=7)
36
+ # Name-based lookup, not a raw positional index: robot_descriptions
37
+ # 3.0.0 changed which upstream repo "ur5" resolves to, reordering
38
+ # links so index 7 silently pointed at a real arm joint instead of
39
+ # the tool attachment link (#578). tool0/ee_link/flange are stable
40
+ # names across the versions checked.
41
+ super().__init__(
42
+ "ur5",
43
+ manufacturer="Universal Robotics",
44
+ gripper_link_name=["tool0", "ee_link", "flange"],
45
+ )
34
46
 
35
47
  # for link in links:
36
48
  # print(link)
@@ -383,10 +383,16 @@ class URDFRobot(Robot):
383
383
  super().__init__(
384
384
  "ur5",
385
385
  manufacturer="Universal Robotics",
386
- gripper_link_index=7,
386
+ gripper_link_name=["tool0", "ee_link"],
387
387
  )
388
388
  self.qz = np.zeros(6)
389
389
  ...
390
+
391
+ ``gripper_link_name`` (a name, or a list of candidate names tried in
392
+ order) survives an upstream URDF reordering that would silently break a
393
+ raw positional ``gripper_link_index`` -- see #578. Prefer it for new
394
+ models; ``gripper_link_index`` remains for models where a stable,
395
+ well-known link name isn't available.
390
396
  """
391
397
 
392
398
  def __init__(
@@ -394,6 +400,7 @@ class URDFRobot(Robot):
394
400
  urdf_path: "str | Path",
395
401
  manufacturer: str = "",
396
402
  gripper_link_index: "int | None" = None,
403
+ gripper_link_name: "str | list[str] | None" = None,
397
404
  patch: "Callable[[str], str] | None" = None,
398
405
  extra_packages: "dict[str, str] | None" = None,
399
406
  **kwargs,
@@ -401,7 +408,26 @@ class URDFRobot(Robot):
401
408
  elinks, name, filepath = URDF_file(
402
409
  urdf_path, patch=patch, extra_packages=extra_packages
403
410
  )
404
- if gripper_link_index is not None:
411
+ if gripper_link_name is not None:
412
+ candidates = (
413
+ [gripper_link_name]
414
+ if isinstance(gripper_link_name, str)
415
+ else gripper_link_name
416
+ )
417
+ for candidate in candidates:
418
+ match = next(
419
+ (link for link in elinks if link.name == candidate), None
420
+ )
421
+ if match is not None:
422
+ kwargs["gripper_links"] = match
423
+ break
424
+ else:
425
+ raise ValueError(
426
+ f"none of the candidate gripper link names {candidates!r} "
427
+ f"were found in the parsed URDF for {name!r} (have: "
428
+ f"{[link.name for link in elinks]})"
429
+ )
430
+ elif gripper_link_index is not None:
405
431
  kwargs["gripper_links"] = elinks[gripper_link_index]
406
432
  super().__init__(elinks, name=name, manufacturer=manufacturer, **kwargs)
407
433
  self._urdf_filepath = str(filepath) if filepath is not None else ""
@@ -41,6 +41,9 @@ class Valkyrie(Robot):
41
41
  from a URDF file. The model describes its kinematic and graphical
42
42
  characteristics.
43
43
 
44
+ The model is loaded via the `robot_descriptions
45
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
46
+
44
47
  .. runblock:: pycon
45
48
 
46
49
  >>> import roboticstoolbox as rtb
@@ -15,6 +15,9 @@ class YuMi(Robot):
15
15
  from a URDF file. The model describes its kinematic and graphical
16
16
  characteristics.
17
17
 
18
+ The model is loaded via the `robot_descriptions
19
+ <https://github.com/robot-descriptions/robot_descriptions.py>`_ package.
20
+
18
21
  .. runblock:: pycon
19
22
 
20
23
  >>> import roboticstoolbox as rtb
@@ -1889,7 +1889,7 @@ class DHRobot(Robot):
1889
1889
  if base is not None:
1890
1890
  T = base.inv() * T
1891
1891
  if tool is not None:
1892
- T = tool.inv() * T
1892
+ T = T * tool.inv()
1893
1893
 
1894
1894
  # q = np.zeros((6,))
1895
1895
  solutions = []
@@ -293,43 +293,25 @@ class IKSolver(ABC):
293
293
  linalg_error = 0
294
294
 
295
295
  # Initialise variables
296
- E = 0.0
296
+ E = np.inf
297
297
  q = q0[0]
298
298
 
299
299
  for search in range(self.slimit):
300
300
  q = q0[search].copy()
301
301
  i = 0
302
302
 
303
- while i < self.ilimit:
304
- i += 1
305
-
306
- # step() reports E for q as it was *before* this iteration's
307
- # update. An undamped update (GN/NR) can overshoot, so if E is
308
- # already below tol we must return this pre-step q, not the
309
- # mutated one step() hands back - otherwise we can report
310
- # success with a q whose actual residual is far above tol.
311
- q_prev = q.copy()
312
-
313
- # Attempt a step
314
- try:
315
- E, q[ets.jindices] = self.step(ets, Tep, q)
316
-
317
- except np.linalg.LinAlgError:
318
- # Abandon search and try again
319
- linalg_error += 1
320
- break
303
+ # Check the initial configuration before attempting an update.
304
+ # An exact or already-converged q0 must not enter a solver step,
305
+ # which can be singular even though the requested pose is solved.
306
+ _, E = self.error(ets.eval(q), Tep)
321
307
 
322
- # Check if we have arrived
308
+ while True:
309
+ # Check convergence for the current q before another update.
323
310
  if E < self.tol:
324
- q = q_prev
325
-
326
- # Wrap q to be within +- 180 deg
327
- # If your robot has larger than 180 deg range on a joint
328
- # this line should be modified in incorporate the extra range
329
- q = (q + np.pi) % (2 * np.pi) - np.pi
311
+ self._normalise_q(ets, q)
330
312
 
331
313
  # Check if we have violated joint limits
332
- jl_valid = self._check_jl(ets, q)
314
+ jl_valid = self._check_jl(ets, q[ets.jindices])
333
315
 
334
316
  if not jl_valid and self.joint_limits:
335
317
  # Abandon search and try again
@@ -344,6 +326,21 @@ class IKSolver(ABC):
344
326
  residual=E,
345
327
  reason="Success",
346
328
  )
329
+
330
+ if i >= self.ilimit:
331
+ break
332
+
333
+ i += 1
334
+
335
+ # Attempt a step. step() reports E for the updated q.
336
+ try:
337
+ E, q[ets.jindices] = self.step(ets, Tep, q)
338
+
339
+ except np.linalg.LinAlgError:
340
+ # Abandon search and try again
341
+ linalg_error += 1
342
+ break
343
+
347
344
  total_i += i
348
345
 
349
346
  # If we make it here, then we have failed
@@ -450,6 +447,46 @@ class IKSolver(ABC):
450
447
 
451
448
  return q
452
449
 
450
+ def _normalise_q(self, ets: "rtb.ETS", q: np.ndarray) -> None:
451
+ """
452
+ Choose equivalent revolute coordinates without changing prismatic joints.
453
+
454
+ :param ets: The ETS defining the joint types and limits
455
+ :param q: Joint coordinates, indexed by joint jindex, modified in place
456
+ :returns: None
457
+
458
+ Preserve coordinates already within their limits, including revolute joints
459
+ outside the principal interval. Otherwise prefer the principal angle, shifted
460
+ by whole turns towards the limits if necessary. If no equivalent angle lies
461
+ within the limits, the subsequent joint-limit check will reject the solution.
462
+ """
463
+ qlim = ets.qlim
464
+ for i, joint in enumerate(ets.joints()):
465
+ j = joint.jindex
466
+ lower, upper = qlim[:, i]
467
+ if not joint.isrotation or lower <= q[j] <= upper:
468
+ continue
469
+
470
+ # Avoid rounding a principal angle across a nearby joint limit.
471
+ angle = q[j]
472
+ if not -np.pi <= angle < np.pi:
473
+ angle = (angle + np.pi) % (2 * np.pi) - np.pi
474
+ if angle < lower:
475
+ angle += 2 * np.pi * np.ceil((lower - angle) / (2 * np.pi))
476
+ elif angle > upper:
477
+ angle -= 2 * np.pi * np.ceil((angle - upper) / (2 * np.pi))
478
+ if not lower <= angle <= upper:
479
+ # Argument reduction can round a limit plus whole turns just
480
+ # outside the closed interval. Accept that endpoint only when
481
+ # it reconstructs the original coordinate exactly, without an
482
+ # epsilon that could admit an unrelated out-of-limit angle.
483
+ for bound in (lower, upper):
484
+ turns = np.rint((q[j] - bound) / (2 * np.pi))
485
+ if turns != 0 and bound + turns * (2 * np.pi) == q[j]:
486
+ angle = bound
487
+ break
488
+ q[j] = angle
489
+
453
490
  def _check_jl(self, ets: "rtb.ETS", q: np.ndarray) -> bool:
454
491
  """
455
492
  Checks if the joints are within their respective limits
@@ -709,6 +746,7 @@ class IK_NR(IKSolver):
709
746
  else:
710
747
  q[ets.jindices] += np.linalg.inv(J) @ e + qnull
711
748
 
749
+ _, E = self.error(ets.eval(q), Tep)
712
750
  return E, q[ets.jindices]
713
751
 
714
752
 
@@ -941,6 +979,7 @@ class IK_LM(IKSolver):
941
979
 
942
980
  q[ets.jindices] += np.linalg.inv(J.T @ self.We @ J + Wn) @ g + qnull
943
981
 
982
+ _, E = self.error(ets.eval(q), Tep)
944
983
  return E, q[ets.jindices]
945
984
 
946
985
 
@@ -1121,6 +1160,7 @@ class IK_GN(IKSolver):
1121
1160
  else:
1122
1161
  q[ets.jindices] += np.linalg.inv(J) @ e + qnull
1123
1162
 
1163
+ _, E = self.error(ets.eval(q), Tep)
1124
1164
  return E, q[ets.jindices]
1125
1165
 
1126
1166
 
@@ -1388,6 +1428,7 @@ class IK_QP(IKSolver):
1388
1428
 
1389
1429
  q += xd[: ets.n]
1390
1430
 
1431
+ _, E = self.error(ets.eval(q), Tep)
1391
1432
  return E, q
1392
1433
 
1393
1434