use crate::traits::{AntiHermitianByConstruction, LieAlgebra, LieGroup, TracelessByConstruction};
use ndarray::Array2;
use num_complex::Complex64;
use std::fmt;
use std::ops::{Add, Mul, MulAssign, Neg, Sub};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Su3Algebra(pub(crate) [f64; 8]);
impl Su3Algebra {
#[must_use]
pub fn new(components: [f64; 8]) -> Self {
Self(components)
}
#[must_use]
pub fn components(&self) -> &[f64; 8] {
&self.0
}
}
impl Add for Su3Algebra {
type Output = Self;
fn add(self, rhs: Self) -> Self {
let mut r = [0.0; 8];
for i in 0..8 {
r[i] = self.0[i] + rhs.0[i];
}
Self(r)
}
}
impl Add<&Su3Algebra> for Su3Algebra {
type Output = Su3Algebra;
fn add(self, rhs: &Su3Algebra) -> Su3Algebra {
self + *rhs
}
}
impl Add<Su3Algebra> for &Su3Algebra {
type Output = Su3Algebra;
fn add(self, rhs: Su3Algebra) -> Su3Algebra {
*self + rhs
}
}
impl Add<&Su3Algebra> for &Su3Algebra {
type Output = Su3Algebra;
fn add(self, rhs: &Su3Algebra) -> Su3Algebra {
*self + *rhs
}
}
impl Sub for Su3Algebra {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
let mut r = [0.0; 8];
for i in 0..8 {
r[i] = self.0[i] - rhs.0[i];
}
Self(r)
}
}
impl Neg for Su3Algebra {
type Output = Self;
fn neg(self) -> Self {
let mut r = [0.0; 8];
for i in 0..8 {
r[i] = -self.0[i];
}
Self(r)
}
}
impl Mul<f64> for Su3Algebra {
type Output = Self;
fn mul(self, scalar: f64) -> Self {
let mut r = [0.0; 8];
for i in 0..8 {
r[i] = self.0[i] * scalar;
}
Self(r)
}
}
impl Mul<Su3Algebra> for f64 {
type Output = Su3Algebra;
fn mul(self, rhs: Su3Algebra) -> Su3Algebra {
rhs * self
}
}
impl LieAlgebra for Su3Algebra {
const DIM: usize = 8;
fn zero() -> Self {
Self([0.0; 8])
}
fn add(&self, other: &Self) -> Self {
let mut result = [0.0; 8];
for i in 0..8 {
result[i] = self.0[i] + other.0[i];
}
Self(result)
}
fn scale(&self, scalar: f64) -> Self {
let mut result = [0.0; 8];
for i in 0..8 {
result[i] = self.0[i] * scalar;
}
Self(result)
}
fn norm(&self) -> f64 {
self.0.iter().map(|x| x.powi(2)).sum::<f64>().sqrt()
}
fn basis_element(i: usize) -> Self {
assert!(i < 8, "SU(3) algebra is 8-dimensional");
let mut coeffs = [0.0; 8];
coeffs[i] = 1.0;
Self(coeffs)
}
fn from_components(components: &[f64]) -> Self {
assert_eq!(components.len(), 8, "su(3) has dimension 8");
let mut coeffs = [0.0; 8];
coeffs.copy_from_slice(components);
Self(coeffs)
}
fn to_components(&self) -> Vec<f64> {
self.0.to_vec()
}
fn bracket(&self, other: &Self) -> Self {
const SQRT3_HALF: f64 = 0.866_025_403_784_438_6;
#[rustfmt::skip]
const STRUCTURE_CONSTANTS: [(usize, usize, usize, f64); 54] = [
(0, 1, 2, 1.0), (1, 2, 0, 1.0), (2, 0, 1, 1.0),
(1, 0, 2, -1.0), (0, 2, 1, -1.0), (2, 1, 0, -1.0),
(0, 3, 6, 0.5), (3, 6, 0, 0.5), (6, 0, 3, 0.5),
(3, 0, 6, -0.5), (0, 6, 3, -0.5), (6, 3, 0, -0.5),
(0, 4, 5, -0.5), (4, 5, 0, -0.5), (5, 0, 4, -0.5),
(4, 0, 5, 0.5), (0, 5, 4, 0.5), (5, 4, 0, 0.5),
(1, 3, 5, 0.5), (3, 5, 1, 0.5), (5, 1, 3, 0.5),
(3, 1, 5, -0.5), (1, 5, 3, -0.5), (5, 3, 1, -0.5),
(1, 4, 6, 0.5), (4, 6, 1, 0.5), (6, 1, 4, 0.5),
(4, 1, 6, -0.5), (1, 6, 4, -0.5), (6, 4, 1, -0.5),
(2, 3, 4, 0.5), (3, 4, 2, 0.5), (4, 2, 3, 0.5),
(3, 2, 4, -0.5), (2, 4, 3, -0.5), (4, 3, 2, -0.5),
(2, 5, 6, -0.5), (5, 6, 2, -0.5), (6, 2, 5, -0.5),
(5, 2, 6, 0.5), (2, 6, 5, 0.5), (6, 5, 2, 0.5),
(3, 4, 7, SQRT3_HALF), (4, 7, 3, SQRT3_HALF), (7, 3, 4, SQRT3_HALF),
(4, 3, 7, -SQRT3_HALF), (3, 7, 4, -SQRT3_HALF), (7, 4, 3, -SQRT3_HALF),
(5, 6, 7, SQRT3_HALF), (6, 7, 5, SQRT3_HALF), (7, 5, 6, SQRT3_HALF),
(6, 5, 7, -SQRT3_HALF), (5, 7, 6, -SQRT3_HALF), (7, 6, 5, -SQRT3_HALF),
];
let mut result = [0.0; 8];
for &(i, j, k, f) in &STRUCTURE_CONSTANTS {
result[k] += self.0[i] * other.0[j] * f;
}
for r in &mut result {
*r *= -1.0;
}
Self(result)
}
#[inline]
fn inner(&self, other: &Self) -> f64 {
let mut sum = 0.0;
for i in 0..8 {
sum += self.0[i] * other.0[i];
}
sum
}
}
impl crate::Casimir for Su3Algebra {
type Representation = crate::Su3Irrep;
fn quadratic_casimir_eigenvalue(irrep: &Self::Representation) -> f64 {
let p = irrep.p as f64;
let q = irrep.q as f64;
(p * p + q * q + p * q + 3.0 * p + 3.0 * q) / 3.0
}
fn higher_casimir_eigenvalues(irrep: &Self::Representation) -> Vec<f64> {
let p = irrep.p as f64;
let q = irrep.q as f64;
let c3 = (p - q) * (2.0 * p + q + 3.0) * (p + 2.0 * q + 3.0) / 18.0;
vec![c3]
}
fn rank() -> usize {
2 }
}
impl Su3Algebra {
#[must_use]
pub fn to_matrix(&self) -> Array2<Complex64> {
let [a1, a2, a3, a4, a5, a6, a7, a8] = self.0;
let i = Complex64::new(0.0, 1.0);
let sqrt3_inv = 1.0 / 3_f64.sqrt();
let mut matrix = Array2::zeros((3, 3));
matrix[[0, 1]] += i * a1;
matrix[[1, 0]] += i * a1;
matrix[[0, 1]] += i * Complex64::new(0.0, -a2); matrix[[1, 0]] += i * Complex64::new(0.0, a2);
matrix[[0, 0]] += i * a3;
matrix[[1, 1]] += -i * a3;
matrix[[0, 2]] += i * a4;
matrix[[2, 0]] += i * a4;
matrix[[0, 2]] += i * Complex64::new(0.0, -a5);
matrix[[2, 0]] += i * Complex64::new(0.0, a5);
matrix[[1, 2]] += i * a6;
matrix[[2, 1]] += i * a6;
matrix[[1, 2]] += i * Complex64::new(0.0, -a7);
matrix[[2, 1]] += i * Complex64::new(0.0, a7);
matrix[[0, 0]] += i * a8 * sqrt3_inv;
matrix[[1, 1]] += i * a8 * sqrt3_inv;
matrix[[2, 2]] += -i * a8 * sqrt3_inv * 2.0;
matrix.mapv_inplace(|z| z * 0.5);
matrix
}
#[must_use]
pub fn from_matrix(matrix: &Array2<Complex64>) -> Self {
let i = Complex64::new(0.0, 1.0);
let neg_i = -i;
let mut coeffs = [0.0; 8];
for j in 0..8 {
let lambda_j = Self::gell_mann_matrix(j);
let product = matrix.dot(&lambda_j);
let trace = product[[0, 0]] + product[[1, 1]] + product[[2, 2]];
coeffs[j] = (neg_i * trace).re;
}
Self(coeffs)
}
#[must_use]
pub fn gell_mann_matrix(j: usize) -> Array2<Complex64> {
assert!(j < 8, "Gell-Mann matrices are indexed 0..7");
let mut matrix = Array2::zeros((3, 3));
let i = Complex64::new(0.0, 1.0);
let sqrt3_inv = 1.0 / 3_f64.sqrt();
match j {
0 => {
matrix[[0, 1]] = Complex64::new(1.0, 0.0);
matrix[[1, 0]] = Complex64::new(1.0, 0.0);
}
1 => {
matrix[[0, 1]] = -i;
matrix[[1, 0]] = i;
}
2 => {
matrix[[0, 0]] = Complex64::new(1.0, 0.0);
matrix[[1, 1]] = Complex64::new(-1.0, 0.0);
}
3 => {
matrix[[0, 2]] = Complex64::new(1.0, 0.0);
matrix[[2, 0]] = Complex64::new(1.0, 0.0);
}
4 => {
matrix[[0, 2]] = -i;
matrix[[2, 0]] = i;
}
5 => {
matrix[[1, 2]] = Complex64::new(1.0, 0.0);
matrix[[2, 1]] = Complex64::new(1.0, 0.0);
}
6 => {
matrix[[1, 2]] = -i;
matrix[[2, 1]] = i;
}
7 => {
matrix[[0, 0]] = Complex64::new(sqrt3_inv, 0.0);
matrix[[1, 1]] = Complex64::new(sqrt3_inv, 0.0);
matrix[[2, 2]] = Complex64::new(-2.0 * sqrt3_inv, 0.0);
}
_ => unreachable!(),
}
matrix
}
#[inline]
#[must_use]
#[allow(dead_code)]
fn structure_constant(i: usize, j: usize, k: usize) -> f64 {
const SQRT3_HALF: f64 = 0.866_025_403_784_438_6;
if i == j || i == k || j == k {
return 0.0;
}
if i > j {
return -Self::structure_constant(j, i, k);
}
match (i, j, k) {
(0, 1, 2) | (1, 2, 0) | (2, 0, 1) => 1.0,
(0, 2, 1) | (2, 1, 0) | (1, 0, 2) => -1.0,
(0, 3, 6) | (3, 6, 0) | (6, 0, 3) => 0.5,
(0, 6, 3) | (6, 3, 0) | (3, 0, 6) => -0.5,
(0, 4, 5) | (4, 5, 0) | (5, 0, 4) => -0.5,
(0, 5, 4) | (5, 4, 0) | (4, 0, 5) => 0.5,
(1, 3, 5) | (3, 5, 1) | (5, 1, 3) => 0.5,
(1, 5, 3) | (5, 3, 1) | (3, 1, 5) => -0.5,
(1, 4, 6) | (4, 6, 1) | (6, 1, 4) => 0.5,
(1, 6, 4) | (6, 4, 1) | (4, 1, 6) => -0.5,
(2, 3, 4) | (3, 4, 2) | (4, 2, 3) => 0.5,
(2, 4, 3) | (4, 3, 2) | (3, 2, 4) => -0.5,
(2, 5, 6) | (5, 6, 2) | (6, 2, 5) => -0.5,
(2, 6, 5) | (6, 5, 2) | (5, 2, 6) => 0.5,
(3, 4, 7) | (4, 7, 3) | (7, 3, 4) => SQRT3_HALF,
(3, 7, 4) | (7, 4, 3) | (4, 3, 7) => -SQRT3_HALF,
(5, 6, 7) | (6, 7, 5) | (7, 5, 6) => SQRT3_HALF,
(5, 7, 6) | (7, 6, 5) | (6, 5, 7) => -SQRT3_HALF,
_ => 0.0,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SU3 {
pub(crate) matrix: Array2<Complex64>,
}
impl SU3 {
#[must_use]
pub fn matrix(&self) -> &Array2<Complex64> {
&self.matrix
}
#[must_use]
pub fn identity() -> Self {
let mut matrix = Array2::zeros((3, 3));
matrix[[0, 0]] = Complex64::new(1.0, 0.0);
matrix[[1, 1]] = Complex64::new(1.0, 0.0);
matrix[[2, 2]] = Complex64::new(1.0, 0.0);
Self { matrix }
}
#[must_use]
pub fn verify_unitarity(&self, tolerance: f64) -> bool {
let adjoint = self.matrix.t().mapv(|z| z.conj());
let product = adjoint.dot(&self.matrix);
let mut identity = Array2::zeros((3, 3));
identity[[0, 0]] = Complex64::new(1.0, 0.0);
identity[[1, 1]] = Complex64::new(1.0, 0.0);
identity[[2, 2]] = Complex64::new(1.0, 0.0);
let diff = product - identity;
let norm: f64 = diff
.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
norm < tolerance
}
#[must_use]
pub fn inverse(&self) -> Self {
Self {
matrix: self.matrix.t().mapv(|z| z.conj()),
}
}
#[must_use]
pub fn conjugate_transpose(&self) -> Self {
self.inverse()
}
#[must_use]
pub fn trace(&self) -> Complex64 {
self.matrix[[0, 0]] + self.matrix[[1, 1]] + self.matrix[[2, 2]]
}
#[must_use]
pub fn distance_to_identity(&self) -> f64 {
let mut identity = Array2::zeros((3, 3));
identity[[0, 0]] = Complex64::new(1.0, 0.0);
identity[[1, 1]] = Complex64::new(1.0, 0.0);
identity[[2, 2]] = Complex64::new(1.0, 0.0);
let diff = &self.matrix - &identity;
diff.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt()
}
#[must_use]
fn gram_schmidt_project(matrix: Array2<Complex64>) -> Array2<Complex64> {
let mut result: Array2<Complex64> = Array2::zeros((3, 3));
let matrix_norm: f64 = matrix
.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
const MACHINE_EPSILON: f64 = 2.2e-16;
const ABSOLUTE_FLOOR: f64 = 1e-14;
let relative_threshold = MACHINE_EPSILON * matrix_norm;
let threshold = relative_threshold.max(ABSOLUTE_FLOOR);
for j in 0..3 {
let mut col = matrix.column(j).to_owned();
for k in 0..j {
let prev_col = result.column(k);
let proj: Complex64 = prev_col
.iter()
.zip(col.iter())
.map(|(p, c)| p.conj() * c)
.sum();
for i in 0..3 {
col[i] -= proj * prev_col[i];
}
}
let norm: f64 = col
.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
debug_assert!(
norm > threshold,
"Gram-Schmidt: column {} is linearly dependent (norm = {:.2e}, threshold = {:.2e}). \
Input matrix is rank-deficient.",
j,
norm,
threshold
);
if norm > threshold {
for i in 0..3 {
result[[i, j]] = col[i] / norm;
}
}
}
let det = result[[0, 0]]
* (result[[1, 1]] * result[[2, 2]] - result[[1, 2]] * result[[2, 1]])
- result[[0, 1]] * (result[[1, 0]] * result[[2, 2]] - result[[1, 2]] * result[[2, 0]])
+ result[[0, 2]] * (result[[1, 0]] * result[[2, 1]] - result[[1, 1]] * result[[2, 0]]);
let det_norm = det.norm();
if det_norm < threshold {
return Array2::eye(3);
}
let det_phase = det / det_norm;
let correction = (det_phase.conj()).powf(1.0 / 3.0);
result.mapv_inplace(|z| z * correction);
result
}
#[must_use]
fn exp_taylor(matrix: &Array2<Complex64>, max_terms: usize) -> Self {
let mut result = Array2::zeros((3, 3));
result[[0, 0]] = Complex64::new(1.0, 0.0);
result[[1, 1]] = Complex64::new(1.0, 0.0);
result[[2, 2]] = Complex64::new(1.0, 0.0);
let mut term = matrix.clone();
let mut factorial = 1.0;
for n in 1..=max_terms {
factorial *= n as f64;
result += &term.mapv(|z| z / factorial);
let term_norm: f64 = term
.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
if term_norm / factorial < 1e-14 {
break;
}
if n < max_terms {
term = term.dot(matrix);
}
}
Self { matrix: result }
}
fn matrix_sqrt_db(u: &Array2<Complex64>) -> Array2<Complex64> {
use nalgebra::{Complex as NaComplex, Matrix3};
fn to_nalgebra(a: &Array2<Complex64>) -> Matrix3<NaComplex<f64>> {
Matrix3::from_fn(|i, j| NaComplex::new(a[[i, j]].re, a[[i, j]].im))
}
fn to_ndarray(m: &Matrix3<NaComplex<f64>>) -> Array2<Complex64> {
Array2::from_shape_fn((3, 3), |(i, j)| Complex64::new(m[(i, j)].re, m[(i, j)].im))
}
let mut y = to_nalgebra(u);
let mut z = Matrix3::<NaComplex<f64>>::identity();
const MAX_ITERS: usize = 20;
const TOL: f64 = 1e-14;
for _ in 0..MAX_ITERS {
let y_inv = y.try_inverse().unwrap_or(y.adjoint()); let z_inv = z.try_inverse().unwrap_or(z.adjoint());
let y_new = (y + z_inv).scale(0.5);
let z_new = (z + y_inv).scale(0.5);
let diff: f64 = (y_new - y).norm();
y = y_new;
z = z_new;
if diff < TOL {
break;
}
}
to_ndarray(&y)
}
}
impl approx::AbsDiffEq for Su3Algebra {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
1e-10
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.0
.iter()
.zip(other.0.iter())
.all(|(a, b)| (a - b).abs() < epsilon)
}
}
impl approx::RelativeEq for Su3Algebra {
fn default_max_relative() -> Self::Epsilon {
1e-10
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
self.0
.iter()
.zip(other.0.iter())
.all(|(a, b)| approx::RelativeEq::relative_eq(a, b, epsilon, max_relative))
}
}
impl fmt::Display for Su3Algebra {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "su(3)[")?;
for (i, c) in self.0.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{:.4}", c)?;
}
write!(f, "]")
}
}
impl fmt::Display for SU3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let dist = self.distance_to_identity();
write!(f, "SU(3)(d={:.4})", dist)
}
}
impl Mul<&SU3> for &SU3 {
type Output = SU3;
fn mul(self, rhs: &SU3) -> SU3 {
SU3 {
matrix: self.matrix.dot(&rhs.matrix),
}
}
}
impl Mul<&SU3> for SU3 {
type Output = SU3;
fn mul(self, rhs: &SU3) -> SU3 {
&self * rhs
}
}
impl MulAssign<&SU3> for SU3 {
fn mul_assign(&mut self, rhs: &SU3) {
self.matrix = self.matrix.dot(&rhs.matrix);
}
}
impl LieGroup for SU3 {
const MATRIX_DIM: usize = 3;
type Algebra = Su3Algebra;
fn identity() -> Self {
Self::identity()
}
fn compose(&self, other: &Self) -> Self {
Self {
matrix: self.matrix.dot(&other.matrix),
}
}
fn inverse(&self) -> Self {
Self::inverse(self)
}
fn conjugate_transpose(&self) -> Self {
Self::conjugate_transpose(self)
}
fn adjoint_action(&self, algebra_element: &Su3Algebra) -> Su3Algebra {
let x_matrix = algebra_element.to_matrix();
let g_x = self.matrix.dot(&x_matrix);
let g_adjoint_matrix = self.matrix.t().mapv(|z| z.conj());
let result = g_x.dot(&g_adjoint_matrix);
Su3Algebra::from_matrix(&result)
}
fn distance_to_identity(&self) -> f64 {
Self::distance_to_identity(self)
}
fn exp(tangent: &Su3Algebra) -> Self {
let x_matrix = tangent.to_matrix();
let norm: f64 = x_matrix
.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
let k = if norm > 0.5 {
(norm / 0.5).log2().ceil() as usize
} else {
0
};
let scale_factor = 1.0 / (1_u64 << k) as f64;
let scaled_matrix = x_matrix.mapv(|z| z * scale_factor);
let exp_scaled = SU3::exp_taylor(&scaled_matrix, 15);
let mut result = exp_scaled.matrix;
for _ in 0..k {
result = result.dot(&result);
result = Self::gram_schmidt_project(result);
}
Self { matrix: result }
}
fn log(&self) -> crate::error::LogResult<Su3Algebra> {
use crate::error::LogError;
let dist = self.distance_to_identity();
const MAX_DISTANCE: f64 = 2.0;
if dist > MAX_DISTANCE {
return Err(LogError::NotNearIdentity {
distance: dist,
threshold: MAX_DISTANCE,
});
}
if dist < 1e-14 {
return Ok(Su3Algebra::zero());
}
let mut current = self.matrix.clone();
let mut num_sqrts = 0;
const MAX_SQRTS: usize = 32; const TARGET_NORM: f64 = 0.5;
let identity: Array2<Complex64> = Array2::eye(3);
while num_sqrts < MAX_SQRTS {
let x_matrix = ¤t - &identity;
let x_norm: f64 = x_matrix
.iter()
.map(num_complex::Complex::norm_sqr)
.sum::<f64>()
.sqrt();
if x_norm < TARGET_NORM {
break;
}
current = Self::matrix_sqrt_db(¤t);
num_sqrts += 1;
}
let x_matrix = ¤t - &identity;
let mut log_matrix = x_matrix.clone();
let mut x_power = x_matrix.clone();
const N_TERMS: usize = 30;
for n in 2..=N_TERMS {
x_power = x_power.dot(&x_matrix);
let coefficient = (-1.0_f64).powi(n as i32 + 1) / n as f64;
log_matrix = log_matrix + x_power.mapv(|z| z * coefficient);
}
let scale_factor = (1_u64 << num_sqrts) as f64;
log_matrix = log_matrix.mapv(|z| z * scale_factor);
Ok(Su3Algebra::from_matrix(&log_matrix))
}
}
use crate::traits::{Compact, SemiSimple, Simple};
impl Compact for SU3 {}
impl Simple for SU3 {}
impl SemiSimple for SU3 {}
impl TracelessByConstruction for Su3Algebra {}
impl AntiHermitianByConstruction for Su3Algebra {}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_identity() {
let id = SU3::identity();
assert!(id.verify_unitarity(1e-10));
assert_relative_eq!(id.distance_to_identity(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_algebra_dimension() {
assert_eq!(Su3Algebra::DIM, 8);
}
#[test]
fn test_gell_mann_hermiticity() {
for j in 0..8 {
let lambda = Su3Algebra::gell_mann_matrix(j);
let adjoint = lambda.t().mapv(|z| z.conj());
for i in 0..3 {
for k in 0..3 {
assert_relative_eq!(lambda[[i, k]].re, adjoint[[i, k]].re, epsilon = 1e-10);
assert_relative_eq!(lambda[[i, k]].im, adjoint[[i, k]].im, epsilon = 1e-10);
}
}
}
}
#[test]
fn test_gell_mann_traceless() {
for j in 0..8 {
let lambda = Su3Algebra::gell_mann_matrix(j);
let trace = lambda[[0, 0]] + lambda[[1, 1]] + lambda[[2, 2]];
assert_relative_eq!(trace.re, 0.0, epsilon = 1e-10);
assert_relative_eq!(trace.im, 0.0, epsilon = 1e-10);
}
}
#[test]
fn test_matrix_roundtrip() {
let algebra = Su3Algebra([1.0, 2.0, 3.0, 0.5, -0.5, 1.5, -1.5, 0.3]);
let matrix = algebra.to_matrix();
let recovered = Su3Algebra::from_matrix(&matrix);
for i in 0..8 {
assert_relative_eq!(algebra.0[i], recovered.0[i], epsilon = 1e-10);
}
}
#[test]
fn test_inverse() {
use crate::traits::LieGroup;
let g = SU3::exp(&Su3Algebra([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]));
let g_inv = g.inverse();
let product = g.compose(&g_inv);
assert_relative_eq!(product.distance_to_identity(), 0.0, epsilon = 1e-6);
}
#[test]
fn test_adjoint_identity() {
use crate::traits::LieGroup;
let e = SU3::identity();
let x = Su3Algebra([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let result = e.adjoint_action(&x);
for i in 0..8 {
assert_relative_eq!(result.0[i], x.0[i], epsilon = 1e-10);
}
}
#[test]
fn test_structure_constants_bracket() {
use crate::traits::LieAlgebra;
let t1 = Su3Algebra::basis_element(0);
let t2 = Su3Algebra::basis_element(1);
let bracket = t1.bracket(&t2);
assert_relative_eq!(bracket.0[2], -1.0, epsilon = 1e-10);
for i in [0, 1, 3, 4, 5, 6, 7] {
assert_relative_eq!(bracket.0[i], 0.0, epsilon = 1e-10);
}
let bracket_reversed = t2.bracket(&t1);
for i in 0..8 {
assert_relative_eq!(bracket.0[i], -bracket_reversed.0[i], epsilon = 1e-10);
}
let t4 = Su3Algebra::basis_element(3);
let t5 = Su3Algebra::basis_element(4);
let bracket_45 = t4.bracket(&t5);
let expected_c8 = -(3.0_f64.sqrt() / 2.0);
assert_relative_eq!(bracket_45.0[7], expected_c8, epsilon = 1e-10);
}
#[test]
fn test_bracket_jacobi_identity() {
use crate::traits::LieAlgebra;
let x = Su3Algebra::basis_element(0);
let y = Su3Algebra::basis_element(3);
let z = Su3Algebra::basis_element(7);
let t1 = x.bracket(&y.bracket(&z));
let t2 = y.bracket(&z.bracket(&x));
let t3 = z.bracket(&x.bracket(&y));
let sum = t1.add(&t2).add(&t3);
assert!(
sum.norm() < 1e-10,
"Jacobi identity violated for SU(3): ||sum|| = {:.2e}",
sum.norm()
);
}
#[test]
fn test_bracket_bilinearity() {
use crate::traits::LieAlgebra;
let x = Su3Algebra::basis_element(0);
let y = Su3Algebra::basis_element(2);
let z = Su3Algebra::basis_element(5);
let alpha = 3.7;
let lhs = x.scale(alpha).add(&y).bracket(&z);
let rhs = x.bracket(&z).scale(alpha).add(&y.bracket(&z));
for i in 0..8 {
assert_relative_eq!(lhs.0[i], rhs.0[i], epsilon = 1e-12);
}
}
#[test]
fn test_exp_large_algebra_element() {
use crate::traits::LieGroup;
let large_algebra = Su3Algebra([2.0, 1.5, -1.8, 0.9, -1.2, 1.1, -0.8, 1.3]);
let norm = large_algebra.norm();
assert!(norm > 1.0, "Test requires ||X|| > 1, got {}", norm);
let g = SU3::exp(&large_algebra);
assert!(
g.verify_unitarity(1e-8),
"Unitarity violated for large algebra element"
);
assert!(g.distance_to_identity() > 0.1);
}
#[test]
fn test_exp_very_small_algebra_element() {
use crate::traits::LieGroup;
let small_algebra = Su3Algebra([1e-8, 2e-8, -1e-8, 3e-9, -2e-9, 1e-9, -5e-10, 2e-10]);
let g = SU3::exp(&small_algebra);
assert!(g.distance_to_identity() < 1e-7);
assert!(g.verify_unitarity(1e-12));
}
#[test]
fn test_exp_scaling_correctness() {
use crate::traits::LieGroup;
let algebra = Su3Algebra([0.5, 0.3, -0.4, 0.2, -0.3, 0.1, -0.2, 0.25]);
let exp_x = SU3::exp(&algebra);
let exp_2x = SU3::exp(&algebra.scale(2.0));
let exp_x_squared = exp_x.compose(&exp_x);
let distance = exp_2x.distance(&exp_x_squared);
assert!(
distance < 1e-6,
"exp(2X) should equal exp(X)^2, distance = {}",
distance
);
}
#[cfg(feature = "proptest")]
use proptest::prelude::*;
#[cfg(feature = "proptest")]
fn arb_su3() -> impl Strategy<Value = SU3> {
let range = -0.5_f64..0.5_f64;
(
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range,
)
.prop_map(|(a1, a2, a3, a4, a5, a6, a7, a8)| {
let algebra = Su3Algebra([a1, a2, a3, a4, a5, a6, a7, a8]);
SU3::exp(&algebra)
})
}
#[cfg(feature = "proptest")]
fn arb_su3_algebra() -> impl Strategy<Value = Su3Algebra> {
let range = -0.5_f64..0.5_f64;
(
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range.clone(),
range,
)
.prop_map(|(a1, a2, a3, a4, a5, a6, a7, a8)| {
Su3Algebra([a1, a2, a3, a4, a5, a6, a7, a8])
})
}
#[cfg(feature = "proptest")]
proptest! {
#[test]
fn prop_identity_axiom(u in arb_su3()) {
let e = SU3::identity();
let left = e.compose(&u);
prop_assert!(
left.distance(&u) < 1e-6,
"Left identity failed: I·U != U, distance = {}",
left.distance(&u)
);
let right = u.compose(&e);
prop_assert!(
right.distance(&u) < 1e-6,
"Right identity failed: U·I != U, distance = {}",
right.distance(&u)
);
}
#[test]
fn prop_inverse_axiom(u in arb_su3()) {
let u_inv = u.inverse();
let right_product = u.compose(&u_inv);
prop_assert!(
right_product.is_near_identity(1e-6),
"Right inverse failed: U·U† != I, distance = {}",
right_product.distance_to_identity()
);
let left_product = u_inv.compose(&u);
prop_assert!(
left_product.is_near_identity(1e-6),
"Left inverse failed: U†·U != I, distance = {}",
left_product.distance_to_identity()
);
}
#[test]
fn prop_associativity(u1 in arb_su3(), u2 in arb_su3(), u3 in arb_su3()) {
let left_assoc = u1.compose(&u2).compose(&u3);
let right_assoc = u1.compose(&u2.compose(&u3));
prop_assert!(
left_assoc.distance(&right_assoc) < 1e-6,
"Associativity failed: (U₁·U₂)·U₃ != U₁·(U₂·U₃), distance = {}",
left_assoc.distance(&right_assoc)
);
}
#[test]
fn prop_inverse_continuity(u in arb_su3()) {
let epsilon = 0.01;
let perturbation = SU3::exp(&Su3Algebra([epsilon, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
let u_perturbed = u.compose(&perturbation);
let inv_distance = u.inverse().distance(&u_perturbed.inverse());
prop_assert!(
inv_distance < 0.1,
"Inverse not continuous: small perturbation caused large inverse change, distance = {}",
inv_distance
);
}
#[test]
fn prop_unitarity_preserved(u1 in arb_su3(), u2 in arb_su3()) {
let product = u1.compose(&u2);
prop_assert!(
product.verify_unitarity(1e-10),
"Composition violated unitarity"
);
let inv = u1.inverse();
prop_assert!(
inv.verify_unitarity(1e-10),
"Inverse violated unitarity"
);
}
#[test]
fn prop_adjoint_homomorphism(
u1 in arb_su3(),
u2 in arb_su3(),
x in arb_su3_algebra()
) {
let u_composed = u1.compose(&u2);
let left = u_composed.adjoint_action(&x);
let ad_u2_x = u2.adjoint_action(&x);
let right = u1.adjoint_action(&ad_u2_x);
let diff = left.add(&right.scale(-1.0));
prop_assert!(
diff.norm() < 1e-6,
"Adjoint homomorphism failed: Ad_{{U₁∘U₂}}(X) != Ad_{{U₁}}(Ad_{{U₂}}(X)), diff norm = {}",
diff.norm()
);
}
#[test]
fn prop_adjoint_identity(x in arb_su3_algebra()) {
let e = SU3::identity();
let result = e.adjoint_action(&x);
let diff = result.add(&x.scale(-1.0));
prop_assert!(
diff.norm() < 1e-10,
"Identity action failed: Ad_I(X) != X, diff norm = {}",
diff.norm()
);
}
#[test]
fn prop_adjoint_bracket_preservation(
u in arb_su3(),
x in arb_su3_algebra(),
y in arb_su3_algebra()
) {
use crate::traits::LieAlgebra;
let bracket_xy = x.bracket(&y);
let left = u.adjoint_action(&bracket_xy);
let ad_x = u.adjoint_action(&x);
let ad_y = u.adjoint_action(&y);
let right = ad_x.bracket(&ad_y);
let diff = left.add(&right.scale(-1.0));
prop_assert!(
diff.norm() < 1e-5,
"Bracket preservation failed: Ad_U([X,Y]) != [Ad_U(X), Ad_U(Y)], diff norm = {}",
diff.norm()
);
}
#[test]
fn prop_adjoint_inverse(u in arb_su3(), x in arb_su3_algebra()) {
let ad_u_x = u.adjoint_action(&x);
let u_inv = u.inverse();
let result = u_inv.adjoint_action(&ad_u_x);
let diff = result.add(&x.scale(-1.0));
prop_assert!(
diff.norm() < 1e-6,
"Inverse property failed: Ad_{{U†}}(Ad_U(X)) != X, diff norm = {}",
diff.norm()
);
}
}
#[test]
fn test_exp_log_roundtrip() {
use crate::traits::{LieAlgebra, LieGroup};
use rand::SeedableRng;
use rand_distr::{Distribution, Uniform};
let mut rng = rand::rngs::StdRng::seed_from_u64(11111);
let dist = Uniform::new(-0.5, 0.5);
for _ in 0..50 {
let mut coeffs = [0.0; 8];
for coeff in &mut coeffs {
*coeff = dist.sample(&mut rng);
}
let x = Su3Algebra(coeffs);
let g = SU3::exp(&x);
let x_recovered = g.log().expect("log should succeed for exp output");
let diff = x.add(&x_recovered.scale(-1.0));
assert!(
diff.norm() < 1e-10,
"log(exp(X)) should equal X: ||diff|| = {:.2e}",
diff.norm()
);
}
}
#[test]
fn test_log_exp_roundtrip() {
use crate::traits::LieGroup;
use rand::SeedableRng;
use rand_distr::{Distribution, Uniform};
let mut rng = rand::rngs::StdRng::seed_from_u64(22222);
let dist = Uniform::new(-0.5, 0.5);
for _ in 0..50 {
let mut coeffs = [0.0; 8];
for coeff in &mut coeffs {
*coeff = dist.sample(&mut rng);
}
let g = SU3::exp(&Su3Algebra(coeffs));
let x = g.log().expect("log should succeed for valid SU(3) element");
let g_recovered = SU3::exp(&x);
let diff = g.compose(&g_recovered.inverse()).distance_to_identity();
assert!(
diff < 1e-10,
"exp(log(g)) should equal g: diff = {:.2e}",
diff
);
}
}
#[test]
fn test_su3_casimir_trivial() {
use crate::Casimir;
use crate::Su3Irrep;
let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::TRIVIAL);
assert_eq!(c2, 0.0, "Casimir of trivial representation should be 0");
}
#[test]
fn test_su3_casimir_fundamental() {
use crate::Casimir;
use crate::Su3Irrep;
let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::FUNDAMENTAL);
let expected = 4.0 / 3.0;
assert!(
(c2 - expected).abs() < 1e-10,
"Casimir of fundamental should be 4/3, got {}",
c2
);
}
#[test]
fn test_su3_casimir_antifundamental() {
use crate::Casimir;
use crate::Su3Irrep;
let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::ANTIFUNDAMENTAL);
let expected = 4.0 / 3.0;
assert!(
(c2 - expected).abs() < 1e-10,
"Casimir of antifundamental should be 4/3, got {}",
c2
);
}
#[test]
fn test_su3_casimir_adjoint() {
use crate::Casimir;
use crate::Su3Irrep;
let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::ADJOINT);
assert_eq!(c2, 3.0, "Casimir of adjoint representation should be 3");
}
#[test]
fn test_su3_casimir_symmetric() {
use crate::Casimir;
use crate::Su3Irrep;
let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&Su3Irrep::SYMMETRIC);
let expected = 10.0 / 3.0;
assert!(
(c2 - expected).abs() < 1e-10,
"Casimir of symmetric should be 10/3, got {}",
c2
);
}
#[test]
fn test_su3_casimir_formula() {
use crate::Casimir;
use crate::Su3Irrep;
for p in 0..5 {
for q in 0..5 {
let irrep = Su3Irrep::new(p, q);
let c2 = Su3Algebra::quadratic_casimir_eigenvalue(&irrep);
let pf = p as f64;
let qf = q as f64;
let expected = (pf * pf + qf * qf + pf * qf + 3.0 * pf + 3.0 * qf) / 3.0;
assert!(
(c2 - expected).abs() < 1e-10,
"Casimir for ({},{}) should be {}, got {}",
p,
q,
expected,
c2
);
}
}
}
#[test]
fn test_su3_rank() {
use crate::Casimir;
assert_eq!(Su3Algebra::rank(), 2, "SU(3) should have rank 2");
assert_eq!(
Su3Algebra::num_casimirs(),
2,
"SU(3) should have 2 Casimir operators"
);
}
#[test]
fn test_su3_cubic_casimir_trivial() {
use crate::Casimir;
use crate::Su3Irrep;
let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::TRIVIAL);
assert_eq!(c3_vec.len(), 1, "Should return exactly one higher Casimir");
assert_eq!(c3_vec[0], 0.0, "Cubic Casimir of trivial should be 0");
}
#[test]
fn test_su3_cubic_casimir_fundamental() {
use crate::Casimir;
use crate::Su3Irrep;
let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::FUNDAMENTAL);
let c3 = c3_vec[0];
let expected = 10.0 / 9.0;
assert!(
(c3 - expected).abs() < 1e-10,
"Cubic Casimir of fundamental should be 10/9, got {}",
c3
);
}
#[test]
fn test_su3_cubic_casimir_antifundamental() {
use crate::Casimir;
use crate::Su3Irrep;
let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::ANTIFUNDAMENTAL);
let c3 = c3_vec[0];
let expected = -10.0 / 9.0;
assert!(
(c3 - expected).abs() < 1e-10,
"Cubic Casimir of antifundamental should be -10/9, got {}",
c3
);
}
#[test]
fn test_su3_cubic_casimir_adjoint() {
use crate::Casimir;
use crate::Su3Irrep;
let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::ADJOINT);
let c3 = c3_vec[0];
assert!(
c3.abs() < 1e-10,
"Cubic Casimir of adjoint (self-conjugate) should be 0, got {}",
c3
);
}
#[test]
fn test_su3_cubic_casimir_symmetric() {
use crate::Casimir;
use crate::Su3Irrep;
let c3_vec = Su3Algebra::higher_casimir_eigenvalues(&Su3Irrep::SYMMETRIC);
let c3 = c3_vec[0];
let expected = 70.0 / 18.0;
assert!(
(c3 - expected).abs() < 1e-10,
"Cubic Casimir of symmetric should be 70/18, got {}",
c3
);
}
#[test]
fn test_su3_cubic_casimir_conjugation_symmetry() {
use crate::Casimir;
use crate::Su3Irrep;
for p in 0..5 {
for q in 0..5 {
let irrep_pq = Su3Irrep::new(p, q);
let irrep_qp = Su3Irrep::new(q, p);
let c3_pq = Su3Algebra::higher_casimir_eigenvalues(&irrep_pq)[0];
let c3_qp = Su3Algebra::higher_casimir_eigenvalues(&irrep_qp)[0];
assert!(
(c3_pq + c3_qp).abs() < 1e-10,
"c₃({},{}) = {} should equal -c₃({},{}) = {}",
p,
q,
c3_pq,
q,
p,
-c3_qp
);
}
}
}
#[test]
fn test_su3_cubic_casimir_formula() {
use crate::Casimir;
use crate::Su3Irrep;
for p in 0..5 {
for q in 0..5 {
let irrep = Su3Irrep::new(p, q);
let c3 = Su3Algebra::higher_casimir_eigenvalues(&irrep)[0];
let pf = p as f64;
let qf = q as f64;
let expected = (pf - qf) * (2.0 * pf + qf + 3.0) * (pf + 2.0 * qf + 3.0) / 18.0;
assert!(
(c3 - expected).abs() < 1e-10,
"Cubic Casimir for ({},{}) should be {}, got {}",
p,
q,
expected,
c3
);
}
}
}
#[test]
fn test_gram_schmidt_small_matrix() {
let small_algebra = Su3Algebra([1e-6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let small_element = SU3::exp(&small_algebra);
assert!(
small_element.verify_unitarity(1e-10),
"Small element should be unitary"
);
let recovered = small_element
.log()
.expect("log should succeed for near-identity");
let diff = (recovered.0[0] - small_algebra.0[0]).abs();
assert!(
diff < 1e-10,
"exp/log round-trip failed for small element: diff = {}",
diff
);
}
#[test]
fn test_gram_schmidt_large_matrix() {
use crate::traits::LieGroup;
let large_algebra = Su3Algebra([2.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let large_element = SU3::exp(&large_algebra);
assert!(
large_element.verify_unitarity(1e-8),
"Large rotation element should be unitary, distance = {}",
large_element.distance_to_identity()
);
}
#[test]
fn test_exp_repeated_squaring_stability() {
use crate::traits::LieGroup;
let algebra = Su3Algebra([1.0, 1.0, 0.5, 0.3, 0.2, 0.1, 0.0, 0.0]);
let element = SU3::exp(&algebra);
assert!(
element.verify_unitarity(1e-10),
"Repeated squaring should preserve unitarity"
);
let u_dag_u = element.conjugate_transpose().compose(&element);
let trace = u_dag_u.trace();
assert!(
(trace.re - 3.0).abs() < 1e-10 && trace.im.abs() < 1e-10,
"U†U should have trace 3, got {:.6}+{:.6}i",
trace.re,
trace.im
);
}
#[test]
fn test_exp_log_near_boundary() {
use crate::traits::LieGroup;
let theta = std::f64::consts::PI - 0.1;
let algebra = Su3Algebra([theta, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
let element = SU3::exp(&algebra);
assert!(
element.verify_unitarity(1e-10),
"Near-boundary element should be unitary"
);
if let Ok(recovered) = element.log() {
let round_trip = SU3::exp(&recovered);
let distance = element.distance(&round_trip);
assert!(
distance < 1e-6,
"Round trip should be close, distance = {}",
distance
);
}
}
#[test]
fn test_composition_stability() {
use crate::traits::LieGroup;
let u1 = SU3::exp(&Su3Algebra([0.5, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]));
let u2 = SU3::exp(&Su3Algebra([0.0, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0]));
let u3 = SU3::exp(&Su3Algebra([0.0, 0.0, 0.3, 0.5, 0.0, 0.0, 0.0, 0.0]));
let mut result = SU3::identity();
for _ in 0..100 {
result = result.compose(&u1);
result = result.compose(&u2);
result = result.compose(&u3);
}
assert!(
result.verify_unitarity(1e-8),
"100 compositions should still be unitary"
);
}
}