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;