Skip to content

Robot: screws.robot

The Robot class: one object holding a serial chain's kinematic and inertial description.

Every method delegates to the chapter functions (so3, se3, kinematics), which remain the reference implementation.

MissingInertias

Bases: Exception

Raised when a dynamics method is called on a robot without link inertias.

Source code in screws/robot.py
22
23
class MissingInertias(Exception):
    """Raised when a dynamics method is called on a robot without link inertias."""

Robot dataclass

A serial-chain robot in the product-of-exponentials description.

M: 4x4 home configuration of {b} in {s}. S: 6xn space-frame screw axes at home, one per column. joint_types: "revolute" or "prismatic" per joint. joint_limits: nx2 lower/upper bounds, or None. joint_frames_home: each joint frame T_{0i}(0), or None. link_frames: MR's M_{i-1,i}, n+1 of them, or None. link_inertias: MR's spatial inertias G_i, 6x6 each, or None. gravity: the gravity vector in {s}.

MR 4.1 (M, S), 4.1.3 (B), 8.3 (link_frames, link_inertias); notes chapter 4.

Source code in screws/robot.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
@dataclass(frozen=True, eq=False)
class Robot:
    """A serial-chain robot in the product-of-exponentials description.

    M: 4x4 home configuration of {b} in {s}. S: 6xn space-frame screw axes at home,
    one per column. joint_types: "revolute" or "prismatic" per joint. joint_limits:
    nx2 lower/upper bounds, or None. joint_frames_home: each joint frame T_{0i}(0), or
    None. link_frames: MR's M_{i-1,i}, n+1 of them, or None. link_inertias: MR's spatial
    inertias G_i, 6x6 each, or None. gravity: the gravity vector in {s}.

    MR 4.1 (M, S), 4.1.3 (B), 8.3 (link_frames, link_inertias); notes chapter 4.
    """

    name: str
    M: np.ndarray
    S: np.ndarray
    joint_types: tuple[str, ...]
    joint_names: tuple[str, ...]
    joint_limits: np.ndarray | None = None
    joint_frames_home: tuple[np.ndarray, ...] | None = None
    link_frames: tuple[np.ndarray, ...] | None = None
    link_inertias: tuple[np.ndarray, ...] | None = None
    gravity: np.ndarray = field(default_factory=_gravity_default)

    def __post_init__(self):
        M = np.asarray(self.M, dtype=float)
        S = np.asarray(self.S, dtype=float)
        if M.shape != (4, 4):
            raise ValueError(f"M must be 4x4; got shape {M.shape}")
        if S.ndim != 2 or S.shape[0] != 6:
            raise ValueError(f"S must be 6xn, one screw axis per column; got shape {S.shape}")
        n = S.shape[1]
        if len(self.joint_types) != n or len(self.joint_names) != n:
            raise ValueError("joint_types and joint_names must have one entry per joint")
        object.__setattr__(self, "M", M)
        object.__setattr__(self, "S", S)
        object.__setattr__(self, "joint_types", tuple(self.joint_types))
        object.__setattr__(self, "joint_names", tuple(self.joint_names))
        object.__setattr__(self, "gravity", np.asarray(self.gravity, dtype=float))
        if self.joint_limits is not None:
            lim = np.asarray(self.joint_limits, dtype=float)
            if lim.shape != (n, 2):
                raise ValueError(f"joint_limits must be nx2; got shape {lim.shape}")
            object.__setattr__(self, "joint_limits", lim)
        for name in ("joint_frames_home", "link_frames", "link_inertias"):
            v = getattr(self, name)
            if v is not None:
                object.__setattr__(self, name, tuple(_frozen(a) for a in v))
        for name in ("M", "S", "gravity", "joint_limits"):
            v = getattr(self, name)
            if v is not None:
                object.__setattr__(self, name, _frozen(v))

    # ----- construction -------------------------------------------------------------

    @classmethod
    def from_screw_axes(
        cls,
        M,
        axes,
        *,
        name: str = "robot",
        joint_types=None,
        joint_names=None,
        joint_limits=None,
        joint_frames_home=None,
    ) -> Robot:
        """Build a robot from M and a sequence of space-frame screw axes, one 6-vector per joint.

        The axes must be a sequence (list or tuple), not a 2-D array: a 6x6 array is
        ambiguous when n = 6. Joint types default to reading each axis (omega = 0 means
        prismatic). joint_frames_home, one 4x4 frame per joint at the home position, lets
        frames() and the drawings place the joints where they really are. Notes 4.1.
        """
        if isinstance(axes, np.ndarray) and axes.ndim == 2:
            raise TypeError(
                "axes must be a sequence of 6-vectors, one per joint; "
                "a 6xn array is ambiguous when n = 6 (pass list(S.T) for columns)"
            )
        vectors = [np.asarray(a, dtype=float).reshape(-1) for a in axes]
        for v in vectors:
            if v.shape != (6,):
                raise ValueError(f"each screw axis must be a 6-vector; got shape {v.shape}")
        S = np.column_stack(vectors)
        n = S.shape[1]
        if joint_types is None:
            joint_types = tuple(
                "prismatic" if near_zero(np.linalg.norm(v[:3])) else "revolute" for v in vectors
            )
        if joint_names is None:
            joint_names = tuple(f"joint{i + 1}" for i in range(n))
        return cls(
            name=name,
            M=M,
            S=S,
            joint_types=tuple(joint_types),
            joint_names=tuple(joint_names),
            joint_limits=joint_limits,
            joint_frames_home=None if joint_frames_home is None else tuple(joint_frames_home),
        )

    @classmethod
    def from_urdf(cls, path, *, base_link=None, ee_link=None) -> Robot:
        """Build a robot from a URDF file or string; see screws.urdf.load."""
        from .urdf import load

        return load(path, base_link=base_link, ee_link=ee_link)

    # ----- derived quantities ---------------------------------------------------------

    @property
    def n(self) -> int:
        """The number of joints."""
        return self.S.shape[1]

    @cached_property
    def B(self) -> np.ndarray:
        """The body-frame screw axes at home, B = [Ad_{M^{-1}}] S. MR 4.1.3; notes 4.2."""
        return adjoint(transform_inv(self.M)) @ self.S

    # ----- kinematics -----------------------------------------------------------------

    def fk(self, theta) -> np.ndarray:
        """T_sb(theta), the end-effector configuration, by the space form. Notes 4.1."""
        return kin.fk_space(self.M, self.S, theta)

    def frames(self, theta) -> list[np.ndarray]:
        """Every joint frame T_{0i}(theta), i = 1..n, followed by T_sb(theta).

        Uses joint_frames_home when the robot has them. Otherwise each joint frame is a
        stand-in for a stick figure: origin at the point of the axis nearest {s},
        q = omega x v, z along omega; a prismatic joint reuses the previous origin.
        """
        homes = self.joint_frames_home
        if homes is None:
            homes = self._fallback_joint_frames()
        out = kin.joint_frames(homes, self.S, theta)
        out.append(self.fk(theta))
        return out

    def _fallback_joint_frames(self) -> list[np.ndarray]:
        frames, prev_q = [], np.zeros(3)
        for i in range(self.n):
            omega, v = self.S[:3, i], self.S[3:, i]
            if self.joint_types[i] == "prismatic":
                q, z = prev_q, normalize(v)
            else:
                q, z = np.cross(omega, v), normalize(omega)
            helper = np.array([1.0, 0.0, 0.0]) if abs(z[0]) < 0.9 else np.array([0.0, 1.0, 0.0])
            x = normalize(np.cross(helper, z))
            y = np.cross(z, x)
            frames.append(rp_to_transform(np.column_stack([x, y, z]), q))
            prev_q = q
        return frames

    def jacobian_space(self, theta) -> np.ndarray:
        """J_s(theta). MR 5.1.1; notes 5.1."""
        return kin.jacobian_space(self.S, theta)

    def jacobian_body(self, theta) -> np.ndarray:
        """J_b(theta). MR 5.1.2; notes 5.1."""
        return kin.jacobian_body(self.B, theta)

    def is_planar(self) -> bool:
        """True when every joint axis is along z and every v lies in the xy plane: the tool
        point then moves in a plane z = const, and its velocity has two components."""
        return bool(np.allclose(self.S[:2], 0) and np.allclose(self.S[5], 0))

    def _tool_block(self, theta, kind, planar=False) -> tuple[np.ndarray, np.ndarray]:
        """(the Jacobian block whose ellipsoid is wanted, in {s} axes; the tool position)."""
        if kind not in ("linear", "angular"):
            raise ValueError(f'kind must be "linear" or "angular"; got {kind!r}')
        T = self.fk(theta)
        if planar:
            if not self.is_planar():
                raise ValueError(f"{self.name} is not planar: its joint axes are not all along z with v in the xy plane")
            if kind == "angular":
                raise ValueError("a planar arm turns its tool about z only: its angular ellipsoid is a segment, not an ellipse")
            return (T[:3, :3] @ self.jacobian_body(theta)[3:])[:2], T[:2, 3]
        if kind == "linear":  # J_b's linear rows, the velocity of {b}'s origin, turned into {s}
            return T[:3, :3] @ self.jacobian_body(theta)[3:], T[:3, 3]
        return self.jacobian_space(theta)[:3], T[:3, 3]

    def manipulability_ellipsoid(self, theta, kind: str = "linear", *, planar: bool = False) -> kin.Ellipsoid:
        """The tool's manipulability ellipsoid at theta, centred at {b}'s origin, in {s} axes.

        kind="linear" uses J_b's linear rows, as MR advises (they give the velocity of a
        point on the tool, J_s's that of the body point at {s}'s origin), turned into {s}:
        R_sb J_bv. kind="angular" uses J_s's angular rows (= R_sb J_bw). A planar arm's is
        3D like any other, flat in z; planar=True gives the ellipse in the plane instead,
        from the x and y rows (the notes' 2 x n J(theta)). MR 5.4; notes 5.5.
        """
        J, p = self._tool_block(theta, kind, planar)
        return kin.manipulability_ellipsoid(J, center=p)

    def force_ellipsoid(self, theta, kind: str = "linear", *, planar: bool = False) -> kin.Ellipsoid:
        """The tool's force ellipsoid at theta (moments for kind="angular"), as above."""
        J, p = self._tool_block(theta, kind, planar)
        return kin.force_ellipsoid(J, center=p)

    def ik(self, T, theta0, *, frame: str = "body", **tol) -> kin.IKResult:
        """Numerical inverse kinematics for the goal T from the guess theta0.

        frame is "body" (default) or "space"; tol may carry tol_omega, tol_v, max_iter.
        MR 6.2; notes 6.2.
        """
        if frame == "body":
            return kin.ik_body(self.M, self.B, T, theta0, **tol)
        if frame == "space":
            return kin.ik_space(self.M, self.S, T, theta0, **tol)
        raise ValueError(f'frame must be "body" or "space"; got {frame!r}')

    def within_limits(self, theta) -> bool:
        """True if every joint value lies within joint_limits (always true without limits)."""
        if self.joint_limits is None:
            return True
        theta = np.asarray(theta, dtype=float)
        return bool(np.all(theta >= self.joint_limits[:, 0]) and np.all(theta <= self.joint_limits[:, 1]))

    def symbolic(self, *, decimals: int | None = None):
        """This robot as a screws.symbolic.SymbolicRobot, its floats read as decimals.

        robot.symbolic().fk() is T_sb as a formula in theta1 ... thetan; jacobian_space,
        jacobian_body and the dynamics methods follow. decimals=k rounds the floats to k
        places first (screws.symbolic.exact), and a robot read from a URDF usually wants it:
        robots.ur5().symbolic(decimals=6). Notes 4.1, 5.1, 8.3.
        """
        from .symbolic import SymbolicRobot

        return SymbolicRobot.from_robot(self, decimals=decimals)

    # ----- dynamics ---------------------------------------------------------------------

    def _need_inertias(self):
        if self.link_inertias is None or self.link_frames is None:
            raise MissingInertias(
                f"{self.name} has no link inertias; load a URDF with <inertial> elements, read "
                "them off a scene with Arm.robot(inertias=True), or call with_inertias(...)"
            )

    def inverse_dynamics(self, theta, dtheta, ddtheta, F_tip=None) -> np.ndarray:
        """tau = M(theta) ddtheta + c(theta, dtheta) + g(theta) + J^T F_tip, with this robot's gravity. MR 8.3."""
        self._need_inertias()
        return dyn.inverse_dynamics(
            theta, dtheta, ddtheta, self.gravity, F_tip, self.link_frames, self.link_inertias, self.S
        )

    def mass_matrix(self, theta) -> np.ndarray:
        """M(theta). MR 8.3."""
        self._need_inertias()
        return dyn.mass_matrix(theta, self.link_frames, self.link_inertias, self.S)

    def velocity_quadratic_forces(self, theta, dtheta) -> np.ndarray:
        """c(theta, dtheta). MR 8.3."""
        self._need_inertias()
        return dyn.velocity_quadratic_forces(theta, dtheta, self.link_frames, self.link_inertias, self.S)

    def gravity_forces(self, theta) -> np.ndarray:
        """g(theta), with this robot's gravity. MR 8.3."""
        self._need_inertias()
        return dyn.gravity_forces(theta, self.gravity, self.link_frames, self.link_inertias, self.S)

    def end_effector_forces(self, theta, F_tip) -> np.ndarray:
        """J^T(theta) F_tip. MR 8.3."""
        self._need_inertias()
        return dyn.end_effector_forces(theta, F_tip, self.link_frames, self.link_inertias, self.S)

    def forward_dynamics(self, theta, dtheta, tau, F_tip=None) -> np.ndarray:
        """ddtheta from tau, with this robot's gravity. MR 8.5."""
        self._need_inertias()
        return dyn.forward_dynamics(
            theta, dtheta, tau, self.gravity, F_tip, self.link_frames, self.link_inertias, self.S
        )

    # ----- copies ---------------------------------------------------------------------

    def with_gravity(self, g) -> Robot:
        """A copy with a different gravity vector."""
        return replace(self, gravity=np.asarray(g, dtype=float))

    def with_inertias(self, link_frames, link_inertias) -> Robot:
        """A copy carrying MR's M_{i-1,i} list and G_i list."""
        return replace(self, link_frames=tuple(link_frames), link_inertias=tuple(link_inertias))

    def with_limits(self, joint_limits) -> Robot:
        """A copy with nx2 joint limits."""
        return replace(self, joint_limits=joint_limits)

