dsfb_robotics/
kinematics.rs1use crate::math;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub enum TorqueSensorSide {
34 Link,
36 Motor,
38}
39
40impl TorqueSensorSide {
41 #[inline]
43 #[must_use]
44 pub const fn label(self) -> &'static str {
45 match self {
46 Self::Link => "LinkSide",
47 Self::Motor => "MotorSide",
48 }
49 }
50}
51
52#[must_use]
63pub fn joint_residual_norm(per_joint_tau_residual: &[f64]) -> Option<f64> {
64 debug_assert!(per_joint_tau_residual.len() <= 32, "per-joint slice fits a typical 7-DoF arm + safety margin");
65 let mut ssq = 0.0_f64;
66 let mut n = 0_usize;
67 for &r in per_joint_tau_residual {
68 if r.is_finite() {
69 debug_assert!(r * r >= 0.0, "squared finite f64 is non-negative");
70 ssq += r * r;
71 n += 1;
72 }
73 }
74 debug_assert!(n <= per_joint_tau_residual.len(), "finite-count cannot exceed slice length");
75 if n == 0 {
76 return None;
77 }
78 debug_assert!(ssq.is_finite() && ssq >= 0.0, "ssq must be a finite non-negative aggregate");
79 math::sqrt_f64(ssq)
80}
81
82#[must_use]
90pub fn tau_residual_norm(tau_measured: &[f64], tau_predicted: &[f64]) -> Option<f64> {
91 debug_assert!(tau_measured.len() <= 32, "torque slice within typical arm DoF + margin");
92 debug_assert!(tau_predicted.len() <= 32, "torque slice within typical arm DoF + margin");
93 if tau_measured.len() != tau_predicted.len() {
94 return None;
95 }
96 if tau_measured.is_empty() {
97 return None;
98 }
99 debug_assert_eq!(tau_measured.len(), tau_predicted.len(), "lengths matched after early returns");
100 let mut ssq = 0.0_f64;
101 let mut n = 0_usize;
102 let mut i = 0_usize;
103 while i < tau_measured.len() {
104 let m = tau_measured[i];
105 let p = tau_predicted[i];
106 if m.is_finite() && p.is_finite() {
107 let d = m - p;
108 debug_assert!(d.is_finite(), "diff of finite operands is finite");
109 ssq += d * d;
110 n += 1;
111 }
112 i += 1;
113 }
114 debug_assert!(n <= tau_measured.len(), "finite-count bounded by slice length");
115 if n == 0 {
116 return None;
117 }
118 debug_assert!(ssq >= 0.0, "ssq is a sum of non-negative squares");
119 math::sqrt_f64(ssq)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn empty_slice_is_none() {
128 assert!(joint_residual_norm(&[]).is_none());
129 }
130
131 #[test]
132 fn all_nan_slice_is_none() {
133 assert!(joint_residual_norm(&[f64::NAN, f64::NAN]).is_none());
134 }
135
136 #[test]
137 fn single_joint_norm_is_absolute_value() {
138 let r = joint_residual_norm(&[0.3]).expect("finite");
139 assert!((r - 0.3).abs() < 1e-12);
140 let r2 = joint_residual_norm(&[-0.3]).expect("finite");
141 assert!((r2 - 0.3).abs() < 1e-12);
142 }
143
144 #[test]
145 fn three_joint_norm_is_euclidean() {
146 let r = joint_residual_norm(&[3.0, 4.0, 0.0]).expect("finite");
148 assert!((r - 5.0).abs() < 1e-12);
149 }
150
151 #[test]
152 fn tau_residual_respects_lengths() {
153 assert!(tau_residual_norm(&[1.0, 2.0], &[1.0]).is_none());
154 assert!(tau_residual_norm(&[], &[]).is_none());
155 }
156
157 #[test]
158 fn tau_residual_is_difference_norm() {
159 let meas = [1.0, 2.0, 3.0];
160 let pred = [1.0, 2.0, 3.0];
161 let r = tau_residual_norm(&meas, &pred).expect("finite");
162 assert!(r.abs() < 1e-12);
163 let pred_off = [1.3, 2.4, 3.0];
164 let r2 = tau_residual_norm(&meas, &pred_off).expect("finite");
165 assert!((r2 - 0.5).abs() < 1e-12);
167 }
168
169 #[test]
170 fn tau_residual_skips_non_finite_joints() {
171 let meas = [1.0, f64::NAN, 3.0];
172 let pred = [1.0, 2.0, 3.0];
173 let r = tau_residual_norm(&meas, &pred).expect("at least one finite joint");
174 assert!(r.abs() < 1e-12);
175 }
176
177 #[test]
178 fn torque_side_labels_are_stable() {
179 assert_eq!(TorqueSensorSide::Link.label(), "LinkSide");
180 assert_eq!(TorqueSensorSide::Motor.label(), "MotorSide");
181 }
182}