Skip to main content

franka/model/so_backend/
mod.rs

1//! [`RobotModelBackend`] over the robot-served `libfcimodels` shared object.
2//!
3//! On FCI v5 (Franka Emika Robot, FER, libfranka 0.9.2) the robot does not publish a
4//! URDF. It serves a *compiled* model instead: `Robot::loadModel` issues a
5//! `LoadModelLibrary` command, libfranka writes the returned bytes to a
6//! temporary file, `dlopen`s it and calls plain C functions out of it
7//! (`src/library_downloader.cpp`, `src/library_loader.h`, `src/model_library.h`,
8//! `src/libfcimodels.h`, `src/model.cpp`, all of 0.9.2).
9//!
10//! This module is the Rust equivalent of `franka::ModelLibrary` +
11//! `franka::LibraryLoader`: [`SoModelBackend::open`] / [`SoModelBackend::from_bytes`]
12//! bind all thirty exported symbols once, up front, exactly like
13//! `ModelLibrary::ModelLibrary` does in its initialiser list, and the
14//! [`RobotModelBackend`] impl dispatches the ten [`crate::model::Frame`] values
15//! onto them the way `franka::Model` does in `model.cpp`.
16//!
17//! # Symbols and frames
18//!
19//! `libfcimodels.h` (0.9.2) declares, all `extern "C"` and all column-major:
20//!
21//! | symbol | signature | used for |
22//! |---|---|---|
23//! | `O_T_J1 .. O_T_J8` | `(const double q[7], double out[16])` | pose of joints 1..7 and the flange |
24//! | `O_T_J9` | `(const double q[7], const double F_T_EE[16], double out[16])` | pose of the end-effector / stiffness frame |
25//! | `Ji_J_J1` | `(double out[42])` | body Jacobian of joint 1 — **no `q`** |
26//! | `Ji_J_J2 .. Ji_J_J8` | `(const double q[7], double out[42])` | body Jacobian of joints 2..7 and the flange |
27//! | `Ji_J_J9` | `(const double q[7], const double F_T_EE[16], double out[42])` | body Jacobian of the end-effector / stiffness frame |
28//! | `O_J_J1` | `(double out[42])` | zero Jacobian of joint 1 — **no `q`** |
29//! | `O_J_J2 .. O_J_J8` | `(const double q[7], double out[42])` | zero Jacobian of joints 2..7 and the flange |
30//! | `O_J_J9` | `(const double q[7], const double F_T_EE[16], double out[42])` | zero Jacobian of the end-effector / stiffness frame |
31//! | `M_NE` | `(const double q[7], const double I_load[9], double m_load, const double F_x_Cload[3], double out[49])` | mass matrix |
32//! | `c_NE` | `(const double q[7], const double dq[7], const double I_load[9], double m_load, const double F_x_Cload[3], double out[7])` | Coriolis vector |
33//! | `g_NE` | `(const double q[7], const double g_earth[3], double m_load, const double F_x_Cload[3], double out[7])` | gravity vector |
34//!
35//! The frame mapping is `model.cpp`'s: `Joint1..Joint7` are `J1..J7`, `Flange`
36//! is `J8`, `EndEffector` is `J9` evaluated with `F_T_EE`, and `Stiffness` is
37//! `J9` evaluated with the column-major product `F_T_EE * EE_T_K` — libfranka
38//! forms exactly that product with
39//! `Eigen::Matrix4d(F_T_EE.data()) * Eigen::Matrix4d(EE_T_K.data())`, and Eigen
40//! is column-major by default.
41//!
42//! # Load parameters
43//!
44//! The `_load` arguments are named after the *payload* in the C header, but
45//! `franka::Model::mass/coriolis/gravity` pass `robot_state.I_total`,
46//! `robot_state.m_total` and `robot_state.F_x_Ctotal`, i.e. the **combined**
47//! end-effector-plus-payload body. This backend therefore forwards the trait's
48//! `i_total / m_total / f_x_ctotal` unchanged, which is what libfranka does.
49//!
50//! # Gravity and Coriolis
51//!
52//! `c_NE` takes no gravity vector: the FCI v5 `franka::Model::coriolis`
53//! (`model.cpp`, 0.9.2) has no `gravity_earth` parameter at all, unlike the FCI
54//! v10 `franka::RobotModel::coriolis` this crate's trait is shaped after.
55//! [`RobotModelBackend::coriolis`]'s `gravity_earth` argument is consequently
56//! **ignored** here; the Coriolis vector the FER's own model returns is
57//! gravity-free by construction. `g_NE` does take `g_earth` and receives the
58//! trait's `gravity_earth` verbatim.
59//!
60//! # Platform
61//!
62//! The robot serves a native shared object, so the process that loads it must
63//! match the `(architecture, system)` pair it asked for. In practice that is
64//! `libfcimodels_x64.so` on x86-64 Linux; see
65//! [`crate::model::model_library::load_from_robot`].
66
67mod symbols;
68mod temp_file;
69
70use symbols::{symbol, ConstJacobianFn, JacobianEeFn, JacobianFn, PoseEeFn, PoseFn, Symbols};
71use temp_file::{write_temp_library, TempLibraryFile};
72
73use std::path::Path;
74
75use libloading::Library;
76
77use crate::error::{FrankaError, FrankaResult};
78use crate::model::native_backend::DOF;
79use crate::model::RobotModelBackend;
80
81/// [`RobotModelBackend`] backed by the robot's own `libfcimodels` shared object.
82///
83/// Obtain one from [`crate::model::model_library::load_from_robot`] (online) or
84/// from [`SoModelBackend::open`] / [`SoModelBackend::from_bytes`] (offline,
85/// e.g. from a captured library).
86pub struct SoModelBackend {
87    /// Resolved function pointers into `library`. Declared first so they are
88    /// dropped (a no-op — they are plain `fn` pointers) before the `dlclose`.
89    symbols: Symbols,
90    /// The `dlopen` handle. Never read after construction, but dropping it
91    /// `dlclose`s the library, which invalidates every pointer in `symbols`;
92    /// nothing may outlive it.
93    _library: Library,
94    /// A handle keeping `libm.so.6` mapped in the process's *global* symbol
95    /// scope, if one was opened; see [`open_libm_global`]. `libloading::Library::new`
96    /// uses `RTLD_LAZY`, so `_library`'s `sin`/`cos`/`sincos` references are not
97    /// resolved until first called — this handle must therefore outlive every
98    /// call through `symbols`, not just the `dlopen`. `None` when `libm.so.6`
99    /// could not be opened; the model library load is then left to fail (or
100    /// succeed, if libm was already global by some other path) on its own.
101    #[cfg(unix)]
102    _libm: Option<libloading::os::unix::Library>,
103    /// The temporary file the library was written to, if this backend owns one.
104    /// Declared last so the unlink happens after the `dlclose`.
105    _temp_file: Option<TempLibraryFile>,
106}
107
108impl std::fmt::Debug for SoModelBackend {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("SoModelBackend").finish_non_exhaustive()
111    }
112}
113
114// `libloading::Library` wraps a `dlopen` handle, which is `Send + Sync` on every platform
115// this crate supports; the resolved symbols are bare `fn` pointers, which are `Send + Sync`
116// unconditionally. The generated `libfcimodels` code is pure arithmetic on caller-provided
117// buffers with no mutable global state, so concurrent calls do not race. libfranka relies on
118// the same property: `franka::Model` is handed out by value and used from control threads.
119const _: () = {
120    const fn assert_send_sync<T: Send + Sync>() {}
121    assert_send_sync::<Library>();
122    #[cfg(unix)]
123    assert_send_sync::<libloading::os::unix::Library>();
124};
125
126/// Opens `libm.so.6` into the process's *global* symbol scope, so a later
127/// `dlopen` of a library with no `DT_NEEDED` of its own can still resolve
128/// libm symbols against it.
129///
130/// # Why this exists
131///
132/// The robot-served `libfcimodels_x64.so` calls `sin`, `cos` and `sincos` but
133/// carries **no `DT_NEEDED` entries at all** (`readelf -d` on a captured copy
134/// prints nothing under `NEEDED`), so the dynamic linker can only resolve
135/// those symbols from the *global* scope of the process that `dlopen`s it —
136/// confirmed by `nm -D --undefined-only`, which lists `sin`, `cos` and
137/// `sincos` as undefined. libfranka's C++ clients get libm for free because
138/// libstdc++ itself has a `DT_NEEDED` on it, loaded into the main
139/// executable's global scope by the dynamic linker at process start. A Rust
140/// binary has no such guarantee: `libloading::Library::new` opens the model
141/// library with `RTLD_LAZY | RTLD_LOCAL`
142/// (`libloading::os::unix::Library::new`), and if the binary itself never
143/// references an libm symbol that survives `--as-needed`, `libm.so.6` may
144/// not be mapped at all, or may be mapped without its symbols published
145/// globally — either way the first call into the model library fails with
146/// `undefined symbol: sincos`.
147///
148/// Calling this before [`SoModelBackend::load`] opens `libm.so.6` itself with
149/// `RTLD_NOW | RTLD_GLOBAL`, publishing its symbols into the scope every
150/// subsequent `dlopen` can see — the same effect libstdc++'s `DT_NEEDED`
151/// has — independent of whether this binary happens to use libm anywhere
152/// else. If `libm.so.6` is already loaded (the common case, since most
153/// binaries pull it in transitively), `dlopen` just bumps its reference
154/// count and, per POSIX, promotes it to global scope if it was not already.
155///
156/// # Fallback
157///
158/// If `libm.so.6` cannot be opened (an unusual libc, a fully static build,
159/// ...) this returns `None` rather than panicking; [`SoModelBackend::load`]
160/// continues regardless; and lets the model library open — or the first call
161/// through it — fail with its own ordinary error if libm truly is not
162/// reachable some other way.
163#[cfg(unix)]
164fn open_libm_global() -> Option<libloading::os::unix::Library> {
165    use libloading::os::unix::{Library as UnixLibrary, RTLD_GLOBAL, RTLD_NOW};
166
167    // SAFETY: `libm.so.6` is glibc's math library; opening it runs no code the caller
168    // does not already trust the C runtime to run, and publishing its already-defined
169    // symbols into the global scope cannot invalidate anything else that is loaded.
170    unsafe { UnixLibrary::open(Some("libm.so.6"), RTLD_NOW | RTLD_GLOBAL) }.ok()
171}
172
173impl SoModelBackend {
174    /// Loads a `libfcimodels` shared object from an existing file.
175    ///
176    /// The file is *not* removed when the backend is dropped; use
177    /// [`SoModelBackend::from_bytes`] for a downloaded library.
178    ///
179    /// Port of `franka::LibraryLoader` plus `franka::ModelLibrary`'s symbol
180    /// binding (`src/library_loader.cpp`, `src/model_library.cpp`, 0.9.2).
181    ///
182    /// # Safety
183    ///
184    /// This `dlopen`s `path`, which runs the shared object's `DT_INIT`
185    /// constructors and then calls function pointers resolved out of it — i.e.
186    /// it executes the file's code in this process, with no sandbox and no
187    /// validation of its contents.
188    ///
189    /// The caller asserts that `path` is a **trusted** `libfcimodels` build for
190    /// the current platform: it came from a source the caller is willing to run
191    /// arbitrary native code from (a Franka control unit over the FCI command
192    /// socket, or a library captured from one and stored somewhere only trusted
193    /// principals can write), the file cannot be swapped between this call and
194    /// the load, and its thirty exported entry points have the signatures
195    /// `libfcimodels.h` of libfranka 0.9.2 declares. Loading anything else is
196    /// undefined behaviour.
197    ///
198    /// # Errors
199    ///
200    /// [`FrankaError::Model`] when the file cannot be `dlopen`ed (wrong
201    /// architecture, missing dependency, not a shared object) or when any of
202    /// the thirty expected symbols is absent — libfranka reports the same two
203    /// conditions as `ModelException("libfranka: Cannot load model library: ...")`
204    /// and `ModelException("libfranka: Symbol cannot be found: ...")`.
205    pub unsafe fn open(path: &Path) -> FrankaResult<SoModelBackend> {
206        // SAFETY: delegated to this function's own contract — the caller asserts `path`
207        // is a trusted `libfcimodels` build for this platform.
208        unsafe { SoModelBackend::load(path, None) }
209    }
210
211    /// Writes `bytes` to a fresh, `0600`, uniquely named file under
212    /// [`std::env::temp_dir`], loads it, and removes the file when the returned
213    /// backend is dropped.
214    ///
215    /// This is what libfranka does with the `LoadModelLibrary` response
216    /// (`LibraryDownloader::LibraryDownloader`, which names the file with
217    /// `Poco::TemporaryFile::tempName()` and unlinks it in its destructor).
218    ///
219    /// # Safety
220    ///
221    /// `bytes` are written to disk and `dlopen`ed, so this executes them in
222    /// this process; see [`SoModelBackend::open`] for what that means. The
223    /// caller asserts that `bytes` are a **trusted** `libfcimodels` build for
224    /// the current platform — in the online path, that trust is the FCI peer
225    /// itself, which already commands the arm.
226    ///
227    /// # Errors
228    ///
229    /// [`FrankaError::Model`] when the file cannot be created or written
230    /// (libfranka: `"libfranka: Cannot save model library."`), or for the
231    /// reasons listed on [`SoModelBackend::open`].
232    pub unsafe fn from_bytes(bytes: &[u8]) -> FrankaResult<SoModelBackend> {
233        let temp_file = write_temp_library(bytes)?;
234        let path = temp_file.path.clone();
235        // SAFETY: delegated to this function's own contract — the caller asserts `bytes`
236        // are a trusted `libfcimodels` build for this platform.
237        unsafe { SoModelBackend::load(&path, Some(temp_file)) }
238    }
239
240    /// # Safety
241    ///
242    /// See [`SoModelBackend::open`]: `path` must name a trusted `libfcimodels`
243    /// build for the current platform.
244    unsafe fn load(
245        path: &Path,
246        temp_file: Option<TempLibraryFile>,
247    ) -> FrankaResult<SoModelBackend> {
248        // Publish libm's symbols into the global scope before the model library is
249        // opened, so its unresolved `sin`/`cos`/`sincos` references can find them even
250        // though it has no `DT_NEEDED` of its own; see `open_libm_global`. Best effort:
251        // if this fails, fall through and let the model library open — or its first
252        // call — report its own error.
253        #[cfg(unix)]
254        let libm = open_libm_global();
255
256        // SAFETY: `dlopen` runs the shared object's initialisers, which is arbitrary code.
257        // Discharged by this function's contract, which `open` and `from_bytes` re-export
258        // as their own `# Safety` sections: the caller asserts the file is a trusted
259        // `libfcimodels` build for this platform. In the online path the bytes come from
260        // the robot over the FCI command socket, which is exactly the trust boundary
261        // libfranka's `LibraryLoader` sits on.
262        let library = unsafe { Library::new(path) }.map_err(|e| {
263            FrankaError::Model(format!("libfranka: Cannot load model library: {e}"))
264        })?;
265
266        // SAFETY: every name below is declared in `src/libfcimodels.h` with the signature
267        // the corresponding Rust type alias spells out, and the pointers are stored next to
268        // the `Library` that owns them, so they cannot outlive it.
269        let symbols = unsafe {
270            Symbols {
271                o_t_j: [
272                    symbol(&library, "O_T_J1")?,
273                    symbol(&library, "O_T_J2")?,
274                    symbol(&library, "O_T_J3")?,
275                    symbol(&library, "O_T_J4")?,
276                    symbol(&library, "O_T_J5")?,
277                    symbol(&library, "O_T_J6")?,
278                    symbol(&library, "O_T_J7")?,
279                    symbol(&library, "O_T_J8")?,
280                ],
281                o_t_j9: symbol(&library, "O_T_J9")?,
282                ji_j_j1: symbol(&library, "Ji_J_J1")?,
283                ji_j: [
284                    symbol(&library, "Ji_J_J2")?,
285                    symbol(&library, "Ji_J_J3")?,
286                    symbol(&library, "Ji_J_J4")?,
287                    symbol(&library, "Ji_J_J5")?,
288                    symbol(&library, "Ji_J_J6")?,
289                    symbol(&library, "Ji_J_J7")?,
290                    symbol(&library, "Ji_J_J8")?,
291                ],
292                ji_j_j9: symbol(&library, "Ji_J_J9")?,
293                o_j_j1: symbol(&library, "O_J_J1")?,
294                o_j: [
295                    symbol(&library, "O_J_J2")?,
296                    symbol(&library, "O_J_J3")?,
297                    symbol(&library, "O_J_J4")?,
298                    symbol(&library, "O_J_J5")?,
299                    symbol(&library, "O_J_J6")?,
300                    symbol(&library, "O_J_J7")?,
301                    symbol(&library, "O_J_J8")?,
302                ],
303                o_j_j9: symbol(&library, "O_J_J9")?,
304                mass: symbol(&library, "M_NE")?,
305                coriolis: symbol(&library, "c_NE")?,
306                gravity: symbol(&library, "g_NE")?,
307            }
308        };
309
310        Ok(SoModelBackend {
311            symbols,
312            _library: library,
313            #[cfg(unix)]
314            _libm: libm,
315            _temp_file: temp_file,
316        })
317    }
318
319    /// `O_T_J1 .. O_T_J8`.
320    fn call_pose(f: PoseFn, q: &[f64; DOF]) -> [f64; 16] {
321        let mut out = [0.0; 16];
322        // SAFETY: `f` has type `PoseFn`, so it was resolved as a
323        // `(const double[7], double[16])` entry point of the model library; `q` and `out`
324        // are live arrays of exactly those lengths. The library holds no state across calls.
325        unsafe { f(q, &mut out) };
326        out
327    }
328
329    /// `O_T_J9`, which takes the flange-to-frame offset.
330    fn call_pose_ee(f: PoseEeFn, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 16] {
331        let mut out = [0.0; 16];
332        // SAFETY: `f` has type `PoseEeFn`, i.e. a
333        // `(const double[7], const double[16], double[16])` entry point; `q`, `f_t_ee` and
334        // `out` are live arrays of exactly those lengths.
335        unsafe { f(q, f_t_ee, &mut out) };
336        out
337    }
338
339    /// `Ji_J_J2 .. Ji_J_J8` and `O_J_J2 .. O_J_J8`.
340    fn call_jacobian(f: JacobianFn, q: &[f64; DOF]) -> [f64; 42] {
341        let mut out = [0.0; 42];
342        // SAFETY: `f` has type `JacobianFn`, i.e. a `(const double[7], double[42])` entry
343        // point; `q` and `out` are live arrays of exactly those lengths.
344        unsafe { f(q, &mut out) };
345        out
346    }
347
348    /// `Ji_J_J9` / `O_J_J9`, which take the flange-to-frame offset.
349    fn call_jacobian_ee(f: JacobianEeFn, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 42] {
350        let mut out = [0.0; 42];
351        // SAFETY: `f` has type `JacobianEeFn`, i.e. a
352        // `(const double[7], const double[16], double[42])` entry point; `q`, `f_t_ee` and
353        // `out` are live arrays of exactly those lengths.
354        unsafe { f(q, f_t_ee, &mut out) };
355        out
356    }
357
358    /// `Ji_J_J1` / `O_J_J1`, which are constant and take no `q`.
359    fn call_const_jacobian(f: ConstJacobianFn) -> [f64; 42] {
360        let mut out = [0.0; 42];
361        // SAFETY: `f` has type `ConstJacobianFn`, i.e. a `(double[42])` entry point; `out`
362        // is a live `[f64; 42]`.
363        unsafe { f(&mut out) };
364        out
365    }
366}
367
368/// Column-major 4x4 product `a * b`.
369///
370/// The exact operation `model.cpp` performs for [`crate::model::Frame::Stiffness`]:
371/// `Eigen::Matrix4d(F_T_EE.data()) * Eigen::Matrix4d(EE_T_K.data())`, with Eigen's
372/// default column-major storage on both sides.
373fn mat4_mul(a: &[f64; 16], b: &[f64; 16]) -> [f64; 16] {
374    let mut out = [0.0; 16];
375    for column in 0..4 {
376        for row in 0..4 {
377            let mut sum = 0.0;
378            for k in 0..4 {
379                sum += a[row + 4 * k] * b[k + 4 * column];
380            }
381            out[row + 4 * column] = sum;
382        }
383    }
384    out
385}
386
387impl RobotModelBackend for SoModelBackend {
388    /// `c_NE(q, dq, I_total, m_total, F_x_Ctotal)`.
389    ///
390    /// `gravity_earth` is **ignored**: the FCI v5 model library's Coriolis
391    /// entry point takes no gravity vector and `franka::Model::coriolis`
392    /// (0.9.2) has no such parameter either. See the module documentation.
393    fn coriolis(
394        &self,
395        q: &[f64; DOF],
396        dq: &[f64; DOF],
397        i_total: &[f64; 9],
398        m_total: f64,
399        f_x_ctotal: &[f64; 3],
400        _gravity_earth: &[f64; 3],
401    ) -> [f64; DOF] {
402        let mut out = [0.0; DOF];
403        // SAFETY: `c_NE` was resolved with the signature `CoriolisFn` spells out; all five
404        // inputs are live arrays of the declared lengths and `out` is a live `[f64; 7]`.
405        unsafe { (self.symbols.coriolis)(q, dq, i_total, m_total, f_x_ctotal, &mut out) };
406        out
407    }
408
409    /// `g_NE(q, gravity_earth, m_total, F_x_Ctotal)`.
410    fn gravity(
411        &self,
412        q: &[f64; DOF],
413        gravity_earth: &[f64; 3],
414        m_total: f64,
415        f_x_ctotal: &[f64; 3],
416    ) -> [f64; DOF] {
417        let mut out = [0.0; DOF];
418        // SAFETY: `g_NE` was resolved with the signature `GravityFn` spells out; all inputs
419        // are live arrays of the declared lengths and `out` is a live `[f64; 7]`.
420        unsafe { (self.symbols.gravity)(q, gravity_earth, m_total, f_x_ctotal, &mut out) };
421        out
422    }
423
424    /// `M_NE(q, I_total, m_total, F_x_Ctotal)`.
425    fn mass(
426        &self,
427        q: &[f64; DOF],
428        i_total: &[f64; 9],
429        m_total: f64,
430        f_x_ctotal: &[f64; 3],
431    ) -> [f64; 49] {
432        let mut out = [0.0; 49];
433        // SAFETY: `M_NE` was resolved with the signature `MassFn` spells out; all inputs are
434        // live arrays of the declared lengths and `out` is a live `[f64; 49]`.
435        unsafe { (self.symbols.mass)(q, i_total, m_total, f_x_ctotal, &mut out) };
436        out
437    }
438
439    fn pose(&self, q: &[f64; DOF], joint_index: usize) -> [f64; 16] {
440        debug_assert!((1..=DOF).contains(&joint_index));
441        SoModelBackend::call_pose(self.symbols.o_t_j[joint_index.clamp(1, DOF) - 1], q)
442    }
443
444    fn pose_flange(&self, q: &[f64; DOF]) -> [f64; 16] {
445        SoModelBackend::call_pose(self.symbols.o_t_j[7], q)
446    }
447
448    fn pose_ee(&self, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 16] {
449        SoModelBackend::call_pose_ee(self.symbols.o_t_j9, q, f_t_ee)
450    }
451
452    fn pose_stiffness(&self, q: &[f64; DOF], f_t_ee: &[f64; 16], ee_t_k: &[f64; 16]) -> [f64; 16] {
453        SoModelBackend::call_pose_ee(self.symbols.o_t_j9, q, &mat4_mul(f_t_ee, ee_t_k))
454    }
455
456    fn body_jacobian(&self, q: &[f64; DOF], joint_index: usize) -> [f64; 42] {
457        debug_assert!((1..=DOF).contains(&joint_index));
458        match joint_index.clamp(1, DOF) {
459            1 => SoModelBackend::call_const_jacobian(self.symbols.ji_j_j1),
460            joint => SoModelBackend::call_jacobian(self.symbols.ji_j[joint - 2], q),
461        }
462    }
463
464    fn body_jacobian_flange(&self, q: &[f64; DOF]) -> [f64; 42] {
465        SoModelBackend::call_jacobian(self.symbols.ji_j[6], q)
466    }
467
468    fn body_jacobian_ee(&self, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 42] {
469        SoModelBackend::call_jacobian_ee(self.symbols.ji_j_j9, q, f_t_ee)
470    }
471
472    fn body_jacobian_stiffness(
473        &self,
474        q: &[f64; DOF],
475        f_t_ee: &[f64; 16],
476        ee_t_k: &[f64; 16],
477    ) -> [f64; 42] {
478        SoModelBackend::call_jacobian_ee(self.symbols.ji_j_j9, q, &mat4_mul(f_t_ee, ee_t_k))
479    }
480
481    fn zero_jacobian(&self, q: &[f64; DOF], joint_index: usize) -> [f64; 42] {
482        debug_assert!((1..=DOF).contains(&joint_index));
483        match joint_index.clamp(1, DOF) {
484            1 => SoModelBackend::call_const_jacobian(self.symbols.o_j_j1),
485            joint => SoModelBackend::call_jacobian(self.symbols.o_j[joint - 2], q),
486        }
487    }
488
489    fn zero_jacobian_flange(&self, q: &[f64; DOF]) -> [f64; 42] {
490        SoModelBackend::call_jacobian(self.symbols.o_j[6], q)
491    }
492
493    fn zero_jacobian_ee(&self, q: &[f64; DOF], f_t_ee: &[f64; 16]) -> [f64; 42] {
494        SoModelBackend::call_jacobian_ee(self.symbols.o_j_j9, q, f_t_ee)
495    }
496
497    fn zero_jacobian_stiffness(
498        &self,
499        q: &[f64; DOF],
500        f_t_ee: &[f64; 16],
501        ee_t_k: &[f64; 16],
502    ) -> [f64; 42] {
503        SoModelBackend::call_jacobian_ee(self.symbols.o_j_j9, q, &mat4_mul(f_t_ee, ee_t_k))
504    }
505}
506
507#[cfg(test)]
508mod tests;