use crate::math;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TorqueSensorSide {
Link,
Motor,
}
impl TorqueSensorSide {
#[inline]
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Link => "LinkSide",
Self::Motor => "MotorSide",
}
}
}
#[must_use]
pub fn joint_residual_norm(per_joint_tau_residual: &[f64]) -> Option<f64> {
debug_assert!(per_joint_tau_residual.len() <= 32, "per-joint slice fits a typical 7-DoF arm + safety margin");
let mut ssq = 0.0_f64;
let mut n = 0_usize;
for &r in per_joint_tau_residual {
if r.is_finite() {
debug_assert!(r * r >= 0.0, "squared finite f64 is non-negative");
ssq += r * r;
n += 1;
}
}
debug_assert!(n <= per_joint_tau_residual.len(), "finite-count cannot exceed slice length");
if n == 0 {
return None;
}
debug_assert!(ssq.is_finite() && ssq >= 0.0, "ssq must be a finite non-negative aggregate");
math::sqrt_f64(ssq)
}
#[must_use]
pub fn tau_residual_norm(tau_measured: &[f64], tau_predicted: &[f64]) -> Option<f64> {
debug_assert!(tau_measured.len() <= 32, "torque slice within typical arm DoF + margin");
debug_assert!(tau_predicted.len() <= 32, "torque slice within typical arm DoF + margin");
if tau_measured.len() != tau_predicted.len() {
return None;
}
if tau_measured.is_empty() {
return None;
}
debug_assert_eq!(tau_measured.len(), tau_predicted.len(), "lengths matched after early returns");
let mut ssq = 0.0_f64;
let mut n = 0_usize;
let mut i = 0_usize;
while i < tau_measured.len() {
let m = tau_measured[i];
let p = tau_predicted[i];
if m.is_finite() && p.is_finite() {
let d = m - p;
debug_assert!(d.is_finite(), "diff of finite operands is finite");
ssq += d * d;
n += 1;
}
i += 1;
}
debug_assert!(n <= tau_measured.len(), "finite-count bounded by slice length");
if n == 0 {
return None;
}
debug_assert!(ssq >= 0.0, "ssq is a sum of non-negative squares");
math::sqrt_f64(ssq)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_slice_is_none() {
assert!(joint_residual_norm(&[]).is_none());
}
#[test]
fn all_nan_slice_is_none() {
assert!(joint_residual_norm(&[f64::NAN, f64::NAN]).is_none());
}
#[test]
fn single_joint_norm_is_absolute_value() {
let r = joint_residual_norm(&[0.3]).expect("finite");
assert!((r - 0.3).abs() < 1e-12);
let r2 = joint_residual_norm(&[-0.3]).expect("finite");
assert!((r2 - 0.3).abs() < 1e-12);
}
#[test]
fn three_joint_norm_is_euclidean() {
let r = joint_residual_norm(&[3.0, 4.0, 0.0]).expect("finite");
assert!((r - 5.0).abs() < 1e-12);
}
#[test]
fn tau_residual_respects_lengths() {
assert!(tau_residual_norm(&[1.0, 2.0], &[1.0]).is_none());
assert!(tau_residual_norm(&[], &[]).is_none());
}
#[test]
fn tau_residual_is_difference_norm() {
let meas = [1.0, 2.0, 3.0];
let pred = [1.0, 2.0, 3.0];
let r = tau_residual_norm(&meas, &pred).expect("finite");
assert!(r.abs() < 1e-12);
let pred_off = [1.3, 2.4, 3.0];
let r2 = tau_residual_norm(&meas, &pred_off).expect("finite");
assert!((r2 - 0.5).abs() < 1e-12);
}
#[test]
fn tau_residual_skips_non_finite_joints() {
let meas = [1.0, f64::NAN, 3.0];
let pred = [1.0, 2.0, 3.0];
let r = tau_residual_norm(&meas, &pred).expect("at least one finite joint");
assert!(r.abs() < 1e-12);
}
#[test]
fn torque_side_labels_are_stable() {
assert_eq!(TorqueSensorSide::Link.label(), "LinkSide");
assert_eq!(TorqueSensorSide::Motor.label(), "MotorSide");
}
}