Skip to main content

franka/model/
native_backend.rs

1//! Native serial-chain rigid-body backend.
2//!
3//! This is the default [`RobotModelBackend`] implementation. It is a direct
4//! port of the behaviour of libfranka's `franka::RobotModel`
5//! (`src/robot_model.cpp`, identical in 0.20.4 and 0.21.2), which delegates to
6//! Pinocchio:
7//!
8//! | libfranka / Pinocchio | here |
9//! |---|---|
10//! | `pinocchio::forwardKinematics` + `data.oMi[i]` | `NativeBackend::forward_kinematics` |
11//! | `getJointJacobian(..., LOCAL)` / `getFrameJacobian(..., LOCAL)` | body Jacobian |
12//! | `... LOCAL_WORLD_ALIGNED` | zero Jacobian |
13//! | `pinocchio::crba` | `NativeBackend::crba` |
14//! | `pinocchio::rnea` / `computeGeneralizedGravity` | `NativeBackend::rnea` |
15//! | `RobotModel::updateInertiaIfNeeded` | `NativeBackend::body_inertias` |
16//!
17//! Everything after [`NativeBackend::from_urdf`] runs on fixed-size arrays and
18//! `nalgebra` static matrices, so no heap allocation happens on the query path.
19
20use nalgebra::{Matrix3, Vector3};
21use std::collections::HashMap;
22
23use crate::error::{FrankaError, FrankaResult};
24use crate::model::spatial::{Force, Motion, SpatialInertia, Transform};
25use crate::model::RobotModelBackend;
26
27/// Number of actuated joints of an FR3 / FER arm.
28pub(crate) const DOF: usize = 7;
29
30/// Name of the flange link, mirroring `RobotModel::kLastLinkName`.
31const LAST_LINK_NAME: &str = "link8";
32
33/// Serial-chain rigid-body model built from a URDF.
34#[derive(Debug, Clone)]
35pub struct NativeBackend {
36    /// Transform from joint `i-1`'s frame (or the world for `i == 0`) to the
37    /// frame of joint `i` before its own rotation is applied. Fixed joints in
38    /// between are folded into this placement, exactly like Pinocchio does.
39    joint_placement: [Transform; DOF],
40    /// Unit rotation axis of each joint, in that joint's own frame.
41    joint_axis: [Vector3<f64>; DOF],
42    /// Body inertia of the link driven by each joint, about that joint's frame
43    /// origin. Inertias of links behind fixed joints are merged in, exactly
44    /// like Pinocchio's URDF parser does.
45    link_inertia: [SpatialInertia; DOF],
46    /// Transform from joint 7's frame to the flange (`link8`) frame.
47    flange_placement: Transform,
48}
49
50/// Which joint a frame hangs off, and where it sits relative to that joint.
51struct FrameSpec {
52    /// Number of joints supporting the frame (`1..=DOF`).
53    support: usize,
54    /// Placement of the frame in the frame of joint `support`.
55    offset: Transform,
56}
57
58impl NativeBackend {
59    /// Builds the model from a URDF string.
60    ///
61    /// The chain from the URDF root to the `link8` frame must contain exactly
62    /// seven revolute (or continuous) joints; fixed joints anywhere along the
63    /// way are folded into the neighbouring placements and the inertias of the
64    /// links they carry are merged into the supporting joint, which is what
65    /// `pinocchio::urdf::buildModelFromXML` does for the model libfranka builds.
66    pub fn from_urdf(urdf: &str) -> FrankaResult<NativeBackend> {
67        let robot = urdf_rs::read_from_string(urdf)
68            .map_err(|e| FrankaError::Model(format!("libfranka model: cannot parse URDF: {e}")))?;
69
70        // child link name -> joint driving it
71        let mut joint_by_child: HashMap<&str, &urdf_rs::Joint> = HashMap::new();
72        for joint in &robot.joints {
73            if joint_by_child
74                .insert(joint.child.link.as_str(), joint)
75                .is_some()
76            {
77                return Err(FrankaError::Model(format!(
78                    "libfranka model: link '{}' is the child of more than one joint",
79                    joint.child.link
80                )));
81            }
82        }
83
84        if !robot.links.iter().any(|l| l.name == LAST_LINK_NAME) {
85            return Err(FrankaError::Model(format!(
86                "libfranka model: URDF has no '{LAST_LINK_NAME}' link"
87            )));
88        }
89
90        // Walk from the flange up to the root and collect the joint chain.
91        let mut chain: Vec<&urdf_rs::Joint> = Vec::new();
92        let mut cursor = LAST_LINK_NAME;
93        while let Some(joint) = joint_by_child.get(cursor) {
94            chain.push(joint);
95            cursor = joint.parent.link.as_str();
96            if chain.len() > robot.joints.len() {
97                return Err(FrankaError::Model(
98                    "libfranka model: URDF joint tree contains a cycle".to_string(),
99                ));
100            }
101        }
102        chain.reverse();
103
104        let mut joint_placement = [Transform::identity(); DOF];
105        let mut joint_axis = [Vector3::z(); DOF];
106        let mut movable_names: Vec<&str> = Vec::new();
107        let mut pending = Transform::identity();
108
109        for joint in &chain {
110            let origin = Transform::from_xyz_rpy(joint.origin.xyz.0, joint.origin.rpy.0);
111            match &joint.joint_type {
112                urdf_rs::JointType::Fixed => {
113                    pending = pending.compose(&origin);
114                }
115                urdf_rs::JointType::Revolute | urdf_rs::JointType::Continuous => {
116                    let index = movable_names.len();
117                    if index >= DOF {
118                        return Err(FrankaError::Model(format!(
119                            "libfranka model: URDF chain to '{LAST_LINK_NAME}' has more than {DOF} movable joints"
120                        )));
121                    }
122                    let axis = Vector3::new(
123                        joint.axis.xyz.0[0],
124                        joint.axis.xyz.0[1],
125                        joint.axis.xyz.0[2],
126                    );
127                    let norm = axis.norm();
128                    if norm < 1e-12 {
129                        return Err(FrankaError::Model(format!(
130                            "libfranka model: joint '{}' has a degenerate axis",
131                            joint.name
132                        )));
133                    }
134                    joint_placement[index] = pending.compose(&origin);
135                    joint_axis[index] = axis / norm;
136                    movable_names.push(joint.name.as_str());
137                    pending = Transform::identity();
138                }
139                other => {
140                    return Err(FrankaError::Model(format!(
141                        "libfranka model: joint '{}' has unsupported type {other:?}",
142                        joint.name
143                    )));
144                }
145            }
146        }
147
148        if movable_names.len() != DOF {
149            return Err(FrankaError::Model(format!(
150                "libfranka model: URDF chain to '{LAST_LINK_NAME}' has {} movable joints, expected {DOF}",
151                movable_names.len()
152            )));
153        }
154
155        // Whatever fixed joints remain after joint 7 place the flange frame.
156        let flange_placement = pending;
157
158        // Merge every link's inertia into the joint that supports it.
159        let mut link_inertia = [SpatialInertia::zero(); DOF];
160        for link in &robot.links {
161            if link.inertial.mass.value == 0.0 {
162                continue;
163            }
164            let Some((support, placement)) =
165                support_of(&joint_by_child, &movable_names, link.name.as_str())?
166            else {
167                // Rigidly attached to the world: Pinocchio folds it into the
168                // universe body, where it has no effect on a fixed-base model.
169                continue;
170            };
171            let inertial = &link.inertial;
172            let com_frame = Transform::from_xyz_rpy(inertial.origin.xyz.0, inertial.origin.rpy.0);
173            let i = &inertial.inertia;
174            let inertia_at_com = Matrix3::new(
175                i.ixx, i.ixy, i.ixz, i.ixy, i.iyy, i.iyz, i.ixz, i.iyz, i.izz,
176            );
177            // The URDF inertia is given in the inertial frame's axes; rotate it
178            // into the link frame before shifting it to the joint origin.
179            let rotated = com_frame.rotation * inertia_at_com * com_frame.rotation.transpose();
180            let body =
181                SpatialInertia::from_com(inertial.mass.value, &com_frame.translation, &rotated);
182            link_inertia[support] = link_inertia[support].add(&placement.act_inertia(&body));
183        }
184
185        Ok(NativeBackend {
186            joint_placement,
187            joint_axis,
188            link_inertia,
189            flange_placement,
190        })
191    }
192
193    /// Placement of the flange (`link8`) frame in joint 7's frame, as a
194    /// column-major 4x4 matrix.
195    ///
196    /// This is `pinocchio_model_.frames[last_link_frame_index_].placement`, the
197    /// transform `RobotModel::updateInertiaIfNeeded` uses to move the total
198    /// load from the flange frame into the last joint's frame.
199    pub fn flange_placement(&self) -> [f64; 16] {
200        self.flange_placement.to_column_major()
201    }
202
203    /// Forward kinematics.
204    ///
205    /// Returns the world pose of every joint frame (`data.oMi[1..=7]` in
206    /// Pinocchio terms, 0-indexed here) together with the parent-to-child
207    /// transform of each joint.
208    fn forward_kinematics(&self, q: &[f64; DOF]) -> ([Transform; DOF], [Transform; DOF]) {
209        let mut local = [Transform::identity(); DOF];
210        let mut world = [Transform::identity(); DOF];
211        for i in 0..DOF {
212            local[i] = self.joint_placement[i]
213                .compose(&Transform::from_axis_angle(&self.joint_axis[i], q[i]));
214            world[i] = if i == 0 {
215                local[0]
216            } else {
217                world[i - 1].compose(&local[i])
218            };
219        }
220        (world, local)
221    }
222
223    /// Resolves a `Frame` into the joint it hangs off and its offset.
224    fn frame_spec(&self, frame: FrameId, f_t_ee: &[f64; 16], ee_t_k: &[f64; 16]) -> FrameSpec {
225        match frame {
226            FrameId::Joint(index) => FrameSpec {
227                support: index,
228                offset: Transform::identity(),
229            },
230            FrameId::Flange => FrameSpec {
231                support: DOF,
232                offset: self.flange_placement,
233            },
234            FrameId::EndEffector => FrameSpec {
235                support: DOF,
236                offset: self
237                    .flange_placement
238                    .compose(&Transform::from_column_major(f_t_ee)),
239            },
240            FrameId::Stiffness => FrameSpec {
241                support: DOF,
242                offset: self
243                    .flange_placement
244                    .compose(&Transform::from_column_major(f_t_ee))
245                    .compose(&Transform::from_column_major(ee_t_k)),
246            },
247        }
248    }
249
250    /// World pose of a frame, column-major.
251    fn frame_pose(&self, q: &[f64; DOF], spec: &FrameSpec) -> [f64; 16] {
252        let (world, _) = self.forward_kinematics(q);
253        world[spec.support - 1]
254            .compose(&spec.offset)
255            .to_column_major()
256    }
257
258    /// Geometric Jacobian of a frame, 6x7 column-major with the linear rows first.
259    ///
260    /// `local == true` reproduces Pinocchio's `LOCAL` reference frame (the body
261    /// Jacobian), `local == false` its `LOCAL_WORLD_ALIGNED` (the zero
262    /// Jacobian). The intermediate quantities follow Pinocchio exactly: the
263    /// columns are first built in its `WORLD` convention (about the world
264    /// origin) and then moved to the target frame, so a slightly
265    /// non-orthonormal `F_T_EE` rotation block behaves the same way here as it
266    /// does in libfranka.
267    fn frame_jacobian(&self, q: &[f64; DOF], spec: &FrameSpec, local: bool) -> [f64; 42] {
268        let (world, _) = self.forward_kinematics(q);
269        let frame = world[spec.support - 1].compose(&spec.offset);
270        let rt = frame.rotation.transpose();
271
272        let mut out = [0.0f64; 42];
273        for j in 0..spec.support {
274            // Pinocchio's `WORLD` column: the joint's motion subspace mapped
275            // into the world frame, taken about the world origin.
276            let column = world[j].act_motion(&Motion::from_axis(&self.joint_axis[j], 1.0));
277            let angular_world = column.angular;
278            let shifted = column.linear - frame.translation.cross(&angular_world);
279            let (linear, angular) = if local {
280                (rt * shifted, rt * angular_world)
281            } else {
282                (shifted, angular_world)
283            };
284            out[j * 6] = linear.x;
285            out[j * 6 + 1] = linear.y;
286            out[j * 6 + 2] = linear.z;
287            out[j * 6 + 3] = angular.x;
288            out[j * 6 + 4] = angular.y;
289            out[j * 6 + 5] = angular.z;
290        }
291        out
292    }
293
294    /// The per-joint body inertias with the total load added to the last link.
295    ///
296    /// Port of `RobotModel::updateInertiaIfNeeded`: the load is expressed in
297    /// the flange frame and moved into the last joint's frame with the flange
298    /// frame's placement, i.e.
299    /// `initial_inertia + placement.act(Inertia(m_total, com, I_total))`.
300    fn body_inertias(
301        &self,
302        i_total: &[f64; 9],
303        m_total: f64,
304        f_x_ctotal: &[f64; 3],
305    ) -> [SpatialInertia; DOF] {
306        let mut inertias = self.link_inertia;
307        let load = SpatialInertia::from_com(
308            m_total,
309            &Vector3::new(f_x_ctotal[0], f_x_ctotal[1], f_x_ctotal[2]),
310            &Matrix3::from_column_slice(i_total),
311        );
312        inertias[DOF - 1] = inertias[DOF - 1].add(&self.flange_placement.act_inertia(&load));
313        inertias
314    }
315
316    /// Recursive Newton-Euler algorithm.
317    ///
318    /// Equivalent to `pinocchio::rnea(model, data, q, dq, ddq)` with
319    /// `model.gravity.linear() == gravity_earth`; with `dq == ddq == 0` it is
320    /// `pinocchio::computeGeneralizedGravity`.
321    fn rnea(
322        &self,
323        q: &[f64; DOF],
324        dq: &[f64; DOF],
325        ddq: &[f64; DOF],
326        gravity_earth: &Vector3<f64>,
327        inertias: &[SpatialInertia; DOF],
328    ) -> [f64; DOF] {
329        let (_, local) = self.forward_kinematics(q);
330
331        let mut velocity = [Motion::zero(); DOF];
332        let mut acceleration = [Motion::zero(); DOF];
333        let mut force = [Force::zero(); DOF];
334
335        // Pinocchio models gravity as a base acceleration of -g.
336        let base_acceleration = Motion::from_linear(-gravity_earth);
337
338        for i in 0..DOF {
339            let parent_velocity = if i == 0 {
340                Motion::zero()
341            } else {
342                velocity[i - 1]
343            };
344            let parent_acceleration = if i == 0 {
345                base_acceleration
346            } else {
347                acceleration[i - 1]
348            };
349
350            let joint_velocity = Motion::from_axis(&self.joint_axis[i], dq[i]);
351            let joint_acceleration = Motion::from_axis(&self.joint_axis[i], ddq[i]);
352
353            let v = local[i]
354                .act_inv_motion(&parent_velocity)
355                .add(&joint_velocity);
356            let a = local[i]
357                .act_inv_motion(&parent_acceleration)
358                .add(&joint_acceleration)
359                .add(&v.cross_motion(&joint_velocity));
360
361            velocity[i] = v;
362            acceleration[i] = a;
363            force[i] = inertias[i]
364                .apply(&a)
365                .add(&v.cross_force(&inertias[i].apply(&v)));
366        }
367
368        let mut tau = [0.0f64; DOF];
369        for i in (0..DOF).rev() {
370            tau[i] = self.joint_axis[i].dot(&force[i].angular);
371            if i > 0 {
372                force[i - 1] = force[i - 1].add(&local[i].act_force(&force[i]));
373            }
374        }
375        tau
376    }
377
378    /// Composite rigid body algorithm, giving the 7x7 joint-space inertia
379    /// matrix column-major. Equivalent to `pinocchio::crba` followed by
380    /// libfranka's mirroring of the strictly-lower triangle.
381    fn crba(&self, q: &[f64; DOF], inertias: &[SpatialInertia; DOF]) -> [f64; DOF * DOF] {
382        let (_, local) = self.forward_kinematics(q);
383
384        let mut composite = *inertias;
385        for i in (1..DOF).rev() {
386            composite[i - 1] = composite[i - 1].add(&local[i].act_inertia(&composite[i]));
387        }
388
389        let mut mass = [0.0f64; DOF * DOF];
390        for i in 0..DOF {
391            let mut f = composite[i].apply(&Motion::from_axis(&self.joint_axis[i], 1.0));
392            mass[i * DOF + i] = self.joint_axis[i].dot(&f.angular);
393            let mut j = i;
394            while j > 0 {
395                f = local[j].act_force(&f);
396                j -= 1;
397                let value = self.joint_axis[j].dot(&f.angular);
398                mass[i * DOF + j] = value;
399                mass[j * DOF + i] = value;
400            }
401        }
402        mass
403    }
404}
405
406/// Internal frame selector, mirroring `franka::Frame`.
407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408enum FrameId {
409    /// `1..=DOF`
410    Joint(usize),
411    Flange,
412    EndEffector,
413    Stiffness,
414}
415
416/// Finds the movable joint a link is rigidly attached to, and the link frame's
417/// placement in that joint's frame. `Ok(None)` means the link is rigidly
418/// attached to the world.
419fn support_of(
420    joint_by_child: &HashMap<&str, &urdf_rs::Joint>,
421    movable_names: &[&str],
422    link: &str,
423) -> FrankaResult<Option<(usize, Transform)>> {
424    let mut placement = Transform::identity();
425    let mut cursor = link;
426    let mut steps = 0usize;
427    while let Some(joint) = joint_by_child.get(cursor) {
428        steps += 1;
429        if steps > joint_by_child.len() + 1 {
430            return Err(FrankaError::Model(
431                "libfranka model: URDF joint tree contains a cycle".to_string(),
432            ));
433        }
434        if let Some(index) = movable_names.iter().position(|n| *n == joint.name.as_str()) {
435            return Ok(Some((index, placement)));
436        }
437        if joint.joint_type != urdf_rs::JointType::Fixed {
438            // A movable joint outside the chain to the flange: the arm model
439            // libfranka builds has none, so refuse rather than silently drop it.
440            return Err(FrankaError::Model(format!(
441                "libfranka model: link '{link}' is behind movable joint '{}', which is not part of the arm chain",
442                joint.name
443            )));
444        }
445        placement =
446            Transform::from_xyz_rpy(joint.origin.xyz.0, joint.origin.rpy.0).compose(&placement);
447        cursor = joint.parent.link.as_str();
448    }
449    Ok(None)
450}
451
452const IDENTITY_16: [f64; 16] = [
453    1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
454];
455
456impl RobotModelBackend for NativeBackend {
457    fn coriolis(
458        &self,
459        q: &[f64; DOF],
460        dq: &[f64; DOF],
461        i_total: &[f64; 9],
462        m_total: f64,
463        f_x_ctotal: &[f64; 3],
464        gravity_earth: &[f64; 3],
465    ) -> [f64; DOF] {
466        let inertias = self.body_inertias(i_total, m_total, f_x_ctotal);
467        let g = Vector3::new(gravity_earth[0], gravity_earth[1], gravity_earth[2]);
468        let zero = [0.0f64; DOF];
469        let full = self.rnea(q, dq, &zero, &g, &inertias);
470        let gravity = self.rnea(q, &zero, &zero, &g, &inertias);
471        let mut out = [0.0f64; DOF];
472        for i in 0..DOF {
473            out[i] = full[i] - gravity[i];
474        }
475        out
476    }
477
478    fn gravity(
479        &self,
480        q: &[f64; DOF],
481        gravity_earth: &[f64; 3],
482        m_total: f64,
483        f_x_ctotal: &[f64; 3],
484    ) -> [f64; DOF] {
485        // `RobotModel::gravity` installs the load with a *zero* rotational
486        // inertia; the generalized gravity does not depend on it either way.
487        let inertias = self.body_inertias(&[0.0; 9], m_total, f_x_ctotal);
488        let g = Vector3::new(gravity_earth[0], gravity_earth[1], gravity_earth[2]);
489        let zero = [0.0f64; DOF];
490        self.rnea(q, &zero, &zero, &g, &inertias)
491    }
492
493    fn mass(
494        &self,
495        q: &[f64; DOF],
496        i_total: &[f64; 9],
497        m_total: f64,
498        f_x_ctotal: &[f64; 3],
499    ) -> [f64; DOF * DOF] {
500        let inertias = self.body_inertias(i_total, m_total, f_x_ctotal);
501        self.crba(q, &inertias)
502    }
503
504    fn pose(&self, q: &[f64; DOF], joint_index: usize) -> [f64; 16] {
505        debug_assert!((1..=DOF).contains(&joint_index));
506        let spec = self.frame_spec(
507            FrameId::Joint(joint_index.clamp(1, DOF)),
508            &IDENTITY_16,
509            &IDENTITY_16,
510        );
511        self.frame_pose(q, &spec)
512    }
513
514    fn pose_flange(&self, q: &[f64; DOF]) -> [f64; 16] {
515        let spec = self.frame_spec(FrameId::Flange, &IDENTITY_16, &IDENTITY_16);
516        self.frame_pose(q, &spec)
517    }
518
519    fn pose_ee(&self, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 16] {
520        let spec = self.frame_spec(FrameId::EndEffector, f_t_ee, &IDENTITY_16);
521        self.frame_pose(q, &spec)
522    }
523
524    fn pose_stiffness(&self, q: &[f64; DOF], f_t_ee: &[f64; 16], ee_t_k: &[f64; 16]) -> [f64; 16] {
525        let spec = self.frame_spec(FrameId::Stiffness, f_t_ee, ee_t_k);
526        self.frame_pose(q, &spec)
527    }
528
529    fn body_jacobian(&self, q: &[f64; DOF], joint_index: usize) -> [f64; 42] {
530        debug_assert!((1..=DOF).contains(&joint_index));
531        let spec = self.frame_spec(
532            FrameId::Joint(joint_index.clamp(1, DOF)),
533            &IDENTITY_16,
534            &IDENTITY_16,
535        );
536        self.frame_jacobian(q, &spec, true)
537    }
538
539    fn body_jacobian_flange(&self, q: &[f64; DOF]) -> [f64; 42] {
540        let spec = self.frame_spec(FrameId::Flange, &IDENTITY_16, &IDENTITY_16);
541        self.frame_jacobian(q, &spec, true)
542    }
543
544    fn body_jacobian_ee(&self, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 42] {
545        let spec = self.frame_spec(FrameId::EndEffector, f_t_ee, &IDENTITY_16);
546        self.frame_jacobian(q, &spec, true)
547    }
548
549    fn body_jacobian_stiffness(
550        &self,
551        q: &[f64; DOF],
552        f_t_ee: &[f64; 16],
553        ee_t_k: &[f64; 16],
554    ) -> [f64; 42] {
555        let spec = self.frame_spec(FrameId::Stiffness, f_t_ee, ee_t_k);
556        self.frame_jacobian(q, &spec, true)
557    }
558
559    fn zero_jacobian(&self, q: &[f64; DOF], joint_index: usize) -> [f64; 42] {
560        debug_assert!((1..=DOF).contains(&joint_index));
561        let spec = self.frame_spec(
562            FrameId::Joint(joint_index.clamp(1, DOF)),
563            &IDENTITY_16,
564            &IDENTITY_16,
565        );
566        self.frame_jacobian(q, &spec, false)
567    }
568
569    fn zero_jacobian_flange(&self, q: &[f64; DOF]) -> [f64; 42] {
570        let spec = self.frame_spec(FrameId::Flange, &IDENTITY_16, &IDENTITY_16);
571        self.frame_jacobian(q, &spec, false)
572    }
573
574    fn zero_jacobian_ee(&self, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 42] {
575        let spec = self.frame_spec(FrameId::EndEffector, f_t_ee, &IDENTITY_16);
576        self.frame_jacobian(q, &spec, false)
577    }
578
579    fn zero_jacobian_stiffness(
580        &self,
581        q: &[f64; DOF],
582        f_t_ee: &[f64; 16],
583        ee_t_k: &[f64; 16],
584    ) -> [f64; 42] {
585        let spec = self.frame_spec(FrameId::Stiffness, f_t_ee, ee_t_k);
586        self.frame_jacobian(q, &spec, false)
587    }
588}