Skip to main content

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;