n property

n

The number of joints.

B cached property

B

The body-frame screw axes at home, B = [Ad_{M^{-1}}] S. MR 4.1.3; notes 4.2.

from_screw_axes classmethod

from_screw_axes(
    M,
    axes,
    *,
    name="robot",
    joint_types=None,
    joint_names=None,
    joint_limits=None,
    joint_frames_home=None,
)

Build a robot from M and a sequence of space-frame screw axes, one 6-vector per joint.

The axes must be a sequence (list or tuple), not a 2-D array: a 6x6 array is ambiguous when n = 6. Joint types default to reading each axis (omega = 0 means prismatic). joint_frames_home, one 4x4 frame per joint at the home position, lets frames() and the drawings place the joints where they really are. Notes 4.1.

Source code in screws/robot.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@classmethod
def from_screw_axes(
    cls,
    M,
    axes,
    *,
    name: str = "robot",
    joint_types=None,
    joint_names=None,
    joint_limits=None,
    joint_frames_home=None,
) -> Robot:
    """Build a robot from M and a sequence of space-frame screw axes, one 6-vector per joint.

    The axes must be a sequence (list or tuple), not a 2-D array: a 6x6 array is
    ambiguous when n = 6. Joint types default to reading each axis (omega = 0 means
    prismatic). joint_frames_home, one 4x4 frame per joint at the home position, lets
    frames() and the drawings place the joints where they really are. Notes 4.1.
    """
    if isinstance(axes, np.ndarray) and axes.ndim == 2:
        raise TypeError(
            "axes must be a sequence of 6-vectors, one per joint; "
            "a 6xn array is ambiguous when n = 6 (pass list(S.T) for columns)"
        )
    vectors = [np.asarray(a, dtype=float).reshape(-1) for a in axes]
    for v in vectors:
        if v.shape != (6,):
            raise ValueError(f"each screw axis must be a 6-vector; got shape {v.shape}")
    S = np.column_stack(vectors)
    n = S.shape[1]
    if joint_types is None:
        joint_types = tuple(
            "prismatic" if near_zero(np.linalg.norm(v[:3])) else "revolute" for v in vectors
        )
    if joint_names is None:
        joint_names = tuple(f"joint{i + 1}" for i in range(n))
    return cls(
        name=name,
        M=M,
        S=S,
        joint_types=tuple(joint_types),
        joint_names=tuple(joint_names),
        joint_limits=joint_limits,
        joint_frames_home=None if joint_frames_home is None else tuple(joint_frames_home),
    )

