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