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 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;