from_urdf classmethod

from_urdf(path, *, base_link=None, ee_link=None)

Build a robot from a URDF file or string; see screws.urdf.load.

Source code in screws/robot.py
138
139
140
141
142
143
@classmethod
def from_urdf(cls, path, *, base_link=None, ee_link=None) -> Robot:
    """Build a robot from a URDF file or string; see screws.urdf.load."""
    from .urdf import load

    return load(path, base_link=base_link, ee_link=ee_link)

fk

fk(theta)

T_sb(theta), the end-effector configuration, by the space form. Notes 4.1.

Source code in screws/robot.py
159
160
161
def fk(self, theta) -> np.ndarray:
    """T_sb(theta), the end-effector configuration, by the space form. Notes 4.1."""
    return kin.fk_space(self.M, self.S, theta)

frames

frames(theta)

Every joint frame T_{0i}(theta), i = 1..n, followed by T_sb(theta).

Uses joint_frames_home when the robot has them. Otherwise each joint frame is a stand-in for a stick figure: origin at the point of the axis nearest {s}, q = omega x v, z along omega; a prismatic joint reuses the previous origin.

Source code in screws/robot.py
163
164
165
166
167
168
169
170
171
172
173
174
175
def frames(self, theta) -> list[np.ndarray]:
    """Every joint frame T_{0i}(theta), i = 1..n, followed by T_sb(theta).

    Uses joint_frames_home when the robot has them. Otherwise each joint frame is a
    stand-in for a stick figure: origin at the point of the axis nearest {s},
    q = omega x v, z along omega; a prismatic joint reuses the previous origin.
    """
    homes = self.joint_frames_home
    if homes is None:
        homes = self._fallback_joint_frames()
    out = kin.joint_frames(homes, self.S, theta)
    out.append(self.fk(theta))
    return out

