franka/model/mod.rs
1//! Kinematics and dynamics of the arm, mirroring libfranka's `franka::Model`.
2//!
3//! [`Model`] is a port of `franka::Model` (`include/franka/model.h`,
4//! `src/model.cpp`); it is a thin dispatch layer over a [`RobotModelBackend`],
5//! the equivalent of libfranka's `franka::RobotModelBase`. The default backend
6//! is [`native_backend::NativeBackend`], a serial-chain implementation that
7//! reproduces libfranka's Pinocchio-based `franka::RobotModel` bit-for-bit
8//! within 1e-9 (kinematics) and 1e-6 (dynamics).
9//!
10//! # Matrix layouts
11//!
12//! * 4x4 poses are vectorised column-major (`[f64; 16]`).
13//! * Jacobians are 6x7 column-major (`[f64; 42]`); within a column the rows are
14//! the linear parts `vx, vy, vz` first and the angular parts `wx, wy, wz`
15//! second. This is Pinocchio's `Motion` layout, which is what libfranka
16//! copies out of Eigen verbatim.
17//! * The mass matrix is 7x7 column-major (`[f64; 49]`); it is symmetric.
18//!
19//! # Example
20//!
21//! ```no_run
22//! use franka::model::{Frame, Model};
23//! use franka::robot_state::RobotState;
24//!
25//! let urdf = std::fs::read_to_string("fr3.urdf").unwrap();
26//! let model = Model::from_urdf(&urdf).unwrap();
27//! let state = RobotState::default();
28//! let o_t_ee = model.pose(Frame::EndEffector, &state);
29//! let gravity = model.gravity(&state);
30//! # let _ = (o_t_ee, gravity);
31//! ```
32
33mod spatial;
34
35pub mod native_backend;
36
37/// Downloading and loading the FCI v5 (Franka Emika Robot, FER) model library.
38///
39/// [`model_library::load_from_robot`] exists in every configuration so that
40/// callers need no `cfg` of their own; the `dlopen`-based backend behind it is
41/// the opt-in `model-library` Cargo feature, which pulls in `libloading`.
42/// Without that feature the crate builds with the URDF-based
43/// [`native_backend`] only and `load_from_robot` reports a
44/// [`crate::FrankaError::Model`].
45pub mod model_library;
46
47#[cfg(feature = "model-library")]
48pub mod so_backend;
49
50use crate::error::FrankaResult;
51use crate::robot_state::RobotState;
52
53pub use model_library::load_from_robot;
54pub use native_backend::NativeBackend;
55#[cfg(feature = "model-library")]
56pub use so_backend::SoModelBackend;
57
58/// Earth's gravity used by [`Model::coriolis`], matching libfranka's
59/// `RobotModel::coriolis` fallback of `{0, 0, -9.81}`.
60pub const DEFAULT_GRAVITY_EARTH: [f64; 3] = [0.0, 0.0, -9.81];
61
62/// The Franka Emika Robot (FER)'s arm, as a URDF the [`native_backend`] can evaluate.
63///
64/// An FER has no `GetRobotModel`, so this file is *not* something the robot
65/// serves: it is a mesh-free, arm-only description built here — the joint
66/// origins and axes of `franka_description`'s `fer_joint1..8`, plus link
67/// inertial parameters identified from an FER's own `libfcimodels_x64.so` by
68/// `tools/fer-model-fit`. [`Model::native_fer`] parses it.
69///
70/// The ten inertial parameters per link are a *base-parameter-equivalent* set,
71/// chosen to reproduce the robot's model, and not a physically realisable
72/// description of each casting; see `docs/book/src/reference/model.md`.
73pub const FER_URDF: &str = include_str!("../../tests/data/fer.urdf");
74
75/// The FR3 URDF from franka_description that the tests and `franka-rerun` replays use.
76pub const FR3_URDF: &str = include_str!("../../tests/data/fr3.urdf");
77
78/// The seven joints, the flange, the end effector and the stiffness frame.
79///
80/// Port of `franka::Frame` (`include/franka/model.h`); the variants are in the
81/// same order as the C++ enumerators `kJoint1 .. kStiffness`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
83pub enum Frame {
84 /// Frame of joint 1.
85 Joint1,
86 /// Frame of joint 2.
87 Joint2,
88 /// Frame of joint 3.
89 Joint3,
90 /// Frame of joint 4.
91 Joint4,
92 /// Frame of joint 5.
93 Joint5,
94 /// Frame of joint 6.
95 Joint6,
96 /// Frame of joint 7.
97 Joint7,
98 /// Flange frame (the `link8` frame of the URDF).
99 Flange,
100 /// End-effector frame, i.e. the flange frame post-multiplied by `F_T_EE`.
101 EndEffector,
102 /// Stiffness frame K, i.e. the end-effector frame post-multiplied by `EE_T_K`.
103 Stiffness,
104}
105
106impl Frame {
107 /// All frames, in the order of the C++ enumerators.
108 pub const ALL: [Frame; 10] = [
109 Frame::Joint1,
110 Frame::Joint2,
111 Frame::Joint3,
112 Frame::Joint4,
113 Frame::Joint5,
114 Frame::Joint6,
115 Frame::Joint7,
116 Frame::Flange,
117 Frame::EndEffector,
118 Frame::Stiffness,
119 ];
120
121 /// The joint index (`1..=7`) of a joint frame, `None` for the others.
122 pub fn joint_index(self) -> Option<usize> {
123 match self {
124 Frame::Joint1 => Some(1),
125 Frame::Joint2 => Some(2),
126 Frame::Joint3 => Some(3),
127 Frame::Joint4 => Some(4),
128 Frame::Joint5 => Some(5),
129 Frame::Joint6 => Some(6),
130 Frame::Joint7 => Some(7),
131 _ => None,
132 }
133 }
134}
135
136/// Rigid-body model backend.
137///
138/// Port of libfranka's `franka::RobotModelBase` (`src/robot_model_base.h`).
139/// The method set is deliberately identical so an alternative backend can be
140/// slotted in the way libfranka's tests slot in a mocked `RobotModelBase`.
141///
142/// All `joint_index` arguments are 1-based and must be in `1..=7`.
143pub trait RobotModelBackend {
144 /// Coriolis force vector `C(q, dq) * dq`, in Nm.
145 ///
146 /// Port of `RobotModelBase::coriolis` with a gravity argument, which
147 /// libfranka evaluates as `rnea(q, dq, 0) - generalized_gravity(q)`.
148 fn coriolis(
149 &self,
150 q: &[f64; 7],
151 dq: &[f64; 7],
152 i_total: &[f64; 9],
153 m_total: f64,
154 f_x_ctotal: &[f64; 3],
155 gravity_earth: &[f64; 3],
156 ) -> [f64; 7];
157
158 /// Gravity torque vector, in Nm. Port of `RobotModelBase::gravity`.
159 fn gravity(
160 &self,
161 q: &[f64; 7],
162 gravity_earth: &[f64; 3],
163 m_total: f64,
164 f_x_ctotal: &[f64; 3],
165 ) -> [f64; 7];
166
167 /// 7x7 joint-space inertia matrix, column-major. Port of `RobotModelBase::mass`.
168 fn mass(
169 &self,
170 q: &[f64; 7],
171 i_total: &[f64; 9],
172 m_total: f64,
173 f_x_ctotal: &[f64; 3],
174 ) -> [f64; 49];
175
176 /// Pose of a joint frame in the base frame. Port of `RobotModelBase::pose`.
177 fn pose(&self, q: &[f64; 7], joint_index: usize) -> [f64; 16];
178
179 /// Pose of the flange frame. Port of `RobotModelBase::poseFlange`.
180 fn pose_flange(&self, q: &[f64; 7]) -> [f64; 16];
181
182 /// Pose of the end-effector frame. Port of `RobotModelBase::poseEe`.
183 fn pose_ee(&self, q: &[f64; 7], f_t_ee: &[f64; 16]) -> [f64; 16];
184
185 /// Pose of the stiffness frame. Port of `RobotModelBase::poseStiffness`.
186 fn pose_stiffness(&self, q: &[f64; 7], f_t_ee: &[f64; 16], ee_t_k: &[f64; 16]) -> [f64; 16];
187
188 /// Body (`LOCAL`) Jacobian of a joint frame. Port of `RobotModelBase::bodyJacobian`.
189 fn body_jacobian(&self, q: &[f64; 7], joint_index: usize) -> [f64; 42];
190
191 /// Body Jacobian of the flange frame. Port of `RobotModelBase::bodyJacobianFlange`.
192 fn body_jacobian_flange(&self, q: &[f64; 7]) -> [f64; 42];
193
194 /// Body Jacobian of the end-effector frame. Port of `RobotModelBase::bodyJacobianEe`.
195 fn body_jacobian_ee(&self, q: &[f64; 7], f_t_ee: &[f64; 16]) -> [f64; 42];
196
197 /// Body Jacobian of the stiffness frame. Port of `RobotModelBase::bodyJacobianStiffness`.
198 fn body_jacobian_stiffness(
199 &self,
200 q: &[f64; 7],
201 f_t_ee: &[f64; 16],
202 ee_t_k: &[f64; 16],
203 ) -> [f64; 42];
204
205 /// Zero (`LOCAL_WORLD_ALIGNED`) Jacobian of a joint frame. Port of
206 /// `RobotModelBase::zeroJacobian`.
207 fn zero_jacobian(&self, q: &[f64; 7], joint_index: usize) -> [f64; 42];
208
209 /// Zero Jacobian of the flange frame. Port of `RobotModelBase::zeroJacobianFlange`.
210 fn zero_jacobian_flange(&self, q: &[f64; 7]) -> [f64; 42];
211
212 /// Zero Jacobian of the end-effector frame. Port of `RobotModelBase::zeroJacobianEe`.
213 fn zero_jacobian_ee(&self, q: &[f64; 7], f_t_ee: &[f64; 16]) -> [f64; 42];
214
215 /// Zero Jacobian of the stiffness frame. Port of `RobotModelBase::zeroJacobianStiffness`.
216 fn zero_jacobian_stiffness(
217 &self,
218 q: &[f64; 7],
219 f_t_ee: &[f64; 16],
220 ee_t_k: &[f64; 16],
221 ) -> [f64; 42];
222}
223
224/// Poses of the joints and dynamic properties of the robot.
225///
226/// Port of `franka::Model`. Obtain one from `Robot::load_model`, or build one
227/// directly from a URDF with [`Model::from_urdf`].
228pub struct Model {
229 backend: Box<dyn RobotModelBackend + Send + Sync>,
230}
231
232impl std::fmt::Debug for Model {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.debug_struct("Model").finish_non_exhaustive()
235 }
236}
237
238impl Model {
239 /// Builds a model from a URDF string using the native backend.
240 ///
241 /// Port of `franka::Model::Model(const std::string&)`.
242 ///
243 /// # Errors
244 ///
245 /// [`crate::FrankaError::Model`] if the URDF cannot be parsed or does not
246 /// describe a seven-axis arm ending in a `link8` flange frame.
247 pub fn from_urdf(urdf: &str) -> FrankaResult<Model> {
248 Ok(Model::from_backend(Box::new(NativeBackend::from_urdf(
249 urdf,
250 )?)))
251 }
252
253 /// Builds the Franka Emika Robot (FER)'s model, without asking a robot for it.
254 ///
255 /// This is the FCI v5 counterpart of what [`Model::from_urdf`] does with an
256 /// FR3's `GetRobotModel` answer: it is the native backend over
257 /// [`FER_URDF`], whose parameters were identified from an FER's own
258 /// `libfcimodels_x64.so`. It needs no download, no `dlopen`, no
259 /// `model-library` feature and no x86-64 Linux host, and it is what
260 /// [`crate::Robot::load_model`] returns on an FER.
261 ///
262 /// Agreement with the shared object, over 208 joint configurations
263 /// (see `docs/book/src/reference/model.md`): poses and both Jacobians to 9e-16 at
264 /// all ten frames, gravity to 5e-14 with any payload, and the mass matrix
265 /// and Coriolis vector to 4e-14 with no payload. With a payload the mass
266 /// matrix and Coriolis vector differ by up to 3e-3 and 5e-2, because the
267 /// robot's own `M_NE` is not affine in `m_load` and therefore is not a
268 /// rigid-body model of the payload; see `docs/book/src/reference/model.md`. Use
269 /// [`crate::Robot::load_model_from_robot`] when you need the library's own
270 /// answer to the last bit.
271 ///
272 /// # Panics
273 ///
274 /// Never: [`FER_URDF`] is compiled in and a unit test parses it.
275 pub fn native_fer() -> Model {
276 Model::from_urdf(FER_URDF).expect("the built-in FER URDF parses")
277 }
278
279 /// Builds a model around an explicit backend.
280 ///
281 /// Port of `franka::Model::Model(std::unique_ptr<RobotModelBase>)`, which
282 /// libfranka provides for its own tests; the conformance suite uses it to
283 /// run the evaluation backends through the same API.
284 pub fn from_backend(backend: Box<dyn RobotModelBackend + Send + Sync>) -> Model {
285 Model { backend }
286 }
287
288 /// Borrows the underlying backend.
289 pub fn backend(&self) -> &(dyn RobotModelBackend + Send + Sync) {
290 self.backend.as_ref()
291 }
292
293 /// 4x4 pose matrix of `frame` in the base frame, column-major.
294 ///
295 /// Port of `franka::Model::pose(Frame, const RobotState&)`.
296 #[allow(non_snake_case)]
297 pub fn pose(&self, frame: Frame, state: &RobotState) -> [f64; 16] {
298 self.pose_q(frame, &state.q, &state.F_T_EE, &state.EE_T_K)
299 }
300
301 /// 4x4 pose matrix of `frame` in the base frame, column-major.
302 ///
303 /// Port of `franka::Model::pose(Frame, q, F_T_EE, EE_T_K)`.
304 #[allow(non_snake_case)]
305 pub fn pose_q(
306 &self,
307 frame: Frame,
308 q: &[f64; 7],
309 F_T_EE: &[f64; 16],
310 EE_T_K: &[f64; 16],
311 ) -> [f64; 16] {
312 match frame {
313 Frame::Flange => self.backend.pose_flange(q),
314 Frame::EndEffector => self.backend.pose_ee(q, F_T_EE),
315 Frame::Stiffness => self.backend.pose_stiffness(q, F_T_EE, EE_T_K),
316 joint => self
317 .backend
318 .pose(q, joint.joint_index().expect("joint frame")),
319 }
320 }
321
322 /// 6x7 body Jacobian of `frame`, column-major, relative to `frame` itself.
323 ///
324 /// Port of `franka::Model::bodyJacobian(Frame, const RobotState&)`.
325 #[allow(non_snake_case)]
326 pub fn body_jacobian(&self, frame: Frame, state: &RobotState) -> [f64; 42] {
327 self.body_jacobian_q(frame, &state.q, &state.F_T_EE, &state.EE_T_K)
328 }
329
330 /// 6x7 body Jacobian of `frame`, column-major, relative to `frame` itself.
331 ///
332 /// Port of `franka::Model::bodyJacobian(Frame, q, F_T_EE, EE_T_K)`.
333 #[allow(non_snake_case)]
334 pub fn body_jacobian_q(
335 &self,
336 frame: Frame,
337 q: &[f64; 7],
338 F_T_EE: &[f64; 16],
339 EE_T_K: &[f64; 16],
340 ) -> [f64; 42] {
341 match frame {
342 Frame::Flange => self.backend.body_jacobian_flange(q),
343 Frame::EndEffector => self.backend.body_jacobian_ee(q, F_T_EE),
344 Frame::Stiffness => self.backend.body_jacobian_stiffness(q, F_T_EE, EE_T_K),
345 joint => self
346 .backend
347 .body_jacobian(q, joint.joint_index().expect("joint frame")),
348 }
349 }
350
351 /// 6x7 zero Jacobian of `frame`, column-major, relative to the base frame.
352 ///
353 /// Port of `franka::Model::zeroJacobian(Frame, const RobotState&)`.
354 #[allow(non_snake_case)]
355 pub fn zero_jacobian(&self, frame: Frame, state: &RobotState) -> [f64; 42] {
356 self.zero_jacobian_q(frame, &state.q, &state.F_T_EE, &state.EE_T_K)
357 }
358
359 /// 6x7 zero Jacobian of `frame`, column-major, relative to the base frame.
360 ///
361 /// Port of `franka::Model::zeroJacobian(Frame, q, F_T_EE, EE_T_K)`.
362 #[allow(non_snake_case)]
363 pub fn zero_jacobian_q(
364 &self,
365 frame: Frame,
366 q: &[f64; 7],
367 F_T_EE: &[f64; 16],
368 EE_T_K: &[f64; 16],
369 ) -> [f64; 42] {
370 match frame {
371 Frame::Flange => self.backend.zero_jacobian_flange(q),
372 Frame::EndEffector => self.backend.zero_jacobian_ee(q, F_T_EE),
373 Frame::Stiffness => self.backend.zero_jacobian_stiffness(q, F_T_EE, EE_T_K),
374 joint => self
375 .backend
376 .zero_jacobian(q, joint.joint_index().expect("joint frame")),
377 }
378 }
379
380 /// 7x7 mass matrix, column-major, in `kg * m^2`.
381 ///
382 /// Port of `franka::Model::mass(const RobotState&)`.
383 pub fn mass(&self, state: &RobotState) -> [f64; 49] {
384 self.mass_q(&state.q, &state.I_total, state.m_total, &state.F_x_Ctotal)
385 }
386
387 /// 7x7 mass matrix, column-major, in `kg * m^2`.
388 ///
389 /// Port of `franka::Model::mass(q, I_total, m_total, F_x_Ctotal)`.
390 #[allow(non_snake_case)]
391 pub fn mass_q(
392 &self,
393 q: &[f64; 7],
394 I_total: &[f64; 9],
395 m_total: f64,
396 F_x_Ctotal: &[f64; 3],
397 ) -> [f64; 49] {
398 self.backend.mass(q, I_total, m_total, F_x_Ctotal)
399 }
400
401 /// Coriolis force vector, in Nm, with Earth's gravity `{0, 0, -9.81}`.
402 ///
403 /// Port of `franka::Model::coriolis(const RobotState&)`, which forwards to
404 /// the deprecated `RobotModel::coriolis` overload that hard-codes that
405 /// gravity vector.
406 pub fn coriolis(&self, state: &RobotState) -> [f64; 7] {
407 self.coriolis_q(
408 &state.q,
409 &state.dq,
410 &state.I_total,
411 state.m_total,
412 &state.F_x_Ctotal,
413 &DEFAULT_GRAVITY_EARTH,
414 )
415 }
416
417 /// Coriolis force vector, in Nm.
418 ///
419 /// Port of `franka::Model::coriolis(q, dq, I_total, m_total, F_x_Ctotal, gravity_earth)`.
420 #[allow(non_snake_case)]
421 pub fn coriolis_q(
422 &self,
423 q: &[f64; 7],
424 dq: &[f64; 7],
425 I_total: &[f64; 9],
426 m_total: f64,
427 F_x_Ctotal: &[f64; 3],
428 gravity_earth: &[f64; 3],
429 ) -> [f64; 7] {
430 self.backend
431 .coriolis(q, dq, I_total, m_total, F_x_Ctotal, gravity_earth)
432 }
433
434 /// Gravity torque vector, in Nm, using the state's `O_ddP_O` as Earth's gravity.
435 ///
436 /// Port of `franka::Model::gravity(const RobotState&)`.
437 pub fn gravity(&self, state: &RobotState) -> [f64; 7] {
438 self.gravity_q(&state.q, state.m_total, &state.F_x_Ctotal, &state.O_ddP_O)
439 }
440
441 /// Gravity torque vector, in Nm.
442 ///
443 /// Port of `franka::Model::gravity(q, m_total, F_x_Ctotal, gravity_earth)`.
444 #[allow(non_snake_case)]
445 pub fn gravity_q(
446 &self,
447 q: &[f64; 7],
448 m_total: f64,
449 F_x_Ctotal: &[f64; 3],
450 gravity_earth: &[f64; 3],
451 ) -> [f64; 7] {
452 self.backend.gravity(q, gravity_earth, m_total, F_x_Ctotal)
453 }
454}
455
456#[cfg(test)]
457mod tests;