Skip to main content

cliffy_test/
manifold.rs

1//! Manifold testing - verify states lie on expected geometric manifolds
2//!
3//! Valid states form geometric manifolds. Testing verifies that state
4//! transformations keep states on the manifold.
5
6use crate::error::GeometricError;
7use crate::result::TestResult;
8use crate::{bivector, vector, GA3};
9
10/// A constraint on a geometric manifold
11pub trait ManifoldConstraint: Send + Sync {
12    /// Check if a point satisfies this constraint
13    fn satisfied(&self, point: &GA3) -> bool;
14
15    /// Calculate distance from this constraint
16    fn distance(&self, point: &GA3) -> f64;
17
18    /// Project a point onto the constraint surface
19    fn project(&self, point: &GA3) -> GA3;
20
21    /// Description of this constraint
22    fn description(&self) -> &str;
23}
24
25/// A geometric manifold defined by constraints
26pub struct Manifold {
27    /// Name of the manifold
28    pub name: String,
29    /// Constraints defining the manifold
30    constraints: Vec<Box<dyn ManifoldConstraint>>,
31    /// Tolerance for membership tests
32    pub tolerance: f64,
33}
34
35impl Manifold {
36    /// Create a new empty manifold
37    pub fn new(name: impl Into<String>) -> Self {
38        Self {
39            name: name.into(),
40            constraints: Vec::new(),
41            tolerance: 1e-10,
42        }
43    }
44
45    /// Set the tolerance for membership tests
46    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
47        self.tolerance = tolerance;
48        self
49    }
50
51    /// Add a constraint to the manifold
52    pub fn with_constraint(mut self, constraint: impl ManifoldConstraint + 'static) -> Self {
53        self.constraints.push(Box::new(constraint));
54        self
55    }
56
57    /// Check if a point lies on the manifold
58    pub fn contains(&self, point: &GA3) -> bool {
59        self.constraints
60            .iter()
61            .all(|c| c.distance(point) < self.tolerance)
62    }
63
64    /// Calculate distance from the manifold
65    pub fn distance(&self, point: &GA3) -> f64 {
66        self.constraints
67            .iter()
68            .map(|c| c.distance(point))
69            .fold(0.0, f64::max)
70    }
71
72    /// Project a point onto the manifold
73    ///
74    /// Iteratively projects through all constraints.
75    /// Note: This is an approximation for non-convex manifolds.
76    pub fn project(&self, point: &GA3) -> GA3 {
77        let mut result = point.clone();
78        for _ in 0..10 {
79            // Max iterations
80            let mut changed = false;
81            for constraint in &self.constraints {
82                if !constraint.satisfied(&result) {
83                    result = constraint.project(&result);
84                    changed = true;
85                }
86            }
87            if !changed {
88                break;
89            }
90        }
91        result
92    }
93
94    /// Verify that a point lies on the manifold
95    pub fn verify(&self, point: &GA3) -> TestResult {
96        let dist = self.distance(point);
97        if dist < self.tolerance {
98            TestResult::Pass
99        } else {
100            let projected = self.project(point);
101            let error_mv = &projected - point;
102            let mut error = GeometricError::from_multivector(
103                &error_mv,
104                format!("Point not on manifold '{}'", self.name),
105            );
106            error.distance = dist;
107            TestResult::Fail(error)
108        }
109    }
110}
111
112/// Constraint: magnitude equals a specific value
113pub struct MagnitudeConstraint {
114    target: f64,
115}
116
117impl MagnitudeConstraint {
118    /// Create a magnitude constraint
119    pub fn new(target: f64) -> Self {
120        Self { target }
121    }
122
123    /// Create a unit magnitude constraint (magnitude = 1)
124    pub fn unit() -> Self {
125        Self { target: 1.0 }
126    }
127}
128
129impl ManifoldConstraint for MagnitudeConstraint {
130    fn satisfied(&self, point: &GA3) -> bool {
131        (point.magnitude() - self.target).abs() < 1e-10
132    }
133
134    fn distance(&self, point: &GA3) -> f64 {
135        (point.magnitude() - self.target).abs()
136    }
137
138    fn project(&self, point: &GA3) -> GA3 {
139        let mag = point.magnitude();
140        if mag > 1e-10 {
141            point * (self.target / mag)
142        } else {
143            // Degenerate - can't normalize zero
144            point.clone()
145        }
146    }
147
148    fn description(&self) -> &str {
149        "Magnitude constraint"
150    }
151}
152
153/// Constraint: scalar part equals a specific value
154pub struct ScalarConstraint {
155    target: f64,
156}
157
158impl ScalarConstraint {
159    /// Create a scalar constraint
160    pub fn new(target: f64) -> Self {
161        Self { target }
162    }
163
164    /// Create a zero scalar constraint
165    pub fn zero() -> Self {
166        Self { target: 0.0 }
167    }
168}
169
170impl ManifoldConstraint for ScalarConstraint {
171    fn satisfied(&self, point: &GA3) -> bool {
172        (point.get(0) - self.target).abs() < 1e-10
173    }
174
175    fn distance(&self, point: &GA3) -> f64 {
176        (point.get(0) - self.target).abs()
177    }
178
179    fn project(&self, point: &GA3) -> GA3 {
180        // Create new multivector with modified scalar
181        let mut coeffs: Vec<f64> = (0..8).map(|i| point.get(i)).collect();
182        coeffs[0] = self.target; // Set scalar to target
183        GA3::from_coefficients(coeffs)
184    }
185
186    fn description(&self) -> &str {
187        "Scalar constraint"
188    }
189}
190
191/// Constraint: point is a pure vector (only grade 1 components)
192///
193/// In GA3 basis indices:
194/// - 0 = scalar, 1 = e1, 2 = e2, 3 = e12, 4 = e3, 5 = e13, 6 = e23, 7 = e123
195/// - Vector components are at indices 1, 2, 4 (e1, e2, e3)
196pub struct PureVectorConstraint;
197
198impl ManifoldConstraint for PureVectorConstraint {
199    fn satisfied(&self, point: &GA3) -> bool {
200        let s = point.get(0).abs(); // scalar
201        let b12 = point.get(3).abs(); // e12
202        let b13 = point.get(5).abs(); // e13
203        let b23 = point.get(6).abs(); // e23
204        let tri = point.get(7).abs(); // e123
205        s < 1e-10 && b12 < 1e-10 && b13 < 1e-10 && b23 < 1e-10 && tri < 1e-10
206    }
207
208    fn distance(&self, point: &GA3) -> f64 {
209        let s = point.get(0);
210        let b12 = point.get(3);
211        let b13 = point.get(5);
212        let b23 = point.get(6);
213        let tri = point.get(7);
214        (s * s + b12 * b12 + b13 * b13 + b23 * b23 + tri * tri).sqrt()
215    }
216
217    fn project(&self, point: &GA3) -> GA3 {
218        // Extract only vector components (e1, e2, e3 at indices 1, 2, 4)
219        vector(point.get(1), point.get(2), point.get(4))
220    }
221
222    fn description(&self) -> &str {
223        "Pure vector constraint"
224    }
225}
226
227/// Constraint: point is a pure bivector (only grade 2 components)
228///
229/// In GA3 basis indices:
230/// - Bivector components are at indices 3, 5, 6 (e12, e13, e23)
231pub struct PureBivectorConstraint;
232
233impl ManifoldConstraint for PureBivectorConstraint {
234    fn satisfied(&self, point: &GA3) -> bool {
235        let s = point.get(0).abs(); // scalar
236        let v1 = point.get(1).abs(); // e1
237        let v2 = point.get(2).abs(); // e2
238        let v3 = point.get(4).abs(); // e3
239        let tri = point.get(7).abs(); // e123
240        s < 1e-10 && v1 < 1e-10 && v2 < 1e-10 && v3 < 1e-10 && tri < 1e-10
241    }
242
243    fn distance(&self, point: &GA3) -> f64 {
244        let s = point.get(0);
245        let v1 = point.get(1);
246        let v2 = point.get(2);
247        let v3 = point.get(4);
248        let tri = point.get(7);
249        (s * s + v1 * v1 + v2 * v2 + v3 * v3 + tri * tri).sqrt()
250    }
251
252    fn project(&self, point: &GA3) -> GA3 {
253        // Extract only bivector components (e12, e13, e23 at indices 3, 5, 6)
254        bivector(point.get(3), point.get(5), point.get(6))
255    }
256
257    fn description(&self) -> &str {
258        "Pure bivector constraint"
259    }
260}
261
262/// Create the unit sphere manifold (vectors with magnitude 1)
263pub fn unit_sphere() -> Manifold {
264    Manifold::new("Unit Sphere")
265        .with_constraint(PureVectorConstraint)
266        .with_constraint(MagnitudeConstraint::unit())
267}
268
269/// Create the rotor manifold (even-grade elements with unit magnitude)
270pub fn rotor_manifold() -> Manifold {
271    Manifold::new("Rotor Manifold").with_constraint(MagnitudeConstraint::unit())
272    // Note: Rotors are even-grade (scalar + bivector), but verifying
273    // the even-grade constraint is more complex in practice
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_magnitude_constraint() {
282        let constraint = MagnitudeConstraint::unit();
283        let unit_vec = vector(1.0, 0.0, 0.0);
284        assert!(constraint.satisfied(&unit_vec));
285
286        let non_unit = vector(2.0, 0.0, 0.0);
287        assert!(!constraint.satisfied(&non_unit));
288
289        let projected = constraint.project(&non_unit);
290        assert!(constraint.satisfied(&projected));
291    }
292
293    #[test]
294    fn test_manifold_contains() {
295        let manifold = unit_sphere();
296
297        let on_sphere = vector(1.0, 0.0, 0.0);
298        assert!(manifold.contains(&on_sphere));
299
300        let off_sphere = vector(2.0, 0.0, 0.0);
301        assert!(!manifold.contains(&off_sphere));
302    }
303
304    #[test]
305    fn test_manifold_project() {
306        let manifold = unit_sphere();
307
308        let off_sphere = vector(3.0, 4.0, 0.0);
309        let projected = manifold.project(&off_sphere);
310
311        assert!(
312            manifold.contains(&projected),
313            "Projected point should be on sphere"
314        );
315        assert!(
316            (projected.magnitude() - 1.0).abs() < 1e-10,
317            "Projected magnitude should be 1"
318        );
319    }
320
321    #[test]
322    fn test_manifold_verify() {
323        let manifold = unit_sphere();
324
325        let on = vector(0.0, 1.0, 0.0);
326        assert!(manifold.verify(&on).is_pass());
327
328        let off = vector(0.0, 2.0, 0.0);
329        let result = manifold.verify(&off);
330        assert!(result.is_fail());
331
332        if let TestResult::Fail(error) = result {
333            assert!(error.distance > 0.0);
334        }
335    }
336}