jacobian_space

jacobian_space(theta)

J_s(theta). MR 5.1.1; notes 5.1.

Source code in screws/robot.py
192
193
194
def jacobian_space(self, theta) -> np.ndarray:
    """J_s(theta). MR 5.1.1; notes 5.1."""
    return kin.jacobian_space(self.S, theta)

jacobian_body

jacobian_body(theta)

J_b(theta). MR 5.1.2; notes 5.1.

Source code in screws/robot.py
196
197
198
def jacobian_body(self, theta) -> np.ndarray:
    """J_b(theta). MR 5.1.2; notes 5.1."""
    return kin.jacobian_body(self.B, theta)

is_planar

is_planar()

True when every joint axis is along z and every v lies in the xy plane: the tool point then moves in a plane z = const, and its velocity has two components.

Source code in screws/robot.py
200
201
202
203
def is_planar(self) -> bool:
    """True when every joint axis is along z and every v lies in the xy plane: the tool
    point then moves in a plane z = const, and its velocity has two components."""
    return bool(np.allclose(self.S[:2], 0) and np.allclose(self.S[5], 0))

manipulability_ellipsoid

manipulability_ellipsoid(
    theta, kind="linear", *, planar=False
)

The tool's manipulability ellipsoid at theta, centred at {b}'s origin, in {s} axes.

