Skip to main content

cliffy_test/
error.rs

1//! Geometric error types for test failures
2//!
3//! When tests fail, they provide geometric information about the failure:
4//! - Distance from expected manifold
5//! - Gradient pointing toward valid states
6//! - Projected correction to nearest valid state
7
8use crate::{vector, GA3};
9use serde::{Deserialize, Serialize};
10
11/// Geometric error information for test failures
12///
13/// Unlike boolean test failures, geometric errors tell you:
14/// 1. How far the state is from being valid (distance)
15/// 2. Which direction to move to become valid (gradient)
16/// 3. What the nearest valid state would be (correction)
17#[derive(Clone, Debug, Serialize, Deserialize)]
18pub struct GeometricError {
19    /// Distance from expected manifold (scalar)
20    pub distance: f64,
21
22    /// Gradient pointing toward valid states
23    ///
24    /// This is the direction of steepest descent toward the manifold.
25    /// Stored as coefficients [e1, e2, e3] for the vector part.
26    pub gradient: [f64; 3],
27
28    /// Projected correction to nearest valid state
29    ///
30    /// If you add this to the invalid state, you get the nearest valid state.
31    /// Stored as full multivector coefficients.
32    pub correction: [f64; 8],
33
34    /// Human-readable description of the error
35    pub description: String,
36}
37
38impl GeometricError {
39    /// Create a new geometric error
40    pub fn new(distance: f64, description: impl Into<String>) -> Self {
41        Self {
42            distance,
43            gradient: [0.0; 3],
44            correction: [0.0; 8],
45            description: description.into(),
46        }
47    }
48
49    /// Create error with gradient information
50    pub fn with_gradient(mut self, gradient: [f64; 3]) -> Self {
51        self.gradient = gradient;
52        self
53    }
54
55    /// Create error with correction information
56    pub fn with_correction(mut self, correction: [f64; 8]) -> Self {
57        self.correction = correction;
58        self
59    }
60
61    /// Create error from a GA3 multivector representing the error
62    ///
63    /// Note: In GA3, basis blade indices use binary representation:
64    /// - 0 = scalar, 1 = e1, 2 = e2, 3 = e12, 4 = e3, 5 = e13, 6 = e23, 7 = e123
65    pub fn from_multivector(error_mv: &GA3, description: impl Into<String>) -> Self {
66        Self {
67            distance: error_mv.magnitude(),
68            gradient: [
69                error_mv.get(1), // e1 (0b001)
70                error_mv.get(2), // e2 (0b010)
71                error_mv.get(4), // e3 (0b100)
72            ],
73            correction: [
74                error_mv.get(0), // scalar
75                error_mv.get(1), // e1
76                error_mv.get(2), // e2
77                error_mv.get(4), // e3
78                error_mv.get(3), // e12
79                error_mv.get(5), // e13
80                error_mv.get(6), // e23
81                error_mv.get(7), // e123
82            ],
83            description: description.into(),
84        }
85    }
86
87    /// Check if this error is within tolerance
88    pub fn is_within_tolerance(&self, epsilon: f64) -> bool {
89        self.distance < epsilon
90    }
91
92    /// Get the gradient as a GA3 vector
93    pub fn gradient_as_ga3(&self) -> GA3 {
94        vector(self.gradient[0], self.gradient[1], self.gradient[2])
95    }
96
97    /// Get the correction as a GA3 multivector
98    pub fn correction_as_ga3(&self) -> GA3 {
99        GA3::from_coefficients(self.correction.to_vec())
100    }
101}
102
103impl std::fmt::Display for GeometricError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(
106            f,
107            "GeometricError {{ distance: {:.6}, description: \"{}\" }}",
108            self.distance, self.description
109        )
110    }
111}
112
113impl std::error::Error for GeometricError {}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_error_creation() {
121        let error = GeometricError::new(0.5, "Test error");
122        assert_eq!(error.distance, 0.5);
123        assert_eq!(error.description, "Test error");
124    }
125
126    #[test]
127    fn test_error_from_multivector() {
128        let mv = vector(1.0, 2.0, 3.0);
129        let error = GeometricError::from_multivector(&mv, "Vector error");
130
131        assert!((error.distance - mv.magnitude()).abs() < 1e-10);
132        assert_eq!(error.gradient[0], 1.0);
133        assert_eq!(error.gradient[1], 2.0);
134        assert_eq!(error.gradient[2], 3.0);
135    }
136
137    #[test]
138    fn test_within_tolerance() {
139        let error = GeometricError::new(0.001, "Small error");
140        assert!(error.is_within_tolerance(0.01));
141        assert!(!error.is_within_tolerance(0.0001));
142    }
143}