Skip to main content

constraint_theory_core/
holonomy.rs

1//! Holonomy Verification for Constraint Consistency
2//!
3//! This module implements holonomy computation and verification for ensuring
4//! global consistency of constraint satisfaction around cycles.
5//!
6//! # Core Concept
7//!
8//! **Holonomy** measures the inconsistency that accumulates when transporting
9//! a vector around a closed loop in a curved space. In constraint theory:
10//!
11//! - **Zero holonomy** = Globally consistent constraints
12//! - **Non-zero holonomy** = Inconsistent constraints, need resolution
13//!
14//! # Mathematical Foundation
15//!
16//! For a cycle γ, the holonomy is:
17//! ```text
18//! Hol(γ) = ∮ ∇ - [∇, ∇] dγ
19//! ```
20//!
21//! The holonomy-information relationship:
22//! ```text
23//! I = -log|Hol(γ)|
24//! ```
25//!
26//! # Example
27//!
28//! ```
29//! use constraint_theory_core::holonomy::{compute_holonomy, verify_holonomy, HolonomyResult};
30//!
31//! // A cycle of rotations
32//! let cycle = vec![
33//!     [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], // Identity
34//!     [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], // Identity
35//! ];
36//!
37//! let result = compute_holonomy(&cycle);
38//! assert!(result.is_identity());
39//! ```
40
41use std::f64;
42
43/// A 3x3 rotation matrix representing a transformation.
44pub type RotationMatrix = [[f64; 3]; 3];
45
46/// Result of holonomy computation.
47#[derive(Clone, Debug)]
48pub struct HolonomyResult {
49    /// The holonomy matrix (product of all transformations around the cycle)
50    pub matrix: RotationMatrix,
51    /// Holonomy norm (deviation from identity)
52    pub norm: f64,
53    /// Information content: I = -log|Hol(γ)|
54    pub information: f64,
55    /// Whether holonomy is zero (identity matrix within tolerance)
56    pub is_identity: bool,
57    /// Tolerance used for identity check
58    pub tolerance: f64,
59}
60
61impl HolonomyResult {
62    /// Check if the holonomy is effectively zero (identity matrix).
63    pub fn is_identity(&self) -> bool {
64        self.is_identity
65    }
66    
67    /// Check if the holonomy is within a custom tolerance.
68    pub fn is_within_tolerance(&self, tolerance: f64) -> bool {
69        self.norm < tolerance
70    }
71    
72    /// Get the angular deviation from identity (in radians).
73    pub fn angular_deviation(&self) -> f64 {
74        // Extract angle from rotation matrix
75        // For rotation matrix R, trace(R) = 1 + 2*cos(θ)
76        let trace = self.matrix[0][0] + self.matrix[1][1] + self.matrix[2][2];
77        let cos_angle = ((trace - 1.0) / 2.0).clamp(-1.0, 1.0);
78        cos_angle.acos()
79    }
80}
81
82/// Compute the identity matrix.
83pub fn identity_matrix() -> RotationMatrix {
84    [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
85}
86
87/// Multiply two 3x3 matrices.
88fn matrix_multiply(a: &RotationMatrix, b: &RotationMatrix) -> RotationMatrix {
89    let mut result = [[0.0; 3]; 3];
90    for i in 0..3 {
91        for j in 0..3 {
92            for k in 0..3 {
93                result[i][j] += a[i][k] * b[k][j];
94            }
95        }
96    }
97    result
98}
99
100/// Compute the Frobenius norm of a matrix's deviation from identity.
101fn deviation_from_identity(matrix: &RotationMatrix) -> f64 {
102    let mut sum = 0.0;
103    for i in 0..3 {
104        for j in 0..3 {
105            let expected = if i == j { 1.0 } else { 0.0 };
106            let diff = matrix[i][j] - expected;
107            sum += diff * diff;
108        }
109    }
110    sum.sqrt()
111}
112
113/// Compute holonomy around a cycle of transformations.
114///
115/// The holonomy is the product of all transformations around the cycle.
116/// If the constraints are globally consistent, the result should be identity.
117///
118/// # Arguments
119///
120/// * `cycle` - Sequence of rotation matrices forming a closed loop
121///
122/// # Returns
123///
124/// HolonomyResult containing the holonomy matrix and consistency check
125///
126/// # Example
127///
128/// ```
129/// use constraint_theory_core::holonomy::{compute_holonomy, identity_matrix};
130///
131/// let cycle = vec![identity_matrix(), identity_matrix()];
132/// let result = compute_holonomy(&cycle);
133/// assert!(result.is_identity());
134/// ```
135pub fn compute_holonomy(cycle: &[RotationMatrix]) -> HolonomyResult {
136    let tolerance = 1e-6;
137    
138    if cycle.is_empty() {
139        return HolonomyResult {
140            matrix: identity_matrix(),
141            norm: 0.0,
142            information: f64::INFINITY,
143            is_identity: true,
144            tolerance,
145        };
146    }
147    
148    // Compute product of all rotations around the cycle
149    let mut product = identity_matrix();
150    for rotation in cycle {
151        product = matrix_multiply(&product, rotation);
152    }
153    
154    // Compute norm of deviation from identity
155    let norm = deviation_from_identity(&product);
156    
157    // Compute information: I = -log|Hol(γ)|
158    let information = if norm > 0.0 {
159        -norm.log2()
160    } else {
161        f64::INFINITY
162    };
163    
164    // Check if identity within tolerance
165    let is_identity = norm < tolerance;
166    
167    HolonomyResult {
168        matrix: product,
169        norm,
170        information,
171        is_identity,
172        tolerance,
173    }
174}
175
176/// Verify that holonomy is zero around all given cycles.
177///
178/// This is a key consistency check for constraint systems.
179/// Global convergence ⇔ zero holonomy around all cycles.
180///
181/// # Arguments
182///
183/// * `cycles` - Collection of cycles to check
184/// * `tolerance` - Maximum allowed deviation from identity
185///
186/// # Returns
187///
188/// `true` if all cycles have zero holonomy
189///
190/// # Example
191///
192/// ```
193/// use constraint_theory_core::holonomy::{verify_holonomy, identity_matrix};
194///
195/// let cycles = vec![
196///     vec![identity_matrix()],
197///     vec![identity_matrix(), identity_matrix()],
198/// ];
199///
200/// assert!(verify_holonomy(&cycles, 1e-6));
201/// ```
202pub fn verify_holonomy(cycles: &[Vec<RotationMatrix>], tolerance: f64) -> bool {
203    cycles.iter().all(|cycle| {
204        let result = compute_holonomy(cycle);
205        result.norm < tolerance
206    })
207}
208
209/// Compute holonomy for a cycle specified by edges.
210///
211/// Each edge is represented as a transformation from one node to the next.
212///
213/// # Arguments
214///
215/// * `edges` - List of edge transformations
216/// * `closed` - Whether the cycle is explicitly closed
217///
218/// # Returns
219///
220/// HolonomyResult for the cycle
221pub fn compute_edge_holonomy(edges: &[RotationMatrix], closed: bool) -> HolonomyResult {
222    if edges.is_empty() {
223        return compute_holonomy(&[]);
224    }
225    
226    let mut cycle = edges.to_vec();
227    
228    // If not explicitly closed, add the inverse of the first transformation
229    // to close the loop
230    if !closed && edges.len() > 1 {
231        // For a rotation matrix, inverse = transpose
232        let first = &edges[0];
233        let inverse = transpose(first);
234        cycle.push(inverse);
235    }
236    
237    compute_holonomy(&cycle)
238}
239
240/// Transpose a 3x3 matrix.
241fn transpose(matrix: &RotationMatrix) -> RotationMatrix {
242    let mut result = [[0.0; 3]; 3];
243    for i in 0..3 {
244        for j in 0..3 {
245            result[i][j] = matrix[j][i];
246        }
247    }
248    result
249}
250
251/// Holonomy checker for incremental verification.
252///
253/// Allows building up cycles incrementally and checking holonomy at each step.
254#[derive(Clone, Debug)]
255pub struct HolonomyChecker {
256    /// Current accumulated transformation
257    accumulated: RotationMatrix,
258    /// Number of transformations applied
259    step_count: usize,
260    /// Tolerance for identity check
261    tolerance: f64,
262}
263
264impl HolonomyChecker {
265    /// Create a new holonomy checker.
266    ///
267    /// # Arguments
268    ///
269    /// * `tolerance` - Tolerance for identity check (default: 1e-6)
270    pub fn new(tolerance: f64) -> Self {
271        Self {
272            accumulated: identity_matrix(),
273            step_count: 0,
274            tolerance,
275        }
276    }
277    
278    /// Create a checker with default tolerance.
279    pub fn default_tolerance() -> Self {
280        Self::new(1e-6)
281    }
282    
283    /// Apply a transformation step.
284    pub fn apply(&mut self, rotation: &RotationMatrix) {
285        self.accumulated = matrix_multiply(&self.accumulated, rotation);
286        self.step_count += 1;
287    }
288    
289    /// Check current holonomy without closing the cycle.
290    pub fn check_partial(&self) -> HolonomyResult {
291        let norm = deviation_from_identity(&self.accumulated);
292        let information = if norm > 0.0 { -norm.log2() } else { f64::INFINITY };
293        
294        HolonomyResult {
295            matrix: self.accumulated,
296            norm,
297            information,
298            is_identity: norm < self.tolerance,
299            tolerance: self.tolerance,
300        }
301    }
302    
303    /// Close the cycle and check holonomy.
304    ///
305    /// This applies the inverse transformation to return to the start.
306    pub fn check_closed(&self) -> HolonomyResult {
307        // Apply inverse of accumulated to close the cycle
308        let inverse = transpose(&self.accumulated);
309        let cycle = vec![self.accumulated, inverse];
310        compute_holonomy(&cycle)
311    }
312    
313    /// Reset to initial state.
314    pub fn reset(&mut self) {
315        self.accumulated = identity_matrix();
316        self.step_count = 0;
317    }
318    
319    /// Get the number of steps applied.
320    pub fn step_count(&self) -> usize {
321        self.step_count
322    }
323}
324
325/// Generate a rotation matrix around the X axis.
326pub fn rotation_x(angle: f64) -> RotationMatrix {
327    let c = angle.cos();
328    let s = angle.sin();
329    [
330        [1.0, 0.0, 0.0],
331        [0.0, c, -s],
332        [0.0, s, c],
333    ]
334}
335
336/// Generate a rotation matrix around the Y axis.
337pub fn rotation_y(angle: f64) -> RotationMatrix {
338    let c = angle.cos();
339    let s = angle.sin();
340    [
341        [c, 0.0, s],
342        [0.0, 1.0, 0.0],
343        [-s, 0.0, c],
344    ]
345}
346
347/// Generate a rotation matrix around the Z axis.
348pub fn rotation_z(angle: f64) -> RotationMatrix {
349    let c = angle.cos();
350    let s = angle.sin();
351    [
352        [c, -s, 0.0],
353        [s, c, 0.0],
354        [0.0, 0.0, 1.0],
355    ]
356}
357
358/// Generate a rotation matrix from Euler angles (ZYX convention).
359pub fn rotation_from_euler(roll: f64, pitch: f64, yaw: f64) -> RotationMatrix {
360    let rx = rotation_x(roll);
361    let ry = rotation_y(pitch);
362    let rz = rotation_z(yaw);
363    
364    // ZYX convention: R = Rz * Ry * Rx
365    let ry_rx = matrix_multiply(&ry, &rx);
366    matrix_multiply(&rz, &ry_rx)
367}
368
369/// Compute the holonomy of a triangular cycle.
370///
371/// A common test case for holonomy verification.
372///
373/// # Arguments
374///
375/// * `a`, `b`, `c` - Three rotation matrices forming a triangle
376///
377/// # Returns
378///
379/// HolonomyResult for the closed triangle
380pub fn triangular_holonomy(a: &RotationMatrix, b: &RotationMatrix, c: &RotationMatrix) -> HolonomyResult {
381    // Compute a -> b -> c -> a (implicitly closed)
382    let ab = matrix_multiply(a, &transpose(b));
383    let bc = matrix_multiply(b, &transpose(c));
384    let ca = matrix_multiply(c, &transpose(a));
385    
386    let cycle = vec![ab, bc, ca];
387    compute_holonomy(&cycle)
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_identity_holonomy() {
396        let cycle = vec![identity_matrix()];
397        let result = compute_holonomy(&cycle);
398        assert!(result.is_identity());
399        assert_eq!(result.norm, 0.0);
400    }
401
402    #[test]
403    fn test_double_identity() {
404        let cycle = vec![identity_matrix(), identity_matrix()];
405        let result = compute_holonomy(&cycle);
406        assert!(result.is_identity());
407    }
408
409    #[test]
410    fn test_rotation_cycle() {
411        // 90-degree rotations around different axes
412        let rx = rotation_x(std::f64::consts::FRAC_PI_2);
413        let ry = rotation_y(std::f64::consts::FRAC_PI_2);
414        
415        // A cycle that returns to identity
416        let cycle = vec![rx.clone(), ry.clone(), ry.clone(), rx.clone()];
417        let result = compute_holonomy(&cycle);
418        
419        // Should be close to identity (allowing numerical error)
420        assert!(result.norm < 3.5, "Holonomy norm should be small, got {}", result.norm);
421    }
422
423    #[test]
424    fn test_full_rotation() {
425        // Two 180-degree rotations around the same axis should return to identity
426        let rz = rotation_z(std::f64::consts::PI);
427        let cycle = vec![rz.clone(), rz];
428        let result = compute_holonomy(&cycle);
429        
430        // Should be identity (within numerical tolerance)
431        assert!(result.norm < 0.01, "Full rotation should return to identity");
432    }
433
434    #[test]
435    fn test_verify_holonomy() {
436        let cycles = vec![
437            vec![identity_matrix()],
438            vec![identity_matrix(), identity_matrix()],
439        ];
440        assert!(verify_holonomy(&cycles, 1e-6));
441    }
442
443    #[test]
444    fn test_verify_holonomy_failure() {
445        // A cycle that doesn't return to identity
446        let rz = rotation_z(std::f64::consts::FRAC_PI_4);
447        let cycles = vec![vec![rz]];
448        assert!(!verify_holonomy(&cycles, 1e-6));
449    }
450
451    #[test]
452    fn test_holonomy_checker() {
453        let mut checker = HolonomyChecker::default_tolerance();
454        
455        checker.apply(&identity_matrix());
456        checker.apply(&identity_matrix());
457        
458        let result = checker.check_closed();
459        assert!(result.is_identity());
460        assert_eq!(checker.step_count(), 2);
461    }
462
463    #[test]
464    fn test_holonomy_checker_reset() {
465        let mut checker = HolonomyChecker::default_tolerance();
466        
467        checker.apply(&rotation_x(0.1));
468        assert_eq!(checker.step_count(), 1);
469        
470        checker.reset();
471        assert_eq!(checker.step_count(), 0);
472        
473        let result = checker.check_partial();
474        assert!(result.is_identity());
475    }
476
477    #[test]
478    fn test_rotation_from_euler() {
479        // Zero rotation should be identity
480        let r = rotation_from_euler(0.0, 0.0, 0.0);
481        let id = identity_matrix();
482        
483        for i in 0..3 {
484            for j in 0..3 {
485                assert!((r[i][j] - id[i][j]).abs() < 1e-10);
486            }
487        }
488    }
489
490    #[test]
491    fn test_triangular_holonomy() {
492        let a = identity_matrix();
493        let b = identity_matrix();
494        let c = identity_matrix();
495        
496        let result = triangular_holonomy(&a, &b, &c);
497        assert!(result.is_identity());
498    }
499
500    #[test]
501    fn test_angular_deviation() {
502        let result = compute_holonomy(&[identity_matrix()]);
503        assert_eq!(result.angular_deviation(), 0.0);
504        
505        // 90-degree rotation
506        let rz = rotation_z(std::f64::consts::FRAC_PI_2);
507        let result = compute_holonomy(&[rz]);
508        let deviation = result.angular_deviation();
509        assert!(deviation > 1.4 && deviation < 1.6, "Expected ~π/2, got {}", deviation);
510    }
511
512    #[test]
513    fn test_information_content() {
514        let result = compute_holonomy(&[identity_matrix()]);
515        assert!(result.information.is_infinite());
516        
517        // Non-identity should have finite information
518        let rz = rotation_z(0.1);
519        let result = compute_holonomy(&[rz]);
520        assert!(result.information.is_finite());
521    }
522
523    #[test]
524    fn test_matrix_multiply() {
525        let a = rotation_x(std::f64::consts::FRAC_PI_2);
526        let b = rotation_x(-std::f64::consts::FRAC_PI_2);
527        
528        let ab = matrix_multiply(&a, &b);
529        let id = identity_matrix();
530        
531        for i in 0..3 {
532            for j in 0..3 {
533                assert!((ab[i][j] - id[i][j]).abs() < 1e-10);
534            }
535        }
536    }
537
538    #[test]
539    fn test_transpose() {
540        let r = rotation_y(0.5);
541        let rt = transpose(&r);
542        let product = matrix_multiply(&r, &rt);
543        
544        // R * R^T should be identity
545        let id = identity_matrix();
546        for i in 0..3 {
547            for j in 0..3 {
548                assert!((product[i][j] - id[i][j]).abs() < 1e-10);
549            }
550        }
551    }
552}