kind="linear" uses J_b's linear rows, as MR advises (they give the velocity of a point on the tool, J_s's that of the body point at {s}'s origin), turned into {s}: R_sb J_bv. kind="angular" uses J_s's angular rows (= R_sb J_bw). A planar arm's is 3D like any other, flat in z; planar=True gives the ellipse in the plane instead, from the x and y rows (the notes' 2 x n J(theta)). MR 5.4; notes 5.5.

Source code in screws/robot.py
220
221
222
223
224
225
226
227
228
229
230
def manipulability_ellipsoid(self, theta, kind: str = "linear", *, planar: bool = False) -> kin.Ellipsoid:
    """The tool's manipulability ellipsoid at theta, centred at {b}'s origin, in {s} axes.

    kind="linear" uses J_b's linear rows, as MR advises (they give the velocity of a
    point on the tool, J_s's that of the body point at {s}'s origin), turned into {s}:
    R_sb J_bv. kind="angular" uses J_s's angular rows (= R_sb J_bw). A planar arm's is
    3D like any other, flat in z; planar=True gives the ellipse in the plane instead,
    from the x and y rows (the notes' 2 x n J(theta)). MR 5.4; notes 5.5.
    """
    J, p = self._tool_block(theta, kind, planar)
    return kin.manipulability_ellipsoid(J, center=p)

