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}