1use crate::traits::{AntiHermitianByConstruction, LieAlgebra, LieGroup, TracelessByConstruction};
73use ndarray::Array2;
74use num_complex::Complex64;
75use std::fmt;
76use std::ops::{Add, Mul, MulAssign, Neg, Sub};
77
78#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct Su3Algebra(pub(crate) [f64; 8]);
106
107impl Su3Algebra {
108 #[must_use]
113 pub fn new(components: [f64; 8]) -> Self {
114 Self(components)
115 }
116
117 #[must_use]
119 pub fn components(&self) -> &[f64; 8] {
120 &self.0
121 }
122}
123
124impl Add for Su3Algebra {
125 type Output = Self;
126 fn add(self, rhs: Self) -> Self {
127 let mut r = [0.0; 8];
128 for i in 0..8 {
129 r[i] = self.0[i] + rhs.0[i];
130 }
131 Self(r)
132 }
133}
134
135impl Add<&Su3Algebra> for Su3Algebra {
136 type Output = Su3Algebra;
137 fn add(self, rhs: &Su3Algebra) -> Su3Algebra {
138 self + *rhs
139 }
140}
141
142impl Add<Su3Algebra> for &Su3Algebra {
143 type Output = Su3Algebra;
144 fn add(self, rhs: Su3Algebra) -> Su3Algebra {
145 *self + rhs
146 }
147}
148
149impl Add<&Su3Algebra> for &Su3Algebra {
150 type Output = Su3Algebra;
151 fn add(self, rhs: &Su3Algebra) -> Su3Algebra {
152 *self + *rhs
153 }
154}
155
156impl Sub for Su3Algebra {
157 type Output = Self;
158 fn sub(self, rhs: Self) -> Self {
159 let mut r = [0.0; 8];
160 for i in 0..8 {
161 r[i] = self.0[i] - rhs.0[i];
162 }
163 Self(r)
164 }
165}
166
167impl Neg for Su3Algebra {
168 type Output = Self;
169 fn neg(self) -> Self {
170 let mut r = [0.0; 8];
171 for i in 0..8 {
172 r[i] = -self.0[i];
173 }
174 Self(r)
175 }
176}
177
178impl Mul<f64> for Su3Algebra {
179 type Output = Self;
180 fn mul(self, scalar: f64) -> Self {
181 let mut r = [0.0; 8];
182 for i in 0..8 {
183 r[i] = self.0[i] * scalar;
184 }
185 Self(r)
186 }
187}
188
189impl Mul<Su3Algebra> for f64 {
190 type Output = Su3Algebra;
191 fn mul(self, rhs: Su3Algebra) -> Su3Algebra {
192 rhs * self
193 }
194}
195
196impl LieAlgebra for Su3Algebra {
197 const DIM: usize = 8;
198
199 fn zero() -> Self {
200 Self([0.0; 8])
201 }
202
203 fn add(&self, other: &Self) -> Self {
204 let mut result = [0.0; 8];
205 for i in 0..8 {
206 result[i] = self.0[i] + other.0[i];
207 }
208 Self(result)
209 }
210
211 fn scale(&self, scalar: f64) -> Self {
212 let mut result = [0.0; 8];
213 for i in 0..8 {
214 result[i] = self.0[i] * scalar;
215 }
216 Self(result)
217 }
218
219 fn norm(&self) -> f64 {
220 self.0.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
221 }
222
223 fn basis_element(i: usize) -> Self {
224 assert!(i < 8, "SU(3) algebra is 8-dimensional");
225 let mut coeffs = [0.0; 8];
226 coeffs[i] = 1.0;
227 Self(coeffs)
228 }
229
230 fn from_components(components: &[f64]) -> Self {
231 assert_eq!(components.len(), 8, "su(3) has dimension 8");
232 let mut coeffs = [0.0; 8];
233 coeffs.copy_from_slice(components);
234 Self(coeffs)
235 }
236
237 fn to_components(&self) -> Vec<f64> {
238 self.0.to_vec()
239 }
240
241 fn bracket(&self, other: &Self) -> Self {
268 const SQRT3_HALF: f64 = 0.866_025_403_784_438_6;
272
273 #[rustfmt::skip]
276 const STRUCTURE_CONSTANTS: [(usize, usize, usize, f64); 54] = [
277 (0, 1, 2, 1.0), (1, 2, 0, 1.0), (2, 0, 1, 1.0),
279 (1, 0, 2, -1.0), (0, 2, 1, -1.0), (2, 1, 0, -1.0),
280 (0, 3, 6, 0.5), (3, 6, 0, 0.5), (6, 0, 3, 0.5),
282 (3, 0, 6, -0.5), (0, 6, 3, -0.5), (6, 3, 0, -0.5),
283 (0, 4, 5, -0.5), (4, 5, 0, -0.5), (5, 0, 4, -0.5),
285 (4, 0, 5, 0.5), (0, 5, 4, 0.5), (5, 4, 0, 0.5),
286 (1, 3, 5, 0.5), (3, 5, 1, 0.5), (5, 1, 3, 0.5),
288 (3, 1, 5, -0.5), (1, 5, 3, -0.5), (5, 3, 1, -0.5),
289 (1, 4, 6, 0.5), (4, 6, 1, 0.5), (6, 1, 4, 0.5),
291 (4, 1, 6, -0.5), (1, 6, 4, -0.5), (6, 4, 1, -0.5),
292 (2, 3, 4, 0.5), (3, 4, 2, 0.5), (4, 2, 3, 0.5),
294 (3, 2, 4, -0.5), (2, 4, 3, -0.5), (4, 3, 2, -0.5),
295 (2, 5, 6, -0.5), (5, 6, 2, -0.5), (6, 2, 5, -0.5),
297 (5, 2, 6, 0.5), (2, 6, 5, 0.5), (6, 5, 2, 0.5),
298 (3, 4, 7, SQRT3_HALF), (4, 7, 3, SQRT3_HALF), (7, 3, 4, SQRT3_HALF),
300 (4, 3, 7, -SQRT3_HALF), (3, 7, 4, -SQRT3_HALF), (7, 4, 3, -SQRT3_HALF),
301 (5, 6, 7, SQRT3_HALF), (6, 7, 5, SQRT3_HALF), (7, 5, 6, SQRT3_HALF),
303 (6, 5, 7, -SQRT3_HALF), (5, 7, 6, -SQRT3_HALF), (7, 6, 5, -SQRT3_HALF),
304 ];
305
306 let mut result = [0.0; 8];
307
308 for &(i, j, k, f) in &STRUCTURE_CONSTANTS {
309 result[k] += self.0[i] * other.0[j] * f;
310 }
311
312 for r in &mut result {
315 *r *= -1.0;
316 }
317
318 Self(result)
319 }
320
321 #[inline]
322 fn inner(&self, other: &Self) -> f64 {
323 let mut sum = 0.0;
324 for i in 0..8 {
325 sum += self.0[i] * other.0[i];
326 }
327 sum
328 }
329}
330
331impl crate::Casimir for Su3Algebra {
336 type Representation = crate::Su3Irrep;
337
338 fn quadratic_casimir_eigenvalue(irrep: &Self::Representation) -> f64 {
339 let p = irrep.p as f64;
340 let q = irrep.q as f64;
341
342 (p * p + q * q + p * q + 3.0 * p + 3.0 * q) / 3.0
344 }
345
346 fn higher_casimir_eigenvalues(irrep: &Self::Representation) -> Vec<f64> {
374 let p = irrep.p as f64;
375 let q = irrep.q as f64;
376
377 let c3 = (p - q) * (2.0 * p + q + 3.0) * (p + 2.0 * q + 3.0) / 18.0;
379
380 vec![c3]
381 }
382
383 fn rank() -> usize {
384 2 }
386}
387
388impl Su3Algebra {
389 #[must_use]
394 pub fn to_matrix(&self) -> Array2<Complex64> {
395 let [a1, a2, a3, a4, a5, a6, a7, a8] = self.0;
396 let i = Complex64::new(0.0, 1.0);
397 let sqrt3_inv = 1.0 / 3_f64.sqrt();
398
399 let mut matrix = Array2::zeros((3, 3));
401
402 matrix[[0, 1]] += i * a1;
404 matrix[[1, 0]] += i * a1;
405
406 matrix[[0, 1]] += i * Complex64::new(0.0, -a2); matrix[[1, 0]] += i * Complex64::new(0.0, a2); matrix[[0, 0]] += i * a3;
412 matrix[[1, 1]] += -i * a3;
413
414 matrix[[0, 2]] += i * a4;
416 matrix[[2, 0]] += i * a4;
417
418 matrix[[0, 2]] += i * Complex64::new(0.0, -a5);
420 matrix[[2, 0]] += i * Complex64::new(0.0, a5);
421
422 matrix[[1, 2]] += i * a6;
424 matrix[[2, 1]] += i * a6;
425
426 matrix[[1, 2]] += i * Complex64::new(0.0, -a7);
428 matrix[[2, 1]] += i * Complex64::new(0.0, a7);
429
430 matrix[[0, 0]] += i * a8 * sqrt3_inv;
432 matrix[[1, 1]] += i * a8 * sqrt3_inv;
433 matrix[[2, 2]] += -i * a8 * sqrt3_inv * 2.0;
434
435 matrix.mapv_inplace(|z| z * 0.5);
437 matrix
438 }
439
440 #[must_use]
446 pub fn from_matrix(matrix: &Array2<Complex64>) -> Self {
447 let i = Complex64::new(0.0, 1.0);
448 let neg_i = -i;
449
450 let mut coeffs = [0.0; 8];
451
452 for j in 0..8 {
454 let lambda_j = Self::gell_mann_matrix(j);
455 let product = matrix.dot(&lambda_j);
456
457 let trace = product[[0, 0]] + product[[1, 1]] + product[[2, 2]];
459
460 coeffs[j] = (neg_i * trace).re;
462 }
463
464 Self(coeffs)
465 }
466
467 #[must_use]
471 pub fn gell_mann_matrix(j: usize) -> Array2<Complex64> {
472 assert!(j < 8, "Gell-Mann matrices are indexed 0..7");
473
474 let mut matrix = Array2::zeros((3, 3));
475 let i = Complex64::new(0.0, 1.0);
476 let sqrt3_inv = 1.0 / 3_f64.sqrt();
477
478 match j {
479 0 => {
480 matrix[[0, 1]] = Complex64::new(1.0, 0.0);
482 matrix[[1, 0]] = Complex64::new(1.0, 0.0);
483 }
484 1 => {
485 matrix[[0, 1]] = -i;
487 matrix[[1, 0]] = i;
488 }
489 2 => {
490 matrix[[0, 0]] = Complex64::new(1.0, 0.0);
492 matrix[[1, 1]] = Complex64::new(-1.0, 0.0);
493 }
494 3 => {
495 matrix[[0, 2]] = Complex64::new(1.0, 0.0);
497 matrix[[2, 0]] = Complex64::new(1.0, 0.0);
498 }
499 4 => {
500 matrix[[0, 2]] = -i;
502 matrix[[2, 0]] = i;
503 }
504 5 => {
505 matrix[[1, 2]] = Complex64::new(1.0, 0.0);
507 matrix[[2, 1]] = Complex64::new(1.0, 0.0);
508 }
509 6 => {
510 matrix[[1, 2]] = -i;
512 matrix[[2, 1]] = i;
513 }
514 7 => {
515 matrix[[0, 0]] = Complex64::new(sqrt3_inv, 0.0);
517 matrix[[1, 1]] = Complex64::new(sqrt3_inv, 0.0);
518 matrix[[2, 2]] = Complex64::new(-2.0 * sqrt3_inv, 0.0);
519 }
520 _ => unreachable!(),
521 }
522
523 matrix
524 }
525
526 #[inline]
545 #[must_use]
546 #[allow(dead_code)]
547 fn structure_constant(i: usize, j: usize, k: usize) -> f64 {
548 const SQRT3_HALF: f64 = 0.866_025_403_784_438_6; if i == j || i == k || j == k {
552 return 0.0;
553 }
554
555 if i > j {
557 return -Self::structure_constant(j, i, k);
558 }
559
560 match (i, j, k) {
563 (0, 1, 2) | (1, 2, 0) | (2, 0, 1) => 1.0,
565 (0, 2, 1) | (2, 1, 0) | (1, 0, 2) => -1.0,
566
567 (0, 3, 6) | (3, 6, 0) | (6, 0, 3) => 0.5,
569 (0, 6, 3) | (6, 3, 0) | (3, 0, 6) => -0.5,
570
571 (0, 4, 5) | (4, 5, 0) | (5, 0, 4) => -0.5,
573 (0, 5, 4) | (5, 4, 0) | (4, 0, 5) => 0.5,
574
575 (1, 3, 5) | (3, 5, 1) | (5, 1, 3) => 0.5,
577 (1, 5, 3) | (5, 3, 1) | (3, 1, 5) => -0.5,
578
579 (1, 4, 6) | (4, 6, 1) | (6, 1, 4) => 0.5,
581 (1, 6, 4) | (6, 4, 1) | (4, 1, 6) => -0.5,
582
583 (2, 3, 4) | (3, 4, 2) | (4, 2, 3) => 0.5,
585 (2, 4, 3) | (4, 3, 2) | (3, 2, 4) => -0.5,
586
587 (2, 5, 6) | (5, 6, 2) | (6, 2, 5) => -0.5,
589 (2, 6, 5) | (6, 5, 2) | (5, 2, 6) => 0.5,
590
591 (3, 4, 7) | (4, 7, 3) | (7, 3, 4) => SQRT3_HALF,
593 (3, 7, 4) | (7, 4, 3) | (4, 3, 7) => -SQRT3_HALF,
594
595 (5, 6, 7) | (6, 7, 5) | (7, 5, 6) => SQRT3_HALF,
597 (5, 7, 6) | (7, 6, 5) | (6, 5, 7) => -SQRT3_HALF,
598
599 _ => 0.0,
601 }
602 }
603}
604
605#[derive(Clone, Debug, PartialEq)]
628pub struct SU3 {
629 pub(crate) matrix: Array2<Complex64>,
631}
632
633impl SU3 {
634 #[must_use]
636 pub fn matrix(&self) -> &Array2<Complex64> {
637 &self.matrix
638 }
639
640 #[must_use]
642 pub fn identity() -> Self {
643 let mut matrix = Array2::zeros((3, 3));
644 matrix[[0, 0]] = Complex64::new(1.0, 0.0);
645 matrix[[1, 1]] = Complex64::new(1.0, 0.0);
646 matrix[[2, 2]] = Complex64::new(1.0, 0.0);
647 Self { matrix }
648 }
649
650 #[must_use]
652 pub fn verify_unitarity(&self, tolerance: f64) -> bool {
653 let adjoint = self.matrix.t().mapv(|z| z.conj());
654 let product = adjoint.dot(&self.matrix);
655
656 let mut identity = Array2::zeros((3, 3));
657 identity[[0, 0]] = Complex64::new(1.0, 0.0);
658 identity[[1, 1]] = Complex64::new(1.0, 0.0);
659 identity[[2, 2]] = Complex64::new(1.0, 0.0);
660
661 let diff = product - identity;
662 let norm: f64 = diff
663 .iter()
664 .map(num_complex::Complex::norm_sqr)
665 .sum::<f64>()
666 .sqrt();
667
668 norm < tolerance
669 }
670
671 #[must_use]
673 pub fn inverse(&self) -> Self {
674 Self {
675 matrix: self.matrix.t().mapv(|z| z.conj()),
676 }
677 }
678
679 #[must_use]
681 pub fn conjugate_transpose(&self) -> Self {
682 self.inverse()
683 }
684
685 #[must_use]
687 pub fn trace(&self) -> Complex64 {
688 self.matrix[[0, 0]] + self.matrix[[1, 1]] + self.matrix[[2, 2]]
689 }
690
691 #[must_use]
693 pub fn distance_to_identity(&self) -> f64 {
694 let mut identity = Array2::zeros((3, 3));
696 identity[[0, 0]] = Complex64::new(1.0, 0.0);
697 identity[[1, 1]] = Complex64::new(1.0, 0.0);
698 identity[[2, 2]] = Complex64::new(1.0, 0.0);
699
700 let diff = &self.matrix - &identity;
701 diff.iter()
702 .map(num_complex::Complex::norm_sqr)
703 .sum::<f64>()
704 .sqrt()
705 }
706
707 #[must_use]
746 fn gram_schmidt_project(matrix: Array2<Complex64>) -> Array2<Complex64> {
747 let mut result: Array2<Complex64> = Array2::zeros((3, 3));
748
749 let matrix_norm: f64 = matrix
751 .iter()
752 .map(num_complex::Complex::norm_sqr)
753 .sum::<f64>()
754 .sqrt();
755
756 const MACHINE_EPSILON: f64 = 2.2e-16;
760 const ABSOLUTE_FLOOR: f64 = 1e-14;
761 let relative_threshold = MACHINE_EPSILON * matrix_norm;
762 let threshold = relative_threshold.max(ABSOLUTE_FLOOR);
763
764 for j in 0..3 {
766 let mut col = matrix.column(j).to_owned();
767
768 for k in 0..j {
770 let prev_col = result.column(k);
771 let proj: Complex64 = prev_col
772 .iter()
773 .zip(col.iter())
774 .map(|(p, c)| p.conj() * c)
775 .sum();
776 for i in 0..3 {
777 col[i] -= proj * prev_col[i];
778 }
779 }
780
781 let norm: f64 = col
783 .iter()
784 .map(num_complex::Complex::norm_sqr)
785 .sum::<f64>()
786 .sqrt();
787
788 debug_assert!(
790 norm > threshold,
791 "Gram-Schmidt: column {} is linearly dependent (norm = {:.2e}, threshold = {:.2e}). \
792 Input matrix is rank-deficient.",
793 j,
794 norm,
795 threshold
796 );
797
798 if norm > threshold {
799 for i in 0..3 {
800 result[[i, j]] = col[i] / norm;
801 }
802 }
803 }
805
806 let det = result[[0, 0]]
809 * (result[[1, 1]] * result[[2, 2]] - result[[1, 2]] * result[[2, 1]])
810 - result[[0, 1]] * (result[[1, 0]] * result[[2, 2]] - result[[1, 2]] * result[[2, 0]])
811 + result[[0, 2]] * (result[[1, 0]] * result[[2, 1]] - result[[1, 1]] * result[[2, 0]]);
812
813 let det_norm = det.norm();
816 if det_norm < threshold {
817 return Array2::eye(3);
820 }
821
822 let det_phase = det / det_norm;
823
824 let correction = (det_phase.conj()).powf(1.0 / 3.0);
826 result.mapv_inplace(|z| z * correction);
827
828 result
829 }
830
831 #[must_use]
845 fn exp_taylor(matrix: &Array2<Complex64>, max_terms: usize) -> Self {
846 let mut result = Array2::zeros((3, 3));
847 result[[0, 0]] = Complex64::new(1.0, 0.0);
848 result[[1, 1]] = Complex64::new(1.0, 0.0);
849 result[[2, 2]] = Complex64::new(1.0, 0.0);
850
851 let mut term = matrix.clone();
852 let mut factorial = 1.0;
853
854 for n in 1..=max_terms {
855 factorial *= n as f64;
856 result += &term.mapv(|z| z / factorial);
857
858 let term_norm: f64 = term
860 .iter()
861 .map(num_complex::Complex::norm_sqr)
862 .sum::<f64>()
863 .sqrt();
864
865 if term_norm / factorial < 1e-14 {
866 break;
867 }
868
869 if n < max_terms {
870 term = term.dot(matrix);
871 }
872 }
873
874 Self { matrix: result }
875 }
876
877 fn matrix_sqrt_db(u: &Array2<Complex64>) -> Array2<Complex64> {
893 use nalgebra::{Complex as NaComplex, Matrix3};
894
895 fn to_nalgebra(a: &Array2<Complex64>) -> Matrix3<NaComplex<f64>> {
897 Matrix3::from_fn(|i, j| NaComplex::new(a[[i, j]].re, a[[i, j]].im))
898 }
899
900 fn to_ndarray(m: &Matrix3<NaComplex<f64>>) -> Array2<Complex64> {
901 Array2::from_shape_fn((3, 3), |(i, j)| Complex64::new(m[(i, j)].re, m[(i, j)].im))
902 }
903
904 let mut y = to_nalgebra(u);
905 let mut z = Matrix3::<NaComplex<f64>>::identity();
906
907 const MAX_ITERS: usize = 20;
908 const TOL: f64 = 1e-14;
909
910 for _ in 0..MAX_ITERS {
911 let y_inv = y.try_inverse().unwrap_or(y.adjoint()); let z_inv = z.try_inverse().unwrap_or(z.adjoint());
914
915 let y_new = (y + z_inv).scale(0.5);
916 let z_new = (z + y_inv).scale(0.5);
917
918 let diff: f64 = (y_new - y).norm();
920
921 y = y_new;
922 z = z_new;
923
924 if diff < TOL {
925 break;
926 }
927 }
928
929 to_ndarray(&y)
930 }
931}
932
933impl approx::AbsDiffEq for Su3Algebra {
934 type Epsilon = f64;
935
936 fn default_epsilon() -> Self::Epsilon {
937 1e-10
938 }
939
940 fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
941 self.0
942 .iter()
943 .zip(other.0.iter())
944 .all(|(a, b)| (a - b).abs() < epsilon)
945 }
946}
947
948impl approx::RelativeEq for Su3Algebra {
949 fn default_max_relative() -> Self::Epsilon {
950 1e-10
951 }
952
953 fn relative_eq(
954 &self,
955 other: &Self,
956 epsilon: Self::Epsilon,
957 max_relative: Self::Epsilon,
958 ) -> bool {
959 self.0
960 .iter()
961 .zip(other.0.iter())
962 .all(|(a, b)| approx::RelativeEq::relative_eq(a, b, epsilon, max_relative))
963 }
964}
965
966impl fmt::Display for Su3Algebra {
967 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
968 write!(f, "su(3)[")?;
969 for (i, c) in self.0.iter().enumerate() {
970 if i > 0 {
971 write!(f, ", ")?;
972 }
973 write!(f, "{:.4}", c)?;
974 }
975 write!(f, "]")
976 }
977}
978
979impl fmt::Display for SU3 {
980 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
981 let dist = self.distance_to_identity();
982 write!(f, "SU(3)(d={:.4})", dist)
983 }
984}
985
986impl Mul<&SU3> for &SU3 {
988 type Output = SU3;
989 fn mul(self, rhs: &SU3) -> SU3 {
990 SU3 {
991 matrix: self.matrix.dot(&rhs.matrix),
992 }
993 }
994}
995
996impl Mul<&SU3> for SU3 {
997 type Output = SU3;
998 fn mul(self, rhs: &SU3) -> SU3 {
999 &self * rhs
1000 }
1001}
1002
1003impl MulAssign<&SU3> for SU3 {
1004 fn mul_assign(&mut self, rhs: &SU3) {
1005 self.matrix = self.matrix.dot(&rhs.matrix);
1006 }
1007}
1008
1009impl LieGroup for SU3 {
1010 const MATRIX_DIM: usize = 3;
1011
1012 type Algebra = Su3Algebra;
1013
1014 fn identity() -> Self {
1015 Self::identity()
1016 }
1017
1018 fn compose(&self, other: &Self) -> Self {
1019 Self {
1020 matrix: self.matrix.dot(&other.matrix),
1021 }
1022 }
1023
1024 fn inverse(&self) -> Self {
1025 Self::inverse(self)
1026 }
1027
1028 fn conjugate_transpose(&self) -> Self {
1029 Self::conjugate_transpose(self)
1030 }
1031
1032 fn adjoint_action(&self, algebra_element: &Su3Algebra) -> Su3Algebra {
1033 let x_matrix = algebra_element.to_matrix();
1035 let g_x = self.matrix.dot(&x_matrix);
1036 let g_adjoint_matrix = self.matrix.t().mapv(|z| z.conj());
1037 let result = g_x.dot(&g_adjoint_matrix);
1038
1039 Su3Algebra::from_matrix(&result)
1040 }
1041
1042 fn distance_to_identity(&self) -> f64 {
1043 Self::distance_to_identity(self)
1044 }
1045
1046 fn exp(tangent: &Su3Algebra) -> Self {
1047 let x_matrix = tangent.to_matrix();
1055
1056 let norm: f64 = x_matrix
1058 .iter()
1059 .map(num_complex::Complex::norm_sqr)
1060 .sum::<f64>()
1061 .sqrt();
1062
1063 let k = if norm > 0.5 {
1066 (norm / 0.5).log2().ceil() as usize
1067 } else {
1068 0
1069 };
1070
1071 let scale_factor = 1.0 / (1_u64 << k) as f64;
1073 let scaled_matrix = x_matrix.mapv(|z| z * scale_factor);
1074
1075 let exp_scaled = SU3::exp_taylor(&scaled_matrix, 15);
1077
1078 let mut result = exp_scaled.matrix;
1092 for _ in 0..k {
1093 result = result.dot(&result);
1094 result = Self::gram_schmidt_project(result);
1096 }
1097
1098 Self { matrix: result }
1100 }
1101
1102 fn log(&self) -> crate::error::LogResult<Su3Algebra> {
1103 use crate::error::LogError;
1104
1105 let dist = self.distance_to_identity();
1116 const MAX_DISTANCE: f64 = 2.0; if dist > MAX_DISTANCE {
1119 return Err(LogError::NotNearIdentity {
1120 distance: dist,
1121 threshold: MAX_DISTANCE,
1122 });
1123 }
1124
1125 if dist < 1e-14 {
1127 return Ok(Su3Algebra::zero());
1128 }
1129
1130 let mut current = self.matrix.clone();
1133 let mut num_sqrts = 0;
1134 const MAX_SQRTS: usize = 32; const TARGET_NORM: f64 = 0.5; let identity: Array2<Complex64> = Array2::eye(3);
1138
1139 while num_sqrts < MAX_SQRTS {
1140 let x_matrix = ¤t - &identity;
1141 let x_norm: f64 = x_matrix
1142 .iter()
1143 .map(num_complex::Complex::norm_sqr)
1144 .sum::<f64>()
1145 .sqrt();
1146
1147 if x_norm < TARGET_NORM {
1148 break;
1149 }
1150
1151 current = Self::matrix_sqrt_db(¤t);
1153 num_sqrts += 1;
1154 }
1155
1156 let x_matrix = ¤t - &identity;
1158
1159 let mut log_matrix = x_matrix.clone();
1161 let mut x_power = x_matrix.clone();
1162
1163 const N_TERMS: usize = 30;
1165
1166 for n in 2..=N_TERMS {
1167 x_power = x_power.dot(&x_matrix);
1168 let coefficient = (-1.0_f64).powi(n as i32 + 1) / n as f64;
1169 log_matrix = log_matrix + x_power.mapv(|z| z * coefficient);
1170 }
1171
1172 let scale_factor = (1_u64 << num_sqrts) as f64;
1174 log_matrix = log_matrix.mapv(|z| z * scale_factor);
1175
1176 Ok(Su3Algebra::from_matrix(&log_matrix))
1178 }
1179}
1180
1181use crate::traits::{Compact, SemiSimple, Simple};
1186
1187impl Compact for SU3 {}
1191
1192impl Simple for SU3 {}
1196
1197impl SemiSimple for SU3 {}
1199
1200impl TracelessByConstruction for Su3Algebra {}
1209
1210impl AntiHermitianByConstruction for Su3Algebra {}
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218 use approx::assert_relative_eq;
1219
1220 #[test]
1221 fn test_identity() {
1222 let id = SU3::identity();
1223 assert!(id.verify_unitarity(1e-10));
1224 assert_relative_eq!(id.distance_to_identity(), 0.0, epsilon = 1e-10);
1225 }
1226
1227 #[test]
1228 fn test_algebra_dimension() {
1229 assert_eq!(Su3Algebra::DIM, 8);
1230 }
1231
1232 #[test]
1233 fn test_gell_mann_hermiticity() {
1234 for j in 0..8 {
1236 let lambda = Su3Algebra::gell_mann_matrix(j);
1237 let adjoint = lambda.t().mapv(|z| z.conj());
1238
1239 for i in 0..3 {
1240 for k in 0..3 {
1241 assert_relative_eq!(lambda[[i, k]].re, adjoint[[i, k]].re, epsilon = 1e-10);
1242 assert_relative_eq!(lambda[[i, k]].im, adjoint[[i, k]].im, epsilon = 1e-10);
1243 }
1244 }
1245 }
1246 }
1247
1248 #[test]
1249 fn test_gell_mann_traceless() {
1250 for j in 0..8 {
1252 let lambda = Su3Algebra::gell_mann_matrix(j);
1253 let trace = lambda[[0, 0]] + lambda[[1, 1]] + lambda[[2, 2]];
1254 assert_relative_eq!(trace.re, 0.0, epsilon = 1e-10);
1255 assert_relative_eq!(trace.im, 0.0, epsilon = 1e-10);
1256 }
1257 }
1258
1259 #[test]
1260 fn test_matrix_roundtrip() {
1261 let algebra = Su3Algebra([1.0, 2.0, 3.0, 0.5, -0.5, 1.5, -1.5, 0.3]);
1263 let matrix = algebra.to_matrix();
1264 let recovered = Su3Algebra::from_matrix(&matrix);
1265
1266 for i in 0..8 {
1267 assert_relative_eq!(algebra.0[i], recovered.0[i], epsilon = 1e-10);
1268 }
1269 }
1270
1271 #[test]
1272 fn test_inverse() {
1273 use crate::traits::LieGroup;
1274
1275 let g = SU3::exp(&Su3Algebra([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]));
1276 let g_inv = g.inverse();
1277 let product = g.compose(&g_inv);
1278
1279 assert_relative_eq!(product.distance_to_identity(), 0.0, epsilon = 1e-6);
1280 }
1281
1282 #[test]
1283 fn test_adjoint_identity() {
1284 use crate::traits::LieGroup;
1285
1286 let e = SU3::identity();
1287 let x = Su3Algebra([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
1288 let result = e.adjoint_action(&x);
1289
1290 for i in 0..8 {
1291 assert_relative_eq!(result.0[i], x.0[i], epsilon = 1e-10);
1292 }
1293 }
1294
1295 #[test]
1296 fn test_structure_constants_bracket() {
1297 use crate::traits::LieAlgebra;
1298
1299 let t1 = Su3Algebra::basis_element(0);
1301 let t2 = Su3Algebra::basis_element(1);
1302 let bracket = t1.bracket(&t2);
1303
1304 assert_relative_eq!(bracket.0[2], -1.0, epsilon = 1e-10);
1306 for i in [0, 1, 3, 4, 5, 6, 7] {
1307 assert_relative_eq!(bracket.0[i], 0.0, epsilon = 1e-10);
1308 }
1309
1310 let bracket_reversed = t2.bracket(&t1);
1312 for i in 0..8 {
1313 assert_relative_eq!(bracket.0[i], -bracket_reversed.0[i], epsilon = 1e-10);
1314 }
1315
1316 let t4 = Su3Algebra::basis_element(3);
1318 let t5 = Su3Algebra::basis_element(4);
1319 let bracket_45 = t4.bracket(&t5);
1320 let expected_c8 = -(3.0_f64.sqrt() / 2.0);
1321 assert_relative_eq!(bracket_45.0[7], expected_c8, epsilon = 1e-10);
1322 }
1323
1324 #[test]
1325 fn test_bracket_jacobi_identity() {
1326 use crate::traits::LieAlgebra;
1327
1328 let x = Su3Algebra::basis_element(0);
1329 let y = Su3Algebra::basis_element(3);
1330 let z = Su3Algebra::basis_element(7);
1331
1332 let t1 = x.bracket(&y.bracket(&z));
1334 let t2 = y.bracket(&z.bracket(&x));
1335 let t3 = z.bracket(&x.bracket(&y));
1336 let sum = t1.add(&t2).add(&t3);
1337
1338 assert!(
1339 sum.norm() < 1e-10,
1340 "Jacobi identity violated for SU(3): ||sum|| = {:.2e}",
1341 sum.norm()
1342 );
1343 }
1344
1345 #[test]
1346 fn test_bracket_bilinearity() {
1347 use crate::traits::LieAlgebra;
1348
1349 let x = Su3Algebra::basis_element(0);
1350 let y = Su3Algebra::basis_element(2);
1351 let z = Su3Algebra::basis_element(5);
1352 let alpha = 3.7;
1353
1354 let lhs = x.scale(alpha).add(&y).bracket(&z);
1356 let rhs = x.bracket(&z).scale(alpha).add(&y.bracket(&z));
1357 for i in 0..8 {
1358 assert_relative_eq!(lhs.0[i], rhs.0[i], epsilon = 1e-12);
1359 }
1360 }
1361
1362 #[test]
1363 fn test_exp_large_algebra_element() {
1364 use crate::traits::LieGroup;
1365
1366 let large_algebra = Su3Algebra([2.0, 1.5, -1.8, 0.9, -1.2, 1.1, -0.8, 1.3]);
1369 let norm = large_algebra.norm();
1370
1371 assert!(norm > 1.0, "Test requires ||X|| > 1, got {}", norm);
1373
1374 let g = SU3::exp(&large_algebra);
1376
1377 assert!(
1379 g.verify_unitarity(1e-8),
1380 "Unitarity violated for large algebra element"
1381 );
1382
1383 assert!(g.distance_to_identity() > 0.1);
1385 }
1386
1387 #[test]
1388 fn test_exp_very_small_algebra_element() {
1389 use crate::traits::LieGroup;
1390
1391 let small_algebra = Su3Algebra([1e-8, 2e-8, -1e-8, 3e-9, -2e-9, 1e-9, -5e-10, 2e-10]);
1393
1394 let g = SU3::exp(&small_algebra);
1395
1396 assert!(g.distance_to_identity() < 1e-7);
1398 assert!(g.verify_unitarity(1e-12));
1399 }
1400
1401 #[test]
1402 fn test_exp_scaling_correctness() {
1403 use crate::traits::LieGroup;
1404
1405 let algebra = Su3Algebra([0.5, 0.3, -0.4, 0.2, -0.3, 0.1, -0.2, 0.25]);
1407
1408 let exp_x = SU3::exp(&algebra);
1409 let exp_2x = SU3::exp(&algebra.scale(2.0));
1410 let exp_x_squared = exp_x.compose(&exp_x);
1411
1412 let distance = exp_2x.distance(&exp_x_squared);
1414 assert!(
1415 distance < 1e-6,
1416 "exp(2X) should equal exp(X)^2, distance = {}",
1417 distance
1418 );
1419 }
1420
1421 #[cfg(feature = "proptest")]
1431 use proptest::prelude::*;
1432
1433 #[cfg(feature = "proptest")]
1439 fn arb_su3() -> impl Strategy<Value = SU3> {
1440 let range = -0.5_f64..0.5_f64;
1442
1443 (
1444 range.clone(),
1445 range.clone(),
1446 range.clone(),
1447 range.clone(),
1448 range.clone(),
1449 range.clone(),
1450 range.clone(),
1451 range,
1452 )
1453 .prop_map(|(a1, a2, a3, a4, a5, a6, a7, a8)| {
1454 let algebra = Su3Algebra([a1, a2, a3, a4, a5, a6, a7, a8]);
1455 SU3::exp(&algebra)
1456 })
1457 }
1458
1459 #[cfg(feature = "proptest")]
1461 fn arb_su3_algebra() -> impl Strategy<Value = Su3Algebra> {
1462 let range = -0.5_f64..0.5_f64;
1463
1464 (
1465 range.clone(),
1466 range.clone(),
1467 range.clone(),
1468 range.clone(),
1469 range.clone(),
1470 range.clone(),
1471 range.clone(),
1472 range,
1473 )
1474 .prop_map(|(a1, a2, a3, a4, a5, a6, a7, a8)| {
1475 Su3Algebra([a1, a2, a3, a4, a5, a6, a7, a8])
1476 })
1477 }
1478
1479 #[cfg(feature = "proptest")]
1480 proptest! {
1481 #[test]
1489 fn prop_identity_axiom(u in arb_su3()) {
1490 let e = SU3::identity();
1491
1492 let left = e.compose(&u);
1494 prop_assert!(
1495 left.distance(&u) < 1e-6,
1496 "Left identity failed: I·U != U, distance = {}",
1497 left.distance(&u)
1498 );
1499
1500 let right = u.compose(&e);
1502 prop_assert!(
1503 right.distance(&u) < 1e-6,
1504 "Right identity failed: U·I != U, distance = {}",
1505 right.distance(&u)
1506 );
1507 }
1508
1509 #[test]
1517 fn prop_inverse_axiom(u in arb_su3()) {
1518 let u_inv = u.inverse();
1519
1520 let right_product = u.compose(&u_inv);
1522 prop_assert!(
1523 right_product.is_near_identity(1e-6),
1524 "Right inverse failed: U·U† != I, distance = {}",
1525 right_product.distance_to_identity()
1526 );
1527
1528 let left_product = u_inv.compose(&u);
1530 prop_assert!(
1531 left_product.is_near_identity(1e-6),
1532 "Left inverse failed: U†·U != I, distance = {}",
1533 left_product.distance_to_identity()
1534 );
1535 }
1536
1537 #[test]
1544 fn prop_associativity(u1 in arb_su3(), u2 in arb_su3(), u3 in arb_su3()) {
1545 let left_assoc = u1.compose(&u2).compose(&u3);
1547
1548 let right_assoc = u1.compose(&u2.compose(&u3));
1550
1551 prop_assert!(
1552 left_assoc.distance(&right_assoc) < 1e-6,
1553 "Associativity failed: (U₁·U₂)·U₃ != U₁·(U₂·U₃), distance = {}",
1554 left_assoc.distance(&right_assoc)
1555 );
1556 }
1557
1558 #[test]
1563 fn prop_inverse_continuity(u in arb_su3()) {
1564 let epsilon = 0.01;
1566 let perturbation = SU3::exp(&Su3Algebra([epsilon, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
1567 let u_perturbed = u.compose(&perturbation);
1568
1569 let inv_distance = u.inverse().distance(&u_perturbed.inverse());
1571
1572 prop_assert!(
1573 inv_distance < 0.1,
1574 "Inverse not continuous: small perturbation caused large inverse change, distance = {}",
1575 inv_distance
1576 );
1577 }
1578
1579 #[test]
1584 fn prop_unitarity_preserved(u1 in arb_su3(), u2 in arb_su3()) {
1585 let product = u1.compose(&u2);
1587 prop_assert!(
1588 product.verify_unitarity(1e-10),
1589 "Composition violated unitarity"
1590 );
1591
1592 let inv = u1.inverse();
1594 prop_assert!(
1595 inv.verify_unitarity(1e-10),
1596 "Inverse violated unitarity"
1597 );
1598 }
1599
1600 #[test]
1608 fn prop_adjoint_homomorphism(
1609 u1 in arb_su3(),
1610 u2 in arb_su3(),
1611 x in arb_su3_algebra()
1612 ) {
1613 let u_composed = u1.compose(&u2);
1615 let left = u_composed.adjoint_action(&x);
1616
1617 let ad_u2_x = u2.adjoint_action(&x);
1619 let right = u1.adjoint_action(&ad_u2_x);
1620
1621 let diff = left.add(&right.scale(-1.0));
1623 prop_assert!(
1624 diff.norm() < 1e-6,
1625 "Adjoint homomorphism failed: Ad_{{U₁∘U₂}}(X) != Ad_{{U₁}}(Ad_{{U₂}}(X)), diff norm = {}",
1626 diff.norm()
1627 );
1628 }
1629
1630 #[test]
1635 fn prop_adjoint_identity(x in arb_su3_algebra()) {
1636 let e = SU3::identity();
1637 let result = e.adjoint_action(&x);
1638
1639 let diff = result.add(&x.scale(-1.0));
1640 prop_assert!(
1641 diff.norm() < 1e-10,
1642 "Identity action failed: Ad_I(X) != X, diff norm = {}",
1643 diff.norm()
1644 );
1645 }
1646
1647 #[test]
1655 fn prop_adjoint_bracket_preservation(
1656 u in arb_su3(),
1657 x in arb_su3_algebra(),
1658 y in arb_su3_algebra()
1659 ) {
1660 use crate::traits::LieAlgebra;
1661
1662 let bracket_xy = x.bracket(&y);
1664 let left = u.adjoint_action(&bracket_xy);
1665
1666 let ad_x = u.adjoint_action(&x);
1668 let ad_y = u.adjoint_action(&y);
1669 let right = ad_x.bracket(&ad_y);
1670
1671 let diff = left.add(&right.scale(-1.0));
1673 prop_assert!(
1674 diff.norm() < 1e-5,
1675 "Bracket preservation failed: Ad_U([X,Y]) != [Ad_U(X), Ad_U(Y)], diff norm = {}",
1676 diff.norm()
1677 );
1678 }
1679
1680 #[test]
1685 fn prop_adjoint_inverse(u in arb_su3(), x in arb_su3_algebra()) {
1686 let ad_u_x = u.adjoint_action(&x);
1688 let u_inv = u.inverse();
1689 let result = u_inv.adjoint_action(&ad_u_x);
1690
1691 let diff = result.add(&x.scale(-1.0));
1693 prop_assert!(
1694 diff.norm() < 1e-6,
1695 "Inverse property failed: Ad_{{U†}}(Ad_U(X)) != X, diff norm = {}",
1696 diff.norm()
1697 );
1698 }
1699 }
1700
1701 #[test]
1712 fn test_exp_log_roundtrip() {
1713 use crate::traits::{LieAlgebra, LieGroup};
1714 use rand::SeedableRng;
1715 use rand_distr::{Distribution, Uniform};
1716
1717 let mut rng = rand::rngs::StdRng::seed_from_u64(11111);
1718 let dist = Uniform::new(-0.5, 0.5); for _ in 0..50 {
1721 let mut coeffs = [0.0; 8];
1722 for coeff in &mut coeffs {
1723 *coeff = dist.sample(&mut rng);
1724 }
1725 let x = Su3Algebra(coeffs);
1726
1727 let g = SU3::exp(&x);
1729 let x_recovered = g.log().expect("log should succeed for exp output");
1730
1731 let diff = x.add(&x_recovered.scale(-1.0));
1733 assert!(
1734 diff.norm() < 1e-10,
1735 "log(exp(X)) should equal X: ||diff|| = {:.2e}",
1736 diff.norm()
1737 );
1738 }
1739 }
1740
1741 #[test]
1748 fn test_log_exp_roundtrip() {
1749 use crate::traits::LieGroup;
1750 use rand::SeedableRng;
1751 use rand_distr::{Distribution, Uniform};
1752
1753 let mut rng = rand::rngs::StdRng::seed_from_u64(22222);
1754 let dist = Uniform::new(-0.5, 0.5);
1755
1756 for _ in 0..50 {
1757 let mut coeffs = [0.0; 8];
1759 for coeff in &mut coeffs {
1760 *coeff = dist.sample(&mut rng);
1761 }
1762 let g = SU3::exp(&Su3Algebra(coeffs));
1763
1764 let x = g.log().expect("log should succeed for valid SU(3) element");
1766 let g_recovered = SU3::exp(&x);
1767
1768 let diff = g.compose(&g_recovered.inverse()).distance_to_identity();
1770 assert!(
1771 diff < 1e-10,
1772 "exp(log(g)) should equal g: diff = {:.2e}",
1773 diff
1774 );
1775 }
1776 }
1777
1778 #[test]
1783 fn test_su3_casimir_trivial() {
1784 use crate::Casimir;
1786 use crate::Su3Irrep;
1787
1788 let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::TRIVIAL);
1789 assert_eq!(c2, 0.0, "Casimir of trivial representation should be 0");
1790 }
1791
1792 #[test]
1793 fn test_su3_casimir_fundamental() {
1794 use crate::Casimir;
1796 use crate::Su3Irrep;
1797
1798 let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::FUNDAMENTAL);
1799 let expected = 4.0 / 3.0;
1800 assert!(
1801 (c2 - expected).abs() < 1e-10,
1802 "Casimir of fundamental should be 4/3, got {}",
1803 c2
1804 );
1805 }
1806
1807 #[test]
1808 fn test_su3_casimir_antifundamental() {
1809 use crate::Casimir;
1811 use crate::Su3Irrep;
1812
1813 let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::ANTIFUNDAMENTAL);
1814 let expected = 4.0 / 3.0;
1815 assert!(
1816 (c2 - expected).abs() < 1e-10,
1817 "Casimir of antifundamental should be 4/3, got {}",
1818 c2
1819 );
1820 }
1821
1822 #[test]
1823 fn test_su3_casimir_adjoint() {
1824 use crate::Casimir;
1826 use crate::Su3Irrep;
1827
1828 let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::ADJOINT);
1829 assert_eq!(c2, 3.0, "Casimir of adjoint representation should be 3");
1830 }
1831
1832 #[test]
1833 fn test_su3_casimir_symmetric() {
1834 use crate::Casimir;
1836 use crate::Su3Irrep;
1837
1838 let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::SYMMETRIC);
1839 let expected = 10.0 / 3.0;
1840 assert!(
1841 (c2 - expected).abs() < 1e-10,
1842 "Casimir of symmetric should be 10/3, got {}",
1843 c2
1844 );
1845 }
1846
1847 #[test]
1848 fn test_su3_casimir_formula() {
1849 use crate::Casimir;
1851 use crate::Su3Irrep;
1852
1853 for p in 0..5 {
1854 for q in 0..5 {
1855 let irrep = Su3Irrep::new(p, q);
1856 let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&irrep);
1857
1858 let pf = p as f64;
1859 let qf = q as f64;
1860 let expected = (pf * pf + qf * qf + pf * qf + 3.0 * pf + 3.0 * qf) / 3.0;
1861
1862 assert!(
1863 (c2 - expected).abs() < 1e-10,
1864 "Casimir for ({},{}) should be {}, got {}",
1865 p,
1866 q,
1867 expected,
1868 c2
1869 );
1870 }
1871 }
1872 }
1873
1874 #[test]
1875 fn test_su3_rank() {
1876 use crate::Casimir;
1878
1879 assert_eq!(Su3Algebra::rank(), 2, "SU(3) should have rank 2");
1880 assert_eq!(
1881 Su3Algebra::num_casimirs(),
1882 2,
1883 "SU(3) should have 2 Casimir operators"
1884 );
1885 }
1886
1887 #[test]
1892 fn test_su3_cubic_casimir_trivial() {
1893 use crate::Casimir;
1895 use crate::Su3Irrep;
1896
1897 let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::TRIVIAL);
1898 assert_eq!(c3_vec.len(), 1, "Should return exactly one higher Casimir");
1899 assert_eq!(c3_vec[0], 0.0, "Cubic Casimir of trivial should be 0");
1900 }
1901
1902 #[test]
1903 fn test_su3_cubic_casimir_fundamental() {
1904 use crate::Casimir;
1906 use crate::Su3Irrep;
1907
1908 let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::FUNDAMENTAL);
1909 let c3 = c3_vec[0];
1910 let expected = 10.0 / 9.0;
1911 assert!(
1912 (c3 - expected).abs() < 1e-10,
1913 "Cubic Casimir of fundamental should be 10/9, got {}",
1914 c3
1915 );
1916 }
1917
1918 #[test]
1919 fn test_su3_cubic_casimir_antifundamental() {
1920 use crate::Casimir;
1922 use crate::Su3Irrep;
1923
1924 let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::ANTIFUNDAMENTAL);
1925 let c3 = c3_vec[0];
1926 let expected = -10.0 / 9.0;
1927 assert!(
1928 (c3 - expected).abs() < 1e-10,
1929 "Cubic Casimir of antifundamental should be -10/9, got {}",
1930 c3
1931 );
1932 }
1933
1934 #[test]
1935 fn test_su3_cubic_casimir_adjoint() {
1936 use crate::Casimir;
1938 use crate::Su3Irrep;
1939
1940 let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::ADJOINT);
1941 let c3 = c3_vec[0];
1942 assert!(
1943 c3.abs() < 1e-10,
1944 "Cubic Casimir of adjoint (self-conjugate) should be 0, got {}",
1945 c3
1946 );
1947 }
1948
1949 #[test]
1950 fn test_su3_cubic_casimir_symmetric() {
1951 use crate::Casimir;
1953 use crate::Su3Irrep;
1954
1955 let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::SYMMETRIC);
1956 let c3 = c3_vec[0];
1957 let expected = 70.0 / 18.0;
1958 assert!(
1959 (c3 - expected).abs() < 1e-10,
1960 "Cubic Casimir of symmetric should be 70/18, got {}",
1961 c3
1962 );
1963 }
1964
1965 #[test]
1966 fn test_su3_cubic_casimir_conjugation_symmetry() {
1967 use crate::Casimir;
1969 use crate::Su3Irrep;
1970
1971 for p in 0..5 {
1972 for q in 0..5 {
1973 let irrep_pq = Su3Irrep::new(p, q);
1974 let irrep_qp = Su3Irrep::new(q, p);
1975
1976 let c3_pq = Su3Algebra::higher_casimir_eigenvalues(&irrep_pq)[0];
1977 let c3_qp = Su3Algebra::higher_casimir_eigenvalues(&irrep_qp)[0];
1978
1979 assert!(
1980 (c3_pq + c3_qp).abs() < 1e-10,
1981 "c₃({},{}) = {} should equal -c₃({},{}) = {}",
1982 p,
1983 q,
1984 c3_pq,
1985 q,
1986 p,
1987 -c3_qp
1988 );
1989 }
1990 }
1991 }
1992
1993 #[test]
1994 fn test_su3_cubic_casimir_formula() {
1995 use crate::Casimir;
1997 use crate::Su3Irrep;
1998
1999 for p in 0..5 {
2000 for q in 0..5 {
2001 let irrep = Su3Irrep::new(p, q);
2002 let c3 = Su3Algebra::higher_casimir_eigenvalues(&irrep)[0];
2003
2004 let pf = p as f64;
2005 let qf = q as f64;
2006 let expected = (pf - qf) * (2.0 * pf + qf + 3.0) * (pf + 2.0 * qf + 3.0) / 18.0;
2007
2008 assert!(
2009 (c3 - expected).abs() < 1e-10,
2010 "Cubic Casimir for ({},{}) should be {}, got {}",
2011 p,
2012 q,
2013 expected,
2014 c3
2015 );
2016 }
2017 }
2018 }
2019
2020 #[test]
2032 fn test_gram_schmidt_small_matrix() {
2033 let small_algebra = Su3Algebra([1e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
2035 let small_element = SU3::exp(&small_algebra);
2036
2037 assert!(
2039 small_element.verify_unitarity(1e-10),
2040 "Small element should be unitary"
2041 );
2042
2043 let recovered = small_element
2045 .log()
2046 .expect("log should succeed for near-identity");
2047 let diff = (recovered.0[0] - small_algebra.0[0]).abs();
2048 assert!(
2049 diff < 1e-10,
2050 "exp/log round-trip failed for small element: diff = {}",
2051 diff
2052 );
2053 }
2054
2055 #[test]
2060 fn test_gram_schmidt_large_matrix() {
2061 use crate::traits::LieGroup;
2062
2063 let large_algebra = Su3Algebra([2.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
2065 let large_element = SU3::exp(&large_algebra);
2066
2067 assert!(
2069 large_element.verify_unitarity(1e-8),
2070 "Large rotation element should be unitary, distance = {}",
2071 large_element.distance_to_identity()
2072 );
2073 }
2074
2075 #[test]
2079 fn test_exp_repeated_squaring_stability() {
2080 use crate::traits::LieGroup;
2081
2082 let algebra = Su3Algebra([1.0, 1.0, 0.5, 0.3, 0.2, 0.1, 0.0, 0.0]);
2085 let element = SU3::exp(&algebra);
2086
2087 assert!(
2089 element.verify_unitarity(1e-10),
2090 "Repeated squaring should preserve unitarity"
2091 );
2092
2093 let u_dag_u = element.conjugate_transpose().compose(&element);
2098 let trace = u_dag_u.trace();
2099 assert!(
2100 (trace.re - 3.0).abs() < 1e-10 && trace.im.abs() < 1e-10,
2101 "U†U should have trace 3, got {:.6}+{:.6}i",
2102 trace.re,
2103 trace.im
2104 );
2105 }
2106
2107 #[test]
2112 fn test_exp_log_near_boundary() {
2113 use crate::traits::LieGroup;
2114
2115 let theta = std::f64::consts::PI - 0.1;
2117 let algebra = Su3Algebra([theta, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
2118 let element = SU3::exp(&algebra);
2119
2120 assert!(
2122 element.verify_unitarity(1e-10),
2123 "Near-boundary element should be unitary"
2124 );
2125
2126 if let Ok(recovered) = element.log() {
2128 let round_trip = SU3::exp(&recovered);
2130 let distance = element.distance(&round_trip);
2131 assert!(
2132 distance < 1e-6,
2133 "Round trip should be close, distance = {}",
2134 distance
2135 );
2136 }
2137 }
2140
2141 #[test]
2145 fn test_composition_stability() {
2146 use crate::traits::LieGroup;
2147
2148 let u1 = SU3::exp(&Su3Algebra([0.5, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
2150 let u2 = SU3::exp(&Su3Algebra([0.0, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0]));
2151 let u3 = SU3::exp(&Su3Algebra([0.0, 0.0, 0.3, 0.5, 0.0, 0.0, 0.0, 0.0]));
2152
2153 let mut result = SU3::identity();
2155 for _ in 0..100 {
2156 result = result.compose(&u1);
2157 result = result.compose(&u2);
2158 result = result.compose(&u3);
2159 }
2160
2161 assert!(
2163 result.verify_unitarity(1e-8),
2164 "100 compositions should still be unitary"
2165 );
2166 }
2167}