force_ellipsoid

force_ellipsoid(theta, kind='linear', *, planar=False)

The tool's force ellipsoid at theta (moments for kind="angular"), as above.

Source code in screws/robot.py
232
233
234
235
def force_ellipsoid(self, theta, kind: str = "linear", *, planar: bool = False) -> kin.Ellipsoid:
    """The tool's force ellipsoid at theta (moments for kind="angular"), as above."""
    J, p = self._tool_block(theta, kind, planar)
    return kin.force_ellipsoid(J, center=p)

ik

ik(T, theta0, *, frame='body', **tol)

Numerical inverse kinematics for the goal T from the guess theta0.

frame is "body" (default) or "space"; tol may carry tol_omega, tol_v, max_iter. MR 6.2; notes 6.2.

Source code in screws/robot.py
237
238
239
240
241
242
243
244
245
246
247
def ik(self, T, theta0, *, frame: str = "body", **tol) -> kin.IKResult:
    """Numerical inverse kinematics for the goal T from the guess theta0.

    frame is "body" (default) or "space"; tol may carry tol_omega, tol_v, max_iter.
    MR 6.2; notes 6.2.
    """
    if frame == "body":
        return kin.ik_body(self.M, self.B, T, theta0, **tol)
    if frame == "space":
        return kin.ik_space(self.M, self.S, T, theta0, **tol)
    raise ValueError(f'frame must be "body" or "space"; got {frame!r}')

within_limits

within_limits(theta)

True if every joint value lies within joint_limits (always true without limits).

Source code in screws/robot.py
249
250
251
252
253
254
def within_limits(self, theta) -> bool:
    """True if every joint value lies within joint_limits (always true without limits)."""
    if self.joint_limits is None:
        return True
    theta = np.asarray(theta, dtype=float)
    return bool(np.all(theta >= self.joint_limits[:, 0]) and np.all(theta <= self.joint_limits[:, 1]))

symbolic

symbolic(*, decimals=None)

This robot as a screws.symbolic.SymbolicRobot, its floats read as decimals.

robot.symbolic().fk() is T_sb as a formula in theta1 ... thetan; jacobian_space, jacobian_body and the dynamics methods follow. decimals=k rounds the floats to k places first (screws.symbolic.exact), and a robot read from a URDF usually wants it: robots.ur5().symbolic(decimals=6). Notes 4.1, 5.1, 8.3.

Source code in screws/robot.py
256
257
258
259
260
261
262
263
264
265
266
def symbolic(self, *, decimals: int | None = None):
    """This robot as a screws.symbolic.SymbolicRobot, its floats read as decimals.

    robot.symbolic().fk() is T_sb as a formula in theta1 ... thetan; jacobian_space,
    jacobian_body and the dynamics methods follow. decimals=k rounds the floats to k
    places first (screws.symbolic.exact), and a robot read from a URDF usually wants it:
    robots.ur5().symbolic(decimals=6). Notes 4.1, 5.1, 8.3.
    """
    from .symbolic import SymbolicRobot

    return SymbolicRobot.from_robot(self, decimals=decimals)

