Skip to main content

lie_groups/
error.rs

1//! Error types for Lie group and Lie algebra operations
2//!
3//! Provides principled error handling for Lie-theoretic computations.
4
5use std::fmt;
6
7/// Errors that can occur during Lie group logarithm computation
8///
9/// The logarithm map `log: G → 𝔤` is only well-defined on a neighborhood
10/// of the identity. Elements far from the identity may not have a unique
11/// or well-defined logarithm.
12///
13/// # Mathematical Background
14///
15/// For matrix Lie groups, the logarithm is the inverse of the exponential map.
16/// However, unlike `exp: 𝔤 → G` which is always defined, `log: G → 𝔤` has
17/// a restricted domain:
18///
19/// - **U(1)**: Log defined for all elements except exact multiples of e^{iπ}
20/// - **SU(2)**: Log defined for all elements except -I
21/// - **SU(3)**: Log defined in a neighborhood of the identity
22/// - **SO(3)**: Log defined for rotations with angle < π
23///
24/// # Physical Interpretation
25///
26/// In lattice gauge theory, when computing curvature F = log(U_□) from
27/// the Wilson loop U_□:
28///
29/// - If U_□ ≈ I (small plaquette, weak field), log is well-defined
30/// - If U_□ far from I (large plaquette, strong field), log may fail
31/// - Solution: Use smaller lattice spacing or smearing techniques
32#[derive(Debug, Clone, PartialEq)]
33#[non_exhaustive]
34pub enum LogError {
35    /// Element is too far from identity for logarithm to be computed
36    ///
37    /// The logarithm map is only guaranteed to exist in a neighborhood
38    /// of the identity. This error indicates the element is outside
39    /// that neighborhood.
40    NotNearIdentity {
41        /// Distance from identity
42        distance: f64,
43        /// Maximum allowed distance for this group
44        threshold: f64,
45    },
46
47    /// Element is exactly at a singularity of the log map
48    ///
49    /// For example:
50    /// - SU(2): element = -I (rotation by π with ambiguous axis)
51    /// - SO(3): rotation by exactly π (axis ambiguous)
52    Singularity {
53        /// Description of the singularity
54        reason: String,
55    },
56
57    /// Numerical precision insufficient for accurate logarithm
58    ///
59    /// When the element is very close to a singularity, numerical
60    /// errors can dominate. This indicates the computation would
61    /// be unreliable.
62    NumericalInstability {
63        /// Description of the instability
64        reason: String,
65    },
66}
67
68impl fmt::Display for LogError {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            LogError::NotNearIdentity {
72                distance,
73                threshold,
74            } => write!(
75                f,
76                "Element too far from identity for log: distance {:.6} exceeds threshold {:.6}",
77                distance, threshold
78            ),
79            LogError::Singularity { reason } => {
80                write!(f, "Logarithm undefined at singularity: {}", reason)
81            }
82            LogError::NumericalInstability { reason } => {
83                write!(f, "Logarithm numerically unstable: {}", reason)
84            }
85        }
86    }
87}
88
89impl std::error::Error for LogError {}
90
91/// Result type for Lie group logarithm operations
92pub type LogResult<T> = Result<T, LogError>;
93
94/// Errors that can occur during representation theory computations
95///
96/// Representation theory involves constructing explicit matrices for
97/// group actions on vector spaces. Some representations require complex
98/// tensor product constructions that may not be implemented.
99#[derive(Debug, Clone, PartialEq)]
100#[non_exhaustive]
101pub enum RepresentationError {
102    /// The requested representation is not yet implemented
103    ///
104    /// This occurs for higher-dimensional representations that require
105    /// tensor product construction or other advanced techniques.
106    UnsupportedRepresentation {
107        /// Description of the representation (e.g., Dynkin labels)
108        representation: String,
109        /// Reason why it's not supported
110        reason: String,
111    },
112
113    /// Invalid representation parameters
114    ///
115    /// For example, negative Dynkin labels or inconsistent dimensions.
116    InvalidParameters {
117        /// Description of the invalid parameters
118        description: String,
119    },
120
121    /// Integration method not implemented for this group
122    ///
123    /// Generic character integration requires group-specific Haar measure
124    /// sampling, which must be implemented separately for each Lie group.
125    UnsupportedIntegrationMethod {
126        /// The integration method attempted
127        method: String,
128        /// The group type (e.g., "generic `LieGroup`")
129        group: String,
130        /// Suggested alternative
131        suggestion: String,
132    },
133}
134
135impl fmt::Display for RepresentationError {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        match self {
138            RepresentationError::UnsupportedRepresentation {
139                representation,
140                reason,
141            } => write!(
142                f,
143                "Representation {} not supported: {}",
144                representation, reason
145            ),
146            RepresentationError::InvalidParameters { description } => {
147                write!(f, "Invalid representation parameters: {}", description)
148            }
149            RepresentationError::UnsupportedIntegrationMethod {
150                method,
151                group,
152                suggestion,
153            } => write!(
154                f,
155                "{} integration not implemented for {}. {}",
156                method, group, suggestion
157            ),
158        }
159    }
160}
161
162impl std::error::Error for RepresentationError {}
163
164/// Result type for representation theory operations
165pub type RepresentationResult<T> = Result<T, RepresentationError>;
166
167// ============================================================================
168// Logarithm Conditioning
169// ============================================================================
170
171/// Conditioning information for logarithm computations.
172///
173/// The logarithm map on Lie groups becomes ill-conditioned near the cut locus
174/// (e.g., θ → π for SU(2), where the rotation axis becomes ambiguous).
175/// This structure provides quantitative information about the reliability
176/// of the computed logarithm.
177///
178/// # Condition Number Interpretation
179///
180/// The condition number κ measures how sensitive the output is to input
181/// perturbations: `|δ(log U)|/|log U| ≤ κ · |δU|/|U|`
182///
183/// - κ ≈ 1: Well-conditioned, result is reliable
184/// - κ ~ 10: Mildly ill-conditioned, expect ~1 digit loss of precision
185/// - κ > 100: Severely ill-conditioned, result may be unreliable
186/// - κ = ∞: At singularity (cut locus), axis is undefined
187///
188/// # Example
189///
190/// ```ignore
191/// let (log_u, cond) = g.log_with_condition()?;
192/// if cond.is_well_conditioned() {
193///     // Safe to use log_u
194/// } else if cond.is_usable() {
195///     // Use with caution, reduced precision
196///     eprintln!("Warning: log condition number = {:.1}", cond.condition_number());
197/// } else {
198///     // Result unreliable
199///     return Err(...);
200/// }
201/// ```
202#[derive(Debug, Clone)]
203pub struct LogCondition {
204    /// Condition number for the logarithm computation.
205    ///
206    /// For SU(2), this is approximately 1/sin(θ/2) for rotation angle θ,
207    /// which diverges as θ → π (approaching the cut locus at -I).
208    condition_number: f64,
209
210    /// Rotation angle (for SO(3)/SU(2)) or equivalent metric.
211    ///
212    /// Useful for understanding why conditioning is poor.
213    angle: f64,
214
215    /// Distance from the cut locus (θ = 2π for SU(2), θ = π for SO(3)).
216    ///
217    /// Values close to 0 indicate the element is near the cut locus
218    /// where the logarithm is ill-defined or ill-conditioned.
219    distance_to_cut_locus: f64,
220
221    /// Quality assessment of the logarithm computation.
222    quality: LogQuality,
223}
224
225/// Quality levels for logarithm computations.
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227#[non_exhaustive]
228pub enum LogQuality {
229    /// Excellent conditioning: κ < 2, full precision expected
230    Excellent,
231    /// Good conditioning: κ < 10, reliable results
232    Good,
233    /// Acceptable conditioning: κ < 100, ~2 digits precision loss
234    Acceptable,
235    /// Poor conditioning: κ < 1000, significant precision loss
236    Poor,
237    /// At or near singularity, result unreliable
238    AtSingularity,
239}
240
241impl std::fmt::Display for LogQuality {
242    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243        match self {
244            LogQuality::Excellent => write!(f, "Excellent"),
245            LogQuality::Good => write!(f, "Good"),
246            LogQuality::Acceptable => write!(f, "Acceptable"),
247            LogQuality::Poor => write!(f, "Poor"),
248            LogQuality::AtSingularity => write!(f, "AtSingularity"),
249        }
250    }
251}
252
253impl LogCondition {
254    /// Create a new `LogCondition` from the rotation angle.
255    ///
256    /// For SU(2)/SO(3), the condition number is approximately 1/sin(θ/2)
257    /// where θ is the rotation angle.
258    pub fn from_angle(angle: f64) -> Self {
259        let half_angle = angle / 2.0;
260        let sin_half = half_angle.sin().abs();
261        // For SU(2), the cut locus is at θ = 2π (U = -I); for SO(3) it's at θ = π.
262        // We use 2π here as this is primarily used by SU(2)'s log_with_condition.
263        let distance_to_cut_locus = (2.0 * std::f64::consts::PI - angle).abs();
264
265        // Condition number: For extracting axis from sin(θ/2) division
266        // κ ≈ 1/sin(θ/2), but bounded by practical considerations
267        let condition_number = if sin_half > 1e-10 {
268            1.0 / sin_half
269        } else {
270            f64::INFINITY
271        };
272
273        let quality = if condition_number < 2.0 {
274            LogQuality::Excellent
275        } else if condition_number < 10.0 {
276            LogQuality::Good
277        } else if condition_number < 100.0 {
278            LogQuality::Acceptable
279        } else if condition_number < 1000.0 {
280            LogQuality::Poor
281        } else {
282            LogQuality::AtSingularity
283        };
284
285        Self {
286            condition_number,
287            angle,
288            distance_to_cut_locus,
289            quality,
290        }
291    }
292
293    /// Returns the condition number κ.
294    #[must_use]
295    pub fn condition_number(&self) -> f64 {
296        self.condition_number
297    }
298
299    /// Returns the rotation angle θ.
300    #[must_use]
301    pub fn angle(&self) -> f64 {
302        self.angle
303    }
304
305    /// Returns the distance from the cut locus.
306    #[must_use]
307    pub fn distance_to_cut_locus(&self) -> f64 {
308        self.distance_to_cut_locus
309    }
310
311    /// Returns the quality assessment.
312    #[must_use]
313    pub fn quality(&self) -> LogQuality {
314        self.quality
315    }
316
317    /// Returns true if the result is well-conditioned (κ < 10).
318    #[must_use]
319    pub fn is_well_conditioned(&self) -> bool {
320        matches!(self.quality, LogQuality::Excellent | LogQuality::Good)
321    }
322
323    /// Returns true if the result is usable (κ < 100).
324    #[must_use]
325    pub fn is_usable(&self) -> bool {
326        matches!(
327            self.quality,
328            LogQuality::Excellent | LogQuality::Good | LogQuality::Acceptable
329        )
330    }
331
332    /// Returns true if the result is at or near a singularity.
333    #[must_use]
334    pub fn is_singular(&self) -> bool {
335        matches!(self.quality, LogQuality::AtSingularity)
336    }
337}
338
339/// Result type for conditioned logarithm operations.
340pub type ConditionedLogResult<T> = Result<(T, LogCondition), LogError>;
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use std::f64::consts::PI;
346
347    // ========================================================================
348    // Condition Number Formula Tests
349    // ========================================================================
350
351    /// Verify condition number formula: κ = 1/sin(θ/2)
352    ///
353    /// The condition number captures the difficulty of extracting the rotation
354    /// axis from sin(θ/2). It diverges as θ → 0 (small angle, axis hard to
355    /// determine) and is well-conditioned near θ = π.
356    #[test]
357    fn test_condition_number_formula() {
358        // For θ = π/2, κ = 1/sin(π/4) = √2 ≈ 1.414
359        let cond_pi_2 = LogCondition::from_angle(PI / 2.0);
360        let expected = 1.0 / (PI / 4.0).sin();
361        assert!(
362            (cond_pi_2.condition_number() - expected).abs() < 1e-10,
363            "θ=π/2: got {}, expected {}",
364            cond_pi_2.condition_number(),
365            expected
366        );
367        assert_eq!(cond_pi_2.quality(), LogQuality::Excellent);
368
369        // For θ = π/3, κ = 1/sin(π/6) = 2.0 (at boundary, classified as Good)
370        let cond_pi_3 = LogCondition::from_angle(PI / 3.0);
371        let expected = 1.0 / (PI / 6.0).sin();
372        assert!(
373            (cond_pi_3.condition_number() - expected).abs() < 1e-10,
374            "θ=π/3: got {}, expected {}",
375            cond_pi_3.condition_number(),
376            expected
377        );
378        // κ = 2.0 exactly, which is at the boundary (κ < 2 is Excellent, κ ≥ 2 is Good)
379        assert_eq!(cond_pi_3.quality(), LogQuality::Good);
380
381        // For θ = 0.1, κ = 1/sin(0.05) ≈ 20.0
382        let cond_small = LogCondition::from_angle(0.1);
383        let expected = 1.0 / (0.05_f64).sin();
384        assert!(
385            (cond_small.condition_number() - expected).abs() < 1e-8,
386            "θ=0.1: got {}, expected {}",
387            cond_small.condition_number(),
388            expected
389        );
390    }
391
392    /// Verify quality classification thresholds
393    #[test]
394    fn test_quality_classification() {
395        // Excellent: κ < 2 → need sin(θ/2) > 0.5 → θ/2 > π/6 → θ > π/3
396        let excellent = LogCondition::from_angle(PI * 0.7); // θ/2 = 0.35π, sin ≈ 0.89
397        assert_eq!(excellent.quality(), LogQuality::Excellent);
398        assert!(excellent.is_well_conditioned());
399        assert!(excellent.is_usable());
400
401        // Good: 2 ≤ κ < 10 → 0.1 < sin(θ/2) ≤ 0.5
402        let good = LogCondition::from_angle(PI * 0.3); // θ/2 = 0.15π, sin ≈ 0.45
403        assert_eq!(good.quality(), LogQuality::Good);
404        assert!(good.is_well_conditioned());
405
406        // Acceptable: 10 ≤ κ < 100 → 0.01 < sin(θ/2) ≤ 0.1
407        let acceptable = LogCondition::from_angle(0.1); // θ/2 = 0.05, sin ≈ 0.05
408        assert_eq!(acceptable.quality(), LogQuality::Acceptable);
409        assert!(!acceptable.is_well_conditioned());
410        assert!(acceptable.is_usable());
411
412        // Poor: 100 ≤ κ < 1000 → 0.001 < sin(θ/2) ≤ 0.01
413        let poor = LogCondition::from_angle(0.01); // θ/2 = 0.005, sin ≈ 0.005
414        assert_eq!(poor.quality(), LogQuality::Poor);
415        assert!(!poor.is_well_conditioned());
416        assert!(!poor.is_usable());
417
418        // AtSingularity: κ ≥ 1000 → sin(θ/2) ≤ 0.001
419        let singular = LogCondition::from_angle(0.001); // θ/2 = 0.0005, sin ≈ 0.0005
420        assert_eq!(singular.quality(), LogQuality::AtSingularity);
421        assert!(singular.is_singular());
422    }
423
424    /// Verify distance to cut locus computation
425    ///
426    /// For SU(2), the cut locus is at θ = 2π (U = -I), not θ = π.
427    #[test]
428    fn test_distance_to_cut_locus() {
429        // At θ = π/2, distance to cut locus = 2π - π/2 = 3π/2
430        let cond = LogCondition::from_angle(PI / 2.0);
431        assert!(
432            (cond.distance_to_cut_locus() - 3.0 * PI / 2.0).abs() < 1e-10,
433            "Distance to cut locus mismatch"
434        );
435
436        // At θ = 2π - 0.1, distance = 0.1
437        let cond_near = LogCondition::from_angle(2.0 * PI - 0.1);
438        assert!(
439            (cond_near.distance_to_cut_locus() - 0.1).abs() < 1e-10,
440            "Near cut locus distance mismatch"
441        );
442
443        // At θ ≈ 0, distance ≈ 2π
444        let cond_far = LogCondition::from_angle(0.01);
445        assert!(
446            (cond_far.distance_to_cut_locus() - (2.0 * PI - 0.01)).abs() < 1e-10,
447            "Far from cut locus distance mismatch"
448        );
449    }
450
451    /// Verify behavior near θ = 0 (where axis extraction becomes ill-conditioned)
452    ///
453    /// The condition number κ = 1/sin(θ/2) diverges at both θ → 0 and θ → 2π.
454    /// Near θ = 0, the rotation is near-identity; near θ = 2π, U → -I.
455    #[test]
456    fn test_small_angle_conditioning() {
457        // As θ → 0, condition number → ∞
458        let small_angles = [0.5, 0.1, 0.01, 0.001, 0.0001];
459        let mut prev_kappa = 0.0;
460
461        for &angle in &small_angles {
462            let cond = LogCondition::from_angle(angle);
463
464            // Condition number should increase monotonically as angle decreases
465            assert!(
466                cond.condition_number() > prev_kappa,
467                "Condition number should increase as θ→0: θ={}, κ={}",
468                angle,
469                cond.condition_number()
470            );
471            prev_kappa = cond.condition_number();
472        }
473
474        // At θ = 1e-11, should be effectively singular
475        let cond_singular = LogCondition::from_angle(1e-11);
476        assert_eq!(cond_singular.quality(), LogQuality::AtSingularity);
477    }
478
479    /// Verify behavior near θ = π and θ = 2π
480    ///
481    /// For SU(2): θ = π is well-conditioned (sin(π/2) = 1, axis extractable).
482    /// The true cut locus is at θ = 2π (U = -I, sin(π) = 0).
483    #[test]
484    fn test_near_cut_locus() {
485        // At θ = π, κ = 1/sin(π/2) = 1 (well-conditioned!)
486        let cond_pi = LogCondition::from_angle(PI);
487        assert!(
488            (cond_pi.condition_number() - 1.0).abs() < 1e-10,
489            "At θ=π, κ should be 1: got {}",
490            cond_pi.condition_number()
491        );
492        assert_eq!(cond_pi.quality(), LogQuality::Excellent);
493
494        // At θ = π - 0.1, still well-conditioned
495        let cond_near_pi = LogCondition::from_angle(PI - 0.1);
496        assert!(cond_near_pi.is_well_conditioned());
497
498        // Distance to cut locus at θ = π should be π (since cut locus is at 2π)
499        assert!(
500            (cond_pi.distance_to_cut_locus() - PI).abs() < 1e-10,
501            "Distance to cut locus at θ=π should be π: got {}",
502            cond_pi.distance_to_cut_locus()
503        );
504
505        // At θ = 2π, distance to cut locus should be 0
506        let cond_2pi = LogCondition::from_angle(2.0 * PI);
507        assert!(
508            cond_2pi.distance_to_cut_locus() < 1e-10,
509            "Distance to cut locus at θ=2π should be 0"
510        );
511    }
512
513    // ========================================================================
514    // Precision Loss Estimation Tests
515    // ========================================================================
516
517    /// Estimate actual precision loss based on condition number
518    ///
519    /// If κ is the condition number and ε is machine epsilon (~2.2e-16),
520    /// then the expected error bound is roughly κ·ε.
521    /// We expect to lose log10(κ) digits of precision.
522    #[test]
523    fn test_precision_loss_estimate() {
524        let machine_eps = f64::EPSILON;
525
526        // For θ = π/2: κ ≈ 1.41, expect ~0 digits lost
527        let cond_good = LogCondition::from_angle(PI / 2.0);
528        let expected_error = cond_good.condition_number() * machine_eps;
529        let digits_lost = cond_good.condition_number().log10();
530        assert!(digits_lost < 1.0, "Should lose < 1 digit at θ=π/2");
531        println!(
532            "θ=π/2: κ={:.2}, expected error={:.2e}, digits lost={:.2}",
533            cond_good.condition_number(),
534            expected_error,
535            digits_lost
536        );
537
538        // For θ = 0.1: κ ≈ 20, expect ~1.3 digits lost
539        let cond_moderate = LogCondition::from_angle(0.1);
540        let digits_lost = cond_moderate.condition_number().log10();
541        assert!(
542            digits_lost > 1.0 && digits_lost < 2.0,
543            "Should lose ~1-2 digits at θ=0.1"
544        );
545        println!(
546            "θ=0.1: κ={:.2}, digits lost={:.2}",
547            cond_moderate.condition_number(),
548            digits_lost
549        );
550
551        // For θ = 0.01: κ ≈ 200, expect ~2.3 digits lost
552        let cond_poor = LogCondition::from_angle(0.01);
553        let digits_lost = cond_poor.condition_number().log10();
554        assert!(
555            digits_lost > 2.0 && digits_lost < 3.0,
556            "Should lose ~2-3 digits at θ=0.01"
557        );
558        println!(
559            "θ=0.01: κ={:.2}, digits lost={:.2}",
560            cond_poor.condition_number(),
561            digits_lost
562        );
563    }
564
565    // ========================================================================
566    // LogError Tests
567    // ========================================================================
568
569    #[test]
570    fn test_log_error_display() {
571        let err = LogError::NotNearIdentity {
572            distance: 2.5,
573            threshold: 1.0,
574        };
575        let msg = format!("{}", err);
576        assert!(msg.contains("2.5"));
577        assert!(msg.contains("1.0"));
578
579        let err2 = LogError::Singularity {
580            reason: "rotation by π".to_string(),
581        };
582        let msg2 = format!("{}", err2);
583        assert!(msg2.contains("rotation by π"));
584    }
585
586    #[test]
587    fn test_representation_error_display() {
588        let err = RepresentationError::UnsupportedRepresentation {
589            representation: "(3,3)".to_string(),
590            reason: "too high dimensional".to_string(),
591        };
592        let msg = format!("{}", err);
593        assert!(msg.contains("(3,3)"));
594        assert!(msg.contains("too high dimensional"));
595    }
596}