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