inverse_dynamics

inverse_dynamics(theta, dtheta, ddtheta, F_tip=None)

tau = M(theta) ddtheta + c(theta, dtheta) + g(theta) + J^T F_tip, with this robot's gravity. MR 8.3.

Source code in screws/robot.py
277
278
279
280
281
282
def inverse_dynamics(self, theta, dtheta, ddtheta, F_tip=None) -> np.ndarray:
    """tau = M(theta) ddtheta + c(theta, dtheta) + g(theta) + J^T F_tip, with this robot's gravity. MR 8.3."""
    self._need_inertias()
    return dyn.inverse_dynamics(
        theta, dtheta, ddtheta, self.gravity, F_tip, self.link_frames, self.link_inertias, self.S
    )

mass_matrix

mass_matrix(theta)

M(theta). MR 8.3.

Source code in screws/robot.py
284
285
286
287
def mass_matrix(self, theta) -> np.ndarray:
    """M(theta). MR 8.3."""
    self._need_inertias()
    return dyn.mass_matrix(theta, self.link_frames, self.link_inertias, self.S)

velocity_quadratic_forces

velocity_quadratic_forces(theta, dtheta)

c(theta, dtheta). MR 8.3.

Source code in screws/robot.py
289
290
291
292
def velocity_quadratic_forces(self, theta, dtheta) -> np.ndarray:
    """c(theta, dtheta). MR 8.3."""
    self._need_inertias()
    return dyn.velocity_quadratic_forces(theta, dtheta, self.link_frames, self.link_inertias, self.S)

gravity_forces

gravity_forces(theta)

g(theta), with this robot's gravity. MR 8.3.

Source code in screws/robot.py
294
295
296
297
def gravity_forces(self, theta) -> np.ndarray:
    """g(theta), with this robot's gravity. MR 8.3."""
    self._need_inertias()
    return dyn.gravity_forces(theta, self.gravity, self.link_frames, self.link_inertias, self.S)

end_effector_forces

end_effector_forces(theta, F_tip)

J^T(theta) F_tip. MR 8.3.

Source code in screws/robot.py
299
300
301
302
def end_effector_forces(self, theta, F_tip) -> np.ndarray:
    """J^T(theta) F_tip. MR 8.3."""
    self._need_inertias()
    return dyn.end_effector_forces(theta, F_tip, self.link_frames, self.link_inertias, self.S)

forward_dynamics

forward_dynamics(theta, dtheta, tau, F_tip=None)

ddtheta from tau, with this robot's gravity. MR 8.5.

Source code in screws/robot.py
304
305
306
307
308
309
def forward_dynamics(self, theta, dtheta, tau, F_tip=None) -> np.ndarray:
    """ddtheta from tau, with this robot's gravity. MR 8.5."""
    self._need_inertias()
    return dyn.forward_dynamics(
        theta, dtheta, tau, self.gravity, F_tip, self.link_frames, self.link_inertias, self.S
    )

with_gravity

with_gravity(g)

A copy with a different gravity vector.

Source code in screws/robot.py
313
314
315
def with_gravity(self, g) -> Robot:
    """A copy with a different gravity vector."""
    return replace(self, gravity=np.asarray(g, dtype=float))

with_inertias

with_inertias(link_frames, link_inertias)

A copy carrying MR's M_{i-1,i} list and G_i list.

Source code in screws/robot.py
317
318
319
def with_inertias(self, link_frames, link_inertias) -> Robot:
    """A copy carrying MR's M_{i-1,i} list and G_i list."""
    return replace(self, link_frames=tuple(link_frames), link_inertias=tuple(link_inertias))

with_limits

with_limits(joint_limits)

A copy with nx2 joint limits.

Source code in screws/robot.py
321
322
323
def with_limits(self, joint_limits) -> Robot:
    """A copy with nx2 joint limits."""
    return replace(self, joint_limits=joint_limits)