1use 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
27pub(crate) const DOF: usize = 7;
29
30const LAST_LINK_NAME: &str = "link8";
32
33#[derive(Debug, Clone)]
35pub struct NativeBackend {
36 joint_placement: [Transform; DOF],
40 joint_axis: [Vector3<f64>; DOF],
42 link_inertia: [SpatialInertia; DOF],
46 flange_placement: Transform,
48}
49
50struct FrameSpec {
52 support: usize,
54 offset: Transform,
56}
57
58impl NativeBackend {
59 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 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 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 let flange_placement = pending;
157
158 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 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 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 pub fn flange_placement(&self) -> [f64; 16] {
200 self.flange_placement.to_column_major()
201 }
202
203 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
408enum FrameId {
409 Joint(usize),
411 Flange,
412 EndEffector,
413 Stiffness,
414}
415
416fn 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 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 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}