dynibo 0.5.2

Tree-structured robot kinematics and dynamics with runtime-size workspace APIs
Documentation
use super::context::TestContext;

#[derive(Clone, Copy, Debug)]
pub struct Tolerance {
    pub absolute: f64,
    pub relative: f64,
}

impl Tolerance {
    pub const fn new(absolute: f64, relative: f64) -> Self {
        Self { absolute, relative }
    }

    fn allowed(self, actual: f64, expected: f64) -> f64 {
        self.absolute + self.relative * actual.abs().max(expected.abs())
    }
}

pub const STRICT: Tolerance = Tolerance::new(2.0e-12, 2.0e-12);
pub const DYNAMICS: Tolerance = Tolerance::new(3.0e-9, 1.0e-9);
pub const KINEMATICS: Tolerance = Tolerance::new(3.0e-9, 1.0e-9);

#[track_caller]
pub fn assert_scalar_close(
    actual: f64,
    expected: f64,
    tolerance: Tolerance,
    context: &TestContext,
) {
    let error = (actual - expected).abs();
    let allowed = tolerance.allowed(actual, expected);
    assert!(
        actual.is_finite() && expected.is_finite() && error <= allowed,
        "numeric mismatch: {context} actual={actual:.17e} expected={expected:.17e} \
         abs_error={error:.17e} allowed={allowed:.17e} atol={:.3e} rtol={:.3e}",
        tolerance.absolute,
        tolerance.relative,
    );
}

#[track_caller]
pub fn assert_slice_close(
    actual: &[f64],
    expected: &[f64],
    tolerance: Tolerance,
    context: &TestContext,
) {
    assert_eq!(
        actual.len(),
        expected.len(),
        "numeric length mismatch: {context}"
    );

    let mut worst: Option<(usize, f64, f64, f64, f64)> = None;
    for (index, (&actual, &expected)) in actual.iter().zip(expected).enumerate() {
        let error = (actual - expected).abs();
        let allowed = tolerance.allowed(actual, expected);
        let normalized = if allowed > 0.0 {
            error / allowed
        } else {
            error
        };
        if worst.is_none_or(|(_, _, _, _, worst_normalized)| normalized > worst_normalized) {
            worst = Some((index, actual, expected, error, normalized));
        }
    }

    if let Some((index, actual, expected, error, _)) = worst {
        let allowed = tolerance.allowed(actual, expected);
        assert!(
            actual.is_finite() && expected.is_finite() && error <= allowed,
            "numeric slice mismatch: {context} index={index} actual={actual:.17e} \
             expected={expected:.17e} abs_error={error:.17e} allowed={allowed:.17e} \
             atol={:.3e} rtol={:.3e}",
            tolerance.absolute,
            tolerance.relative,
        );
    }
}