use crate::kinematics::{self, TorqueSensorSide};
pub const NUM_JOINTS: usize = 7;
pub const SENSOR_SIDE: TorqueSensorSide = TorqueSensorSide::Link;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sample {
pub tau_measured: [f64; NUM_JOINTS],
pub tau_predicted: [f64; NUM_JOINTS],
}
impl Sample {
#[inline]
#[must_use]
pub fn residual_norm(&self) -> Option<f64> {
kinematics::tau_residual_norm(&self.tau_measured, &self.tau_predicted)
}
}
pub fn residual_stream(samples: &[Sample], out: &mut [f64]) -> usize {
debug_assert!(samples.len() <= 1_000_000, "sample slice unreasonably large");
let n = samples.len().min(out.len());
debug_assert!(n <= out.len() && n <= samples.len(), "n bounded by both buffers");
let mut i = 0_usize;
while i < n {
out[i] = samples[i].residual_norm().unwrap_or(0.0);
i += 1;
}
debug_assert_eq!(i, n, "loop must run exactly n iterations");
n
}
pub fn fixture_residuals(out: &mut [f64]) -> usize {
debug_assert!(!out.is_empty(), "fixture buffer must be non-empty");
residual_stream(&FIXTURE, out)
}
pub const FIXTURE: [Sample; 6] = [
Sample {
tau_measured: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
tau_predicted: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
},
Sample {
tau_measured: [0.11, -0.02, -0.18, 0.06, 0.01, -0.01, 0.00],
tau_predicted: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
},
Sample {
tau_measured: [0.15, -0.05, -0.10, 0.10, 0.02, 0.00, 0.01],
tau_predicted: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
},
Sample {
tau_measured: [0.20, -0.08, 0.00, 0.15, 0.03, 0.01, 0.02],
tau_predicted: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
},
Sample {
tau_measured: [0.15, -0.05, -0.10, 0.10, 0.02, 0.00, 0.01],
tau_predicted: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
},
Sample {
tau_measured: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
tau_predicted: [0.10, 0.00, -0.20, 0.05, 0.00, -0.01, 0.00],
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_match_produces_zero_residual() {
let s = Sample {
tau_measured: [0.1; NUM_JOINTS],
tau_predicted: [0.1; NUM_JOINTS],
};
let r = s.residual_norm().expect("finite");
assert!(r.abs() < 1e-12);
}
#[test]
fn pythagorean_triple_across_joints() {
let mut meas = [0.0_f64; NUM_JOINTS];
let pred = [0.0_f64; NUM_JOINTS];
meas[0] = 3.0;
meas[1] = 4.0;
let s = Sample { tau_measured: meas, tau_predicted: pred };
let r = s.residual_norm().expect("finite");
assert!((r - 5.0).abs() < 1e-12);
}
#[test]
fn fixture_residuals_non_decreasing_in_perturbation_window() {
let mut buf = [0.0_f64; 6];
let n = residual_stream(&FIXTURE, &mut buf);
assert_eq!(n, 6);
assert!(buf[0] < 1e-12, "first sample matches model → zero");
assert!(buf[3] > buf[1], "residual peaks away from the nominal trajectory");
assert!(buf[5] < 1e-12, "last sample matches model again → zero");
}
#[test]
fn sensor_side_is_link() {
assert_eq!(SENSOR_SIDE, TorqueSensorSide::Link);
}
}