lie_groups/u1.rs
1//! U(1): The Circle Group
2//!
3//! This module implements U(1), the group of complex numbers with unit modulus.
4//! U(1) appears as the gauge group of electromagnetism and in many other contexts.
5//!
6//! # Mathematical Background
7//!
8//! ## Definition
9//!
10//! ```text
11//! U(1) = { e^{iθ} ∈ ℂ | θ ∈ ℝ } ≅ S¹
12//! ```
13//!
14//! The circle group: complex numbers z with |z| = 1, isomorphic to the unit circle.
15//!
16//! ## Group Structure
17//!
18//! - **Multiplication**: e^{iθ₁} · e^{iθ₂} = e^{i(θ₁+θ₂)}
19//! - **Identity**: e^{i·0} = 1
20//! - **Inverse**: (e^{iθ})^{-1} = e^{-iθ}
21//! - **Abelian**: e^{iθ₁} · e^{iθ₂} = e^{iθ₂} · e^{iθ₁}
22//!
23//! ## Lie Algebra
24//!
25//! ```text
26//! u(1) = iℝ (purely imaginary numbers)
27//! Exponential map: exp(ia) = e^{ia} for a ∈ ℝ
28//! ```
29//!
30//! ## Logarithm and Branch Cuts
31//!
32//! The logarithm map log: U(1) → u(1) is **multivalued** - for any θ, the values
33//! θ, θ + 2π, θ + 4π, ... all represent the same group element e^{iθ}.
34//!
35//! **Principal Branch**: We use **θ ∈ [0, 2π)** with branch cut at θ = 0.
36//!
37//! ### Why [0, 2π) instead of (-π, π]?
38//!
39//! | Choice | Branch Cut | Pros | Cons |
40//! |--------|------------|------|------|
41//! | **[0, 2π)** (ours) | At θ = 0 (positive real axis) | Natural for angles, matches `from_angle()` normalization | Non-standard in complex analysis |
42//! | (-π, π] (std) | At θ = π (negative real axis) | Standard in complex analysis | Asymmetric around identity |
43//!
44//! We chose [0, 2π) for **consistency**: our internal representation normalizes
45//! angles to [0, 2π) in `from_angle()`, so `log(g)` returns exactly the stored value.
46//!
47//! ### Discontinuity Example
48//!
49//! ```text
50//! lim_{ε→0⁺} log(e^{i(2π-ε)}) = 2π - ε → 2π
51//! lim_{ε→0⁺} log(e^{iε}) = ε → 0
52//!
53//! Jump discontinuity of 2π at θ = 0 (the identity).
54//! ```
55//!
56//! ### Practical Impact
57//!
58//! - **Gauge fixing**: When computing holonomy defects `∫ log(g)`, discontinuities
59//! at branch cuts can cause artificial jumps in the objective function.
60//! - **Unwrapping**: For smooth paths that cross θ = 0, consider phase unwrapping
61//! algorithms that track accumulated 2π crossings.
62//! - **Optimization**: If optimizing over U(1) connections, use angle differences
63//! `Δθ = θ₂ - θ₁` which are smooth, rather than logarithms.
64//!
65//! ## Physical Interpretation
66//!
67//! ### Electromagnetism
68//!
69//! - **Gauge field**: Vector potential `A_μ(x)`
70//! - **Field strength**: `F_μν` = ∂_μ `A_ν` - ∂_ν `A_μ` (electromagnetic field)
71//! - **Gauge transformation**: `A_μ` → `A_μ` + ∂_μ λ for λ: M → ℝ
72//! - **Connection**: U(x,y) = exp(i ∫_x^y `A_μ` dx^μ) ∈ U(1)
73//!
74//! ### Wilson Loops
75//!
76//! For a closed path C:
77//! ```text
78//! W_C = exp(i ∮_C A_μ dx^μ) = exp(i Φ)
79//! ```
80//! where Φ is the magnetic flux through C (Aharonov-Bohm phase).
81//!
82//! ## Applications
83//!
84//! 1. **Lattice QED**: Discretized electromagnetic field theory
85//! 2. **Phase synchronization**: Kuramoto model, coupled oscillators
86//! 3. **Superconductivity**: Order parameter ψ = |ψ|e^{iθ}, θ ∈ U(1)
87//! 4. **XY model**: Classical spin system with continuous symmetry
88//! 5. **Clock synchronization**: Distributed network of oscillators
89
90use crate::traits::AntiHermitianByConstruction;
91use crate::{LieAlgebra, LieGroup};
92use num_complex::Complex;
93use std::f64::consts::PI;
94use std::fmt;
95use std::ops::{Add, Mul, MulAssign, Neg, Sub};
96
97/// Lie algebra u(1) ≅ ℝ
98///
99/// The Lie algebra of U(1) consists of purely imaginary numbers i·a where a ∈ ℝ.
100/// We represent this as a 1-dimensional real vector space.
101///
102/// # Mathematical Structure
103///
104/// Elements of u(1) have the form:
105/// ```text
106/// X = i·a where a ∈ ℝ
107/// ```
108///
109/// The exponential map is simply:
110/// ```text
111/// exp(i·a) = e^{ia} ∈ U(1)
112/// ```
113///
114/// # Examples
115///
116/// ```
117/// use lie_groups::u1::U1Algebra;
118/// use lie_groups::traits::LieAlgebra;
119///
120/// // Create algebra element
121/// let v = U1Algebra::from_components(&[1.5]);
122///
123/// // Scale
124/// let w = v.scale(2.0);
125/// assert_eq!(w.value(), 3.0);
126/// ```
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct U1Algebra(pub f64);
129
130impl Add for U1Algebra {
131 type Output = Self;
132 fn add(self, rhs: Self) -> Self {
133 Self(self.0 + rhs.0)
134 }
135}
136
137impl Add<&U1Algebra> for U1Algebra {
138 type Output = U1Algebra;
139 fn add(self, rhs: &U1Algebra) -> U1Algebra {
140 self + *rhs
141 }
142}
143
144impl Add<U1Algebra> for &U1Algebra {
145 type Output = U1Algebra;
146 fn add(self, rhs: U1Algebra) -> U1Algebra {
147 *self + rhs
148 }
149}
150
151impl Add<&U1Algebra> for &U1Algebra {
152 type Output = U1Algebra;
153 fn add(self, rhs: &U1Algebra) -> U1Algebra {
154 *self + *rhs
155 }
156}
157
158impl Sub for U1Algebra {
159 type Output = Self;
160 fn sub(self, rhs: Self) -> Self {
161 Self(self.0 - rhs.0)
162 }
163}
164
165impl Neg for U1Algebra {
166 type Output = Self;
167 fn neg(self) -> Self {
168 Self(-self.0)
169 }
170}
171
172impl Mul<f64> for U1Algebra {
173 type Output = Self;
174 fn mul(self, scalar: f64) -> Self {
175 Self(self.0 * scalar)
176 }
177}
178
179impl Mul<U1Algebra> for f64 {
180 type Output = U1Algebra;
181 fn mul(self, rhs: U1Algebra) -> U1Algebra {
182 rhs * self
183 }
184}
185
186impl U1Algebra {
187 /// Get the real value a from the algebra element i·a
188 #[inline]
189 pub fn value(&self) -> f64 {
190 self.0
191 }
192}
193
194impl LieAlgebra for U1Algebra {
195 const DIM: usize = 1;
196
197 #[inline]
198 fn zero() -> Self {
199 Self(0.0)
200 }
201
202 #[inline]
203 fn add(&self, other: &Self) -> Self {
204 Self(self.0 + other.0)
205 }
206
207 #[inline]
208 fn scale(&self, scalar: f64) -> Self {
209 Self(self.0 * scalar)
210 }
211
212 #[inline]
213 fn norm(&self) -> f64 {
214 self.0.abs()
215 }
216
217 #[inline]
218 fn basis_element(i: usize) -> Self {
219 assert_eq!(i, 0, "U(1) algebra is 1-dimensional");
220 Self(1.0)
221 }
222
223 #[inline]
224 fn from_components(components: &[f64]) -> Self {
225 assert_eq!(components.len(), 1, "u(1) has dimension 1");
226 Self(components[0])
227 }
228
229 #[inline]
230 fn to_components(&self) -> Vec<f64> {
231 vec![self.0]
232 }
233
234 // u(1) is abelian: [X, Y] = 0 for all X, Y.
235 // Stated explicitly rather than inherited from the trait default,
236 // so the algebraic property is visible at the definition site.
237 #[inline]
238 fn bracket(&self, _other: &Self) -> Self {
239 Self::zero()
240 }
241}
242
243/// An element of U(1), the circle group
244///
245/// Represented as a phase angle θ ∈ [0, 2π), corresponding to e^{iθ}.
246///
247/// # Representation
248///
249/// We use the angle representation rather than storing the complex number
250/// directly to avoid floating-point accumulation errors and to make the
251/// group structure (angle addition) explicit.
252///
253/// # Examples
254///
255/// ```rust
256/// use lie_groups::{LieGroup, U1};
257/// use std::f64::consts::PI;
258///
259/// // Create elements
260/// let g = U1::from_angle(0.5);
261/// let h = U1::from_angle(0.3);
262///
263/// // Group multiplication (angle addition)
264/// let product = g.compose(&h);
265/// assert!((product.angle() - 0.8).abs() < 1e-10);
266///
267/// // Inverse (angle negation)
268/// let g_inv = g.inverse();
269/// assert!((g_inv.angle() - (2.0 * PI - 0.5)).abs() < 1e-10);
270/// ```
271#[derive(Clone, Copy, Debug, PartialEq)]
272pub struct U1 {
273 /// Phase angle θ ∈ [0, 2π)
274 ///
275 /// Represents the group element e^{iθ}
276 theta: f64,
277}
278
279impl U1 {
280 /// Create U(1) element from angle in radians
281 ///
282 /// Automatically normalizes to [0, 2π)
283 ///
284 /// # Arguments
285 ///
286 /// * `theta` - Phase angle in radians (any real number)
287 ///
288 /// # Examples
289 ///
290 /// ```rust
291 /// use lie_groups::U1;
292 ///
293 /// let g = U1::from_angle(0.5);
294 /// assert!((g.angle() - 0.5).abs() < 1e-10);
295 ///
296 /// // Normalization
297 /// let h = U1::from_angle(2.0 * std::f64::consts::PI + 0.3);
298 /// assert!((h.angle() - 0.3).abs() < 1e-10);
299 /// ```
300 #[must_use]
301 pub fn from_angle(theta: f64) -> Self {
302 Self {
303 theta: theta.rem_euclid(2.0 * PI),
304 }
305 }
306
307 /// Create U(1) element from complex number
308 ///
309 /// Extracts the phase angle from z = re^{iθ}, ignoring the magnitude.
310 ///
311 /// # Arguments
312 ///
313 /// * `z` - Complex number (does not need to have unit modulus)
314 ///
315 /// # Examples
316 ///
317 /// ```rust
318 /// use lie_groups::U1;
319 /// use num_complex::Complex;
320 ///
321 /// let z = Complex::new(0.0, 1.0); // i
322 /// let g = U1::from_complex(z);
323 /// assert!((g.angle() - std::f64::consts::PI / 2.0).abs() < 1e-10);
324 /// ```
325 #[must_use]
326 pub fn from_complex(z: Complex<f64>) -> Self {
327 Self::from_angle(z.arg())
328 }
329
330 /// Get the phase angle θ ∈ [0, 2π)
331 ///
332 /// # Examples
333 ///
334 /// ```rust
335 /// use lie_groups::U1;
336 ///
337 /// let g = U1::from_angle(1.5);
338 /// assert!((g.angle() - 1.5).abs() < 1e-10);
339 /// ```
340 #[must_use]
341 pub fn angle(&self) -> f64 {
342 self.theta
343 }
344
345 /// Convert to complex number e^{iθ}
346 ///
347 /// Returns the complex number representation with unit modulus.
348 ///
349 /// # Examples
350 ///
351 /// ```rust
352 /// use lie_groups::U1;
353 ///
354 /// let g = U1::from_angle(std::f64::consts::PI / 2.0); // π/2
355 /// let z = g.to_complex();
356 ///
357 /// assert!(z.re.abs() < 1e-10); // cos(π/2) ≈ 0
358 /// assert!((z.im - 1.0).abs() < 1e-10); // sin(π/2) = 1
359 /// assert!((z.norm() - 1.0).abs() < 1e-10); // |z| = 1
360 /// ```
361 #[must_use]
362 pub fn to_complex(&self) -> Complex<f64> {
363 Complex::new(self.theta.cos(), self.theta.sin())
364 }
365
366 /// Trace of the 1×1 matrix representation
367 ///
368 /// For U(1), the trace is just the complex number itself: Tr(e^{iθ}) = e^{iθ}
369 ///
370 /// # Examples
371 ///
372 /// ```rust
373 /// use lie_groups::U1;
374 ///
375 /// let g = U1::from_angle(0.0); // Identity
376 /// let tr = g.trace_complex();
377 ///
378 /// assert!((tr.re - 1.0).abs() < 1e-10);
379 /// assert!(tr.im.abs() < 1e-10);
380 /// ```
381 #[must_use]
382 pub fn trace_complex(&self) -> Complex<f64> {
383 self.to_complex()
384 }
385
386 /// Rotation generators for lattice updates
387 ///
388 /// Returns a small U(1) element for MCMC proposals
389 ///
390 /// # Arguments
391 ///
392 /// * `magnitude` - Step size in radians
393 ///
394 /// # Examples
395 ///
396 /// ```rust
397 /// use lie_groups::U1;
398 ///
399 /// let perturbation = U1::rotation(0.1);
400 /// assert!((perturbation.angle() - 0.1).abs() < 1e-10);
401 /// ```
402 #[must_use]
403 pub fn rotation(magnitude: f64) -> Self {
404 Self::from_angle(magnitude)
405 }
406
407 /// Random U(1) element uniformly distributed on the circle
408 ///
409 /// Requires the `rand` feature (enabled by default).
410 /// Samples θ uniformly from [0, 2π).
411 ///
412 /// # Examples
413 ///
414 /// ```rust
415 /// use lie_groups::U1;
416 /// use rand::SeedableRng;
417 ///
418 /// let mut rng = rand::rngs::StdRng::seed_from_u64(42);
419 /// let g = U1::random(&mut rng);
420 ///
421 /// assert!(g.angle() >= 0.0 && g.angle() < 2.0 * std::f64::consts::PI);
422 /// ```
423 #[cfg(feature = "rand")]
424 #[must_use]
425 pub fn random<R: rand::Rng>(rng: &mut R) -> Self {
426 use rand::distributions::{Distribution, Uniform};
427 let dist = Uniform::new(0.0, 2.0 * PI);
428 Self::from_angle(dist.sample(rng))
429 }
430
431 /// Random small perturbation for MCMC proposals
432 ///
433 /// Samples θ uniformly from [-`step_size`, +`step_size`]
434 ///
435 /// # Arguments
436 ///
437 /// * `step_size` - Maximum angle deviation
438 ///
439 /// # Examples
440 ///
441 /// ```rust
442 /// use lie_groups::{U1, LieGroup};
443 /// use rand::SeedableRng;
444 ///
445 /// let mut rng = rand::rngs::StdRng::seed_from_u64(42);
446 /// let delta = U1::random_small(0.1, &mut rng);
447 ///
448 /// // Should be close to identity
449 /// assert!(delta.distance_to_identity() <= 0.1);
450 /// ```
451 #[cfg(feature = "rand")]
452 #[must_use]
453 pub fn random_small<R: rand::Rng>(step_size: f64, rng: &mut R) -> Self {
454 use rand::distributions::{Distribution, Uniform};
455 let dist = Uniform::new(-step_size, step_size);
456 let angle = dist.sample(rng);
457 Self::from_angle(angle)
458 }
459}
460
461impl fmt::Display for U1Algebra {
462 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463 write!(f, "u(1)({:.4})", self.0)
464 }
465}
466
467/// Group multiplication: g₁ · g₂ (phase addition)
468impl Mul<&U1> for &U1 {
469 type Output = U1;
470 fn mul(self, rhs: &U1) -> U1 {
471 self.compose(rhs)
472 }
473}
474
475impl Mul<&U1> for U1 {
476 type Output = U1;
477 fn mul(self, rhs: &U1) -> U1 {
478 self.compose(rhs)
479 }
480}
481
482impl MulAssign<&U1> for U1 {
483 fn mul_assign(&mut self, rhs: &U1) {
484 *self = self.compose(rhs);
485 }
486}
487
488impl LieGroup for U1 {
489 const DIM: usize = 1;
490
491 type Algebra = U1Algebra;
492
493 fn identity() -> Self {
494 Self { theta: 0.0 }
495 }
496
497 fn compose(&self, other: &Self) -> Self {
498 Self::from_angle(self.theta + other.theta)
499 }
500
501 fn inverse(&self) -> Self {
502 Self::from_angle(-self.theta)
503 }
504
505 fn adjoint(&self) -> Self {
506 // For U(1) (abelian group), adjoint = complex conjugate = inverse
507 self.inverse()
508 }
509
510 fn adjoint_action(&self, algebra_element: &U1Algebra) -> U1Algebra {
511 // For abelian groups, the adjoint representation is trivial:
512 // Ad_g(X) = g X g⁻¹ = X (since everything commutes)
513 //
514 // Mathematically: For U(1), [g, X] = 0 for all g, X
515 // Therefore: g X g⁻¹ = g g⁻¹ X = X
516 *algebra_element
517 }
518
519 fn distance_to_identity(&self) -> f64 {
520 // Shortest arc distance on circle
521 let normalized = self.theta.rem_euclid(2.0 * PI);
522 let dist = normalized.min(2.0 * PI - normalized);
523 dist.abs()
524 }
525
526 fn exp(tangent: &U1Algebra) -> Self {
527 // exp(i·a) = e^{ia}
528 // Represented as angle a (mod 2π)
529 Self::from_angle(tangent.0)
530 }
531
532 fn log(&self) -> crate::error::LogResult<U1Algebra> {
533 // For U(1), log(e^{iθ}) = iθ
534 //
535 // **Principal Branch Choice**: θ ∈ [0, 2π) with branch cut at θ = 0
536 //
537 // See module documentation for detailed discussion of branch cuts.
538 //
539 // # Mathematical Properties
540 //
541 // - **Surjectivity**: Every algebra element a ∈ [0, 2π) has exp(ia) ∈ U(1)
542 // - **Branch cut**: Discontinuity at θ = 0 (positive real axis, identity)
543 // - **Consistency**: Returns exactly the normalized angle from `from_angle()`
544 //
545 // # Examples
546 //
547 // ```
548 // use lie_groups::{LieGroup, U1};
549 // use std::f64::consts::PI;
550 //
551 // // Identity
552 // let e = U1::identity();
553 // assert!(e.log().unwrap().value().abs() < 1e-14);
554 //
555 // // π/2
556 // let g = U1::from_angle(PI / 2.0);
557 // assert!((g.log().unwrap().value() - PI / 2.0).abs() < 1e-14);
558 //
559 // // Branch cut demonstration: values near 2π
560 // let g1 = U1::from_angle(2.0 * PI - 0.01); // Just before branch cut
561 // let g2 = U1::from_angle(0.01); // Just after branch cut
562 //
563 // // log values jump discontinuously
564 // assert!(g1.log().unwrap().value() > 6.0); // ≈ 2π - 0.01
565 // assert!(g2.log().unwrap().value() < 0.1); // ≈ 0.01
566 // // Jump size ≈ 2π
567 // ```
568 //
569 // # Design Rationale
570 //
571 // Unlike SU(2) or SU(3), where log() can fail for elements far from identity
572 // (returning `Err`), U(1) log is **always defined** because:
573 // 1. U(1) is 1-dimensional (no exponential map singularities)
574 // 2. Our angle representation is already the logarithm
575 // 3. The covering map exp: ℝ → U(1) is a local diffeomorphism everywhere
576 //
577 // However, users should be aware of the **discontinuity** at θ = 0 when
578 // using log in optimization or when computing angle differences.
579 Ok(U1Algebra(self.theta))
580 }
581
582 fn dim() -> usize {
583 1 // U(1) is 1-dimensional (complex numbers as 1×1 matrices)
584 }
585
586 fn trace(&self) -> Complex<f64> {
587 // Trace of e^{iθ} viewed as 1×1 matrix
588 // Tr([e^{iθ}]) = e^{iθ}
589 Complex::new(self.theta.cos(), self.theta.sin())
590 }
591}
592
593/// Display implementation shows the phase angle
594impl std::fmt::Display for U1 {
595 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
596 write!(f, "U1(θ={:.4})", self.theta)
597 }
598}
599
600// ============================================================================
601// Mathematical Property Implementations for U(1)
602// ============================================================================
603
604use crate::traits::{Abelian, Compact};
605
606/// U(1) is compact.
607///
608/// The circle group {e^{iθ} : θ ∈ ℝ} is diffeomorphic to S¹.
609/// All elements have unit modulus: |e^{iθ}| = 1.
610///
611/// # Topological Structure
612///
613/// U(1) ≅ SO(2) ≅ S¹ (the unit circle)
614/// - Closed and bounded in ℂ
615/// - Every sequence has a convergent subsequence (Bolzano-Weierstrass)
616/// - Admits a finite Haar measure
617///
618/// # Physical Significance
619///
620/// Compactness of U(1) ensures:
621/// - Well-defined Yang-Mills action
622/// - Completely reducible representations
623/// - Quantization of electric charge
624impl Compact for U1 {}
625
626/// U(1) is abelian.
627///
628/// Complex multiplication commutes: e^{iα} · e^{iβ} = e^{iβ} · e^{iα}
629///
630/// # Mathematical Structure
631///
632/// For all g, h ∈ U(1):
633/// ```text
634/// g · h = e^{i(α+β)} = e^{i(β+α)} = h · g
635/// ```
636///
637/// This makes U(1) the structure group for **Maxwell's electromagnetism**.
638///
639/// # Type Safety Example
640///
641/// ```ignore
642/// // This function requires an abelian gauge group
643/// fn compute_chern_number<G: Abelian>(conn: &NetworkConnection<G>) -> i32 {
644/// // Chern class computation requires commutativity
645/// }
646///
647/// // ✅ Compiles: U(1) is abelian
648/// let u1_conn = NetworkConnection::<U1>::new(graph);
649/// compute_chern_number(&u1_conn);
650///
651/// // ❌ Won't compile: SU(2) is not abelian
652/// let su2_conn = NetworkConnection::<SU2>::new(graph);
653/// // compute_chern_number(&su2_conn); // Compile error!
654/// ```
655///
656/// # Physical Significance
657///
658/// - Gauge transformations commute (no self-interaction)
659/// - Field strength is linear: F = dA (no `[A,A]` term)
660/// - Maxwell's equations are linear PDEs
661/// - Superposition principle holds
662impl Abelian for U1 {}
663
664// Note: U(1) is NOT Simple or SemiSimple
665// - Abelian groups (except trivial group) are not simple
666// - They contain proper normal subgroups
667// - The center Z(U(1)) = U(1) itself
668
669// ============================================================================
670// Algebra Marker Traits
671// ============================================================================
672
673/// u(1) algebra elements are anti-Hermitian by construction.
674///
675/// The representation `U1Algebra(f64)` stores a real number `a` representing
676/// the purely imaginary element `ia ∈ iℝ ⊂ ℂ`. Since `(ia)* = -ia`, these
677/// are anti-Hermitian (as 1×1 complex matrices).
678///
679/// # Note: NOT `TracelessByConstruction`
680///
681/// Unlike su(n), the u(1) algebra is NOT traceless. For a 1×1 matrix `[ia]`,
682/// the trace is ia ≠ 0 (for a ≠ 0). This is why U(1) ≠ SU(1).
683///
684/// The theorem `det(exp(X)) = exp(tr(X))` still holds, but gives:
685/// - det(exp(ia)) = exp(ia) ≠ 1 in general
686/// - This is correct: U(1) elements have |det| = 1, not det = 1
687impl AntiHermitianByConstruction for U1Algebra {}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use rand::SeedableRng;
693
694 #[test]
695 fn test_from_angle() {
696 let g = U1::from_angle(0.5);
697 assert!((g.angle() - 0.5).abs() < 1e-10);
698
699 // Test normalization
700 let h = U1::from_angle(2.0 * PI + 0.3);
701 assert!((h.angle() - 0.3).abs() < 1e-10);
702
703 // Negative angles
704 let k = U1::from_angle(-0.5);
705 assert!((k.angle() - (2.0 * PI - 0.5)).abs() < 1e-10);
706 }
707
708 #[test]
709 fn test_to_complex() {
710 // Identity
711 let e = U1::identity();
712 let z = e.to_complex();
713 assert!((z.re - 1.0).abs() < 1e-10);
714 assert!(z.im.abs() < 1e-10);
715
716 // π/2
717 let g = U1::from_angle(PI / 2.0);
718 let z = g.to_complex();
719 assert!(z.re.abs() < 1e-10);
720 assert!((z.im - 1.0).abs() < 1e-10);
721
722 // All should have unit modulus
723 let h = U1::from_angle(1.234);
724 assert!((h.to_complex().norm() - 1.0).abs() < 1e-10);
725 }
726
727 #[test]
728 fn test_group_identity() {
729 let g = U1::from_angle(1.5);
730 let e = U1::identity();
731
732 let g_e = g.compose(&e);
733 let e_g = e.compose(&g);
734
735 assert!((g_e.angle() - g.angle()).abs() < 1e-10);
736 assert!((e_g.angle() - g.angle()).abs() < 1e-10);
737 }
738
739 #[test]
740 fn test_group_inverse() {
741 let g = U1::from_angle(1.2);
742 let g_inv = g.inverse();
743 let product = g.compose(&g_inv);
744
745 assert!(product.is_near_identity(1e-10));
746
747 // Double inverse
748 let g_inv_inv = g_inv.inverse();
749 assert!((g_inv_inv.angle() - g.angle()).abs() < 1e-10);
750 }
751
752 #[test]
753 fn test_group_associativity() {
754 let g1 = U1::from_angle(0.5);
755 let g2 = U1::from_angle(1.2);
756 let g3 = U1::from_angle(0.8);
757
758 let left = g1.compose(&g2.compose(&g3));
759 let right = g1.compose(&g2).compose(&g3);
760
761 assert!((left.angle() - right.angle()).abs() < 1e-10);
762 }
763
764 #[test]
765 fn test_commutativity() {
766 // U(1) is abelian
767 let g = U1::from_angle(0.7);
768 let h = U1::from_angle(1.3);
769
770 let gh = g.compose(&h);
771 let hg = h.compose(&g);
772
773 assert!((gh.angle() - hg.angle()).abs() < 1e-10);
774 }
775
776 #[test]
777 fn test_distance_to_identity() {
778 let e = U1::identity();
779 assert!(e.distance_to_identity().abs() < 1e-10);
780
781 // Small angle
782 let g1 = U1::from_angle(0.1);
783 assert!((g1.distance_to_identity() - 0.1).abs() < 1e-10);
784
785 // Angle > π should take shorter arc
786 let g2 = U1::from_angle(1.9 * PI);
787 assert!((g2.distance_to_identity() - 0.1 * PI).abs() < 1e-10);
788 }
789
790 #[test]
791 fn test_distance_symmetry() {
792 let g = U1::from_angle(1.5);
793 let h = U1::from_angle(0.8);
794
795 let d_gh = g.distance(&h);
796 let d_hg = h.distance(&g);
797
798 assert!((d_gh - d_hg).abs() < 1e-10);
799 }
800
801 #[test]
802 fn test_adjoint_equals_inverse() {
803 // For abelian U(1), adjoint = inverse
804 let g = U1::from_angle(1.7);
805 let adj = g.adjoint();
806 let inv = g.inverse();
807
808 assert!((adj.angle() - inv.angle()).abs() < 1e-10);
809 }
810
811 #[test]
812 #[cfg(feature = "rand")]
813 fn test_random_distribution() {
814 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
815
816 // Generate many random elements
817 let samples: Vec<U1> = (0..1000).map(|_| U1::random(&mut rng)).collect();
818
819 // Check all are in [0, 2π)
820 for g in &samples {
821 assert!(g.angle() >= 0.0 && g.angle() < 2.0 * PI);
822 }
823
824 // Check distribution is roughly uniform (basic test)
825 let mean_angle = samples.iter().map(super::U1::angle).sum::<f64>() / samples.len() as f64;
826 assert!((mean_angle - PI).abs() < 0.2); // Should be near π
827 }
828
829 #[test]
830 #[cfg(feature = "rand")]
831 fn test_random_small() {
832 let mut rng = rand::rngs::StdRng::seed_from_u64(123);
833 let step_size = 0.1;
834
835 let samples: Vec<U1> = (0..100)
836 .map(|_| U1::random_small(step_size, &mut rng))
837 .collect();
838
839 // Most should be close to identity
840 let close_to_identity = samples
841 .iter()
842 .filter(|g| g.distance_to_identity() < 0.3)
843 .count();
844
845 assert!(close_to_identity > 80); // At least 80% within 3σ
846 }
847
848 #[test]
849 fn test_trace() {
850 let g = U1::from_angle(PI / 3.0);
851 let tr = g.trace_complex();
852
853 // Tr(e^{iθ}) = e^{iθ}
854 let expected = g.to_complex();
855 assert!((tr.re - expected.re).abs() < 1e-10);
856 assert!((tr.im - expected.im).abs() < 1e-10);
857 }
858
859 // ========================================================================
860 // Logarithm Map Tests
861 // ========================================================================
862
863 #[test]
864 fn test_log_identity() {
865 use crate::traits::LieGroup;
866
867 let e = U1::identity();
868 let log_e = e.log().unwrap();
869
870 // log(identity) = 0
871 assert!(log_e.norm() < 1e-14);
872 }
873
874 #[test]
875 fn test_log_exp_roundtrip() {
876 use crate::traits::LieGroup;
877
878 // For small algebra elements, log(exp(X)) ≈ X
879 let x = U1Algebra(0.5);
880 let g = U1::exp(&x);
881 let x_recovered = g.log().unwrap();
882
883 // Should match original
884 assert!((x_recovered.0 - x.0).abs() < 1e-10);
885 }
886
887 #[test]
888 fn test_exp_log_roundtrip() {
889 use crate::traits::LieGroup;
890
891 // For group elements near identity, exp(log(g)) ≈ g
892 let g = U1::from_angle(0.7);
893 let x = g.log().unwrap();
894 let g_recovered = U1::exp(&x);
895
896 // Should match original
897 assert!((g_recovered.angle() - g.angle()).abs() < 1e-10);
898 }
899
900 #[test]
901 fn test_log_branch_cut_at_zero() {
902 use crate::traits::LieGroup;
903
904 // Test the branch cut at θ = 0 (identity)
905 // Values just before 2π should log to ~2π
906 // Values just after 0 should log to ~0
907 // Demonstrating the discontinuity
908
909 let eps = 0.01;
910
911 // Just before the branch cut (θ ≈ 2π - ε)
912 let g_before = U1::from_angle(2.0 * PI - eps);
913 let log_before = g_before.log().unwrap().value();
914
915 // Just after the branch cut (θ ≈ 0 + ε)
916 let g_after = U1::from_angle(eps);
917 let log_after = g_after.log().unwrap().value();
918
919 // Verify we're on the correct sides
920 assert!(
921 (log_before - (2.0 * PI - eps)).abs() < 1e-10,
922 "Expected log(e^{{i(2π-ε)}}) ≈ 2π-ε, got {}",
923 log_before
924 );
925 assert!(
926 (log_after - eps).abs() < 1e-10,
927 "Expected log(e^{{iε}}) ≈ ε, got {}",
928 log_after
929 );
930
931 // The jump discontinuity is ~2π
932 let jump = log_before - log_after;
933 assert!(
934 (jump - 2.0 * PI).abs() < 0.1,
935 "Expected discontinuity ≈ 2π, got {}",
936 jump
937 );
938 }
939
940 #[test]
941 fn test_log_principal_branch_coverage() {
942 use crate::traits::LieGroup;
943
944 // Verify log returns values in [0, 2π) for all inputs
945 let test_angles = vec![
946 0.0,
947 PI / 4.0,
948 PI / 2.0,
949 PI,
950 3.0 * PI / 2.0,
951 2.0 * PI - 0.001,
952 ];
953
954 for theta in test_angles {
955 let g = U1::from_angle(theta);
956 let log_value = g.log().unwrap().value();
957
958 // Should be in principal branch [0, 2π)
959 assert!(
960 (0.0..2.0 * PI).contains(&log_value),
961 "log value {} outside principal branch [0, 2π) for θ = {}",
962 log_value,
963 theta
964 );
965
966 // Should match the stored angle
967 assert!(
968 (log_value - g.angle()).abs() < 1e-14,
969 "log inconsistent with angle() for θ = {}",
970 theta
971 );
972 }
973 }
974
975 // ========================================================================
976 // Property-Based Tests for Group Axioms
977 // ========================================================================
978 //
979 // These tests use proptest to verify that U(1) satisfies the
980 // mathematical axioms of a Lie group for randomly generated elements.
981 //
982 // U(1) is **abelian**, so we also test commutativity.
983 //
984 // Run with: cargo test --features property-tests
985
986 #[cfg(feature = "proptest")]
987 use proptest::prelude::*;
988
989 /// Strategy for generating arbitrary U(1) elements.
990 ///
991 /// We generate U(1) elements by sampling angles uniformly from [0, 2π).
992 #[cfg(feature = "proptest")]
993 fn arb_u1() -> impl Strategy<Value = U1> {
994 (0.0..2.0 * PI).prop_map(U1::from_angle)
995 }
996
997 #[cfg(feature = "proptest")]
998 proptest! {
999 /// **Group Axiom 1: Identity Element**
1000 ///
1001 /// For all g ∈ U(1):
1002 /// - e · g = g (left identity)
1003 /// - g · e = g (right identity)
1004 #[test]
1005 fn prop_identity_axiom(g in arb_u1()) {
1006 let e = U1::identity();
1007
1008 // Left identity: e · g = g
1009 let left = e.compose(&g);
1010 prop_assert!(
1011 (left.angle() - g.angle()).abs() < 1e-10,
1012 "Left identity failed: e·g != g"
1013 );
1014
1015 // Right identity: g · e = g
1016 let right = g.compose(&e);
1017 prop_assert!(
1018 (right.angle() - g.angle()).abs() < 1e-10,
1019 "Right identity failed: g·e != g"
1020 );
1021 }
1022
1023 /// **Group Axiom 2: Inverse Element**
1024 ///
1025 /// For all g ∈ U(1):
1026 /// - g · g⁻¹ = e (right inverse)
1027 /// - g⁻¹ · g = e (left inverse)
1028 #[test]
1029 fn prop_inverse_axiom(g in arb_u1()) {
1030 let g_inv = g.inverse();
1031
1032 // Right inverse: g · g⁻¹ = e
1033 let right_product = g.compose(&g_inv);
1034 prop_assert!(
1035 right_product.is_near_identity(1e-10),
1036 "Right inverse failed: g·g⁻¹ != e, distance = {}",
1037 right_product.distance_to_identity()
1038 );
1039
1040 // Left inverse: g⁻¹ · g = e
1041 let left_product = g_inv.compose(&g);
1042 prop_assert!(
1043 left_product.is_near_identity(1e-10),
1044 "Left inverse failed: g⁻¹·g != e, distance = {}",
1045 left_product.distance_to_identity()
1046 );
1047 }
1048
1049 /// **Group Axiom 3: Associativity**
1050 ///
1051 /// For all g₁, g₂, g₃ ∈ U(1):
1052 /// - (g₁ · g₂) · g₃ = g₁ · (g₂ · g₃)
1053 #[test]
1054 fn prop_associativity(g1 in arb_u1(), g2 in arb_u1(), g3 in arb_u1()) {
1055 // Left association: (g₁ · g₂) · g₃
1056 let left_assoc = g1.compose(&g2).compose(&g3);
1057
1058 // Right association: g₁ · (g₂ · g₃)
1059 let right_assoc = g1.compose(&g2.compose(&g3));
1060
1061 prop_assert!(
1062 (left_assoc.angle() - right_assoc.angle()).abs() < 1e-10,
1063 "Associativity failed: (g₁·g₂)·g₃ != g₁·(g₂·g₃)"
1064 );
1065 }
1066
1067 /// **Abelian Property: Commutativity**
1068 ///
1069 /// For all g, h ∈ U(1):
1070 /// - g · h = h · g
1071 ///
1072 /// U(1) is abelian (commutative), which is NOT true for SU(2).
1073 /// This is why U(1) is used for electromagnetism (no self-interaction).
1074 #[test]
1075 fn prop_commutativity(g in arb_u1(), h in arb_u1()) {
1076 let gh = g.compose(&h);
1077 let hg = h.compose(&g);
1078
1079 prop_assert!(
1080 (gh.angle() - hg.angle()).abs() < 1e-10,
1081 "Commutativity failed: g·h != h·g, g·h = {}, h·g = {}",
1082 gh.angle(),
1083 hg.angle()
1084 );
1085 }
1086
1087 /// **Exponential Map Property**
1088 ///
1089 /// For U(1), exp: u(1) → U(1) is the exponential map.
1090 /// We verify that exp(a) · exp(b) = exp(a + b) for scalars a, b ∈ ℝ.
1091 ///
1092 /// This is the Baker-Campbell-Hausdorff formula specialized to the
1093 /// abelian case (where it simplifies dramatically).
1094 #[test]
1095 fn prop_exponential_map_homomorphism(a in -PI..PI, b in -PI..PI) {
1096 use crate::traits::LieGroup;
1097
1098 let exp_a = U1::exp(&U1Algebra(a));
1099 let exp_b = U1::exp(&U1Algebra(b));
1100 let exp_sum = U1::exp(&U1Algebra(a + b));
1101
1102 let product = exp_a.compose(&exp_b);
1103
1104 prop_assert!(
1105 product.distance(&exp_sum) < 1e-10,
1106 "Exponential map homomorphism failed: exp(a)·exp(b) != exp(a+b)"
1107 );
1108 }
1109
1110 /// **Compactness: Angles Wrap Around**
1111 ///
1112 /// U(1) is compact (isomorphic to S¹, the circle).
1113 /// This means angles wrap around modulo 2π.
1114 #[test]
1115 fn prop_compactness_angle_wrapping(theta in -10.0 * PI..10.0 * PI) {
1116 let g = U1::from_angle(theta);
1117
1118 // All angles should be normalized to [0, 2π)
1119 prop_assert!(
1120 g.angle() >= 0.0 && g.angle() < 2.0 * PI,
1121 "Angle not normalized: θ = {}",
1122 g.angle()
1123 );
1124
1125 // Adding 2πn should give the same element
1126 let g_plus_2pi = U1::from_angle(theta + 2.0 * PI);
1127 prop_assert!(
1128 (g.angle() - g_plus_2pi.angle()).abs() < 1e-10,
1129 "Adding 2π changed the element"
1130 );
1131 }
1132 }
1133}