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};
const UNITARITY_VIOLATION_THRESHOLD: f64 = 1e-10;
const SMALL_ANGLE_EXP_THRESHOLD: f64 = 1e-10;
const SIN_HALF_THETA_THRESHOLD: f64 = 1e-10;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Su2Algebra(pub(crate) [f64; 3]);
impl Su2Algebra {
#[must_use]
pub fn new(components: [f64; 3]) -> Self {
Self(components)
}
#[must_use]
pub fn components(&self) -> &[f64; 3] {
&self.0
}
}
impl Add for Su2Algebra {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self([
self.0[0] + rhs.0[0],
self.0[1] + rhs.0[1],
self.0[2] + rhs.0[2],
])
}
}
impl Add<&Su2Algebra> for Su2Algebra {
type Output = Su2Algebra;
fn add(self, rhs: &Su2Algebra) -> Su2Algebra {
Self([
self.0[0] + rhs.0[0],
self.0[1] + rhs.0[1],
self.0[2] + rhs.0[2],
])
}
}
impl Add<Su2Algebra> for &Su2Algebra {
type Output = Su2Algebra;
fn add(self, rhs: Su2Algebra) -> Su2Algebra {
Su2Algebra([
self.0[0] + rhs.0[0],
self.0[1] + rhs.0[1],
self.0[2] + rhs.0[2],
])
}
}
impl Add<&Su2Algebra> for &Su2Algebra {
type Output = Su2Algebra;
fn add(self, rhs: &Su2Algebra) -> Su2Algebra {
Su2Algebra([
self.0[0] + rhs.0[0],
self.0[1] + rhs.0[1],
self.0[2] + rhs.0[2],
])
}
}
impl Sub for Su2Algebra {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self([
self.0[0] - rhs.0[0],
self.0[1] - rhs.0[1],
self.0[2] - rhs.0[2],
])
}
}
impl Neg for Su2Algebra {
type Output = Self;
fn neg(self) -> Self {
Self([-self.0[0], -self.0[1], -self.0[2]])
}
}
impl Mul<f64> for Su2Algebra {
type Output = Self;
fn mul(self, scalar: f64) -> Self {
Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
}
}
impl Mul<Su2Algebra> for f64 {
type Output = Su2Algebra;
fn mul(self, rhs: Su2Algebra) -> Su2Algebra {
rhs * self
}
}
impl LieAlgebra for Su2Algebra {
const DIM: usize = 3;
#[inline]
fn zero() -> Self {
Self([0.0, 0.0, 0.0])
}
#[inline]
fn add(&self, other: &Self) -> Self {
Self([
self.0[0] + other.0[0],
self.0[1] + other.0[1],
self.0[2] + other.0[2],
])
}
#[inline]
fn scale(&self, scalar: f64) -> Self {
Self([self.0[0] * scalar, self.0[1] * scalar, self.0[2] * scalar])
}
#[inline]
fn norm(&self) -> f64 {
(self.0[0].powi(2) + self.0[1].powi(2) + self.0[2].powi(2)).sqrt()
}
#[inline]
fn basis_element(i: usize) -> Self {
assert!(i < 3, "SU(2) algebra is 3-dimensional");
let mut v = [0.0; 3];
v[i] = 1.0;
Self(v)
}
#[inline]
fn from_components(components: &[f64]) -> Self {
assert_eq!(components.len(), 3, "su(2) has dimension 3");
Self([components[0], components[1], components[2]])
}
#[inline]
fn to_components(&self) -> Vec<f64> {
self.0.to_vec()
}
#[inline]
fn bracket(&self, other: &Self) -> Self {
let x = self.0;
let y = other.0;
Self([
-(x[1] * y[2] - x[2] * y[1]),
-(x[2] * y[0] - x[0] * y[2]),
-(x[0] * y[1] - x[1] * y[0]),
])
}
#[inline]
fn inner(&self, other: &Self) -> f64 {
self.0[0] * other.0[0] + self.0[1] * other.0[1] + self.0[2] * other.0[2]
}
}
impl crate::Casimir for Su2Algebra {
type Representation = crate::representation::Spin;
#[inline]
fn quadratic_casimir_eigenvalue(irrep: &Self::Representation) -> f64 {
let j = irrep.value();
j * (j + 1.0)
}
#[inline]
fn rank() -> usize {
1 }
}
#[derive(Debug, Clone, PartialEq)]
pub struct SU2 {
pub(crate) matrix: Array2<Complex64>,
}
impl SU2 {
#[must_use]
pub fn matrix(&self) -> &Array2<Complex64> {
&self.matrix
}
#[must_use]
pub fn identity() -> Self {
Self {
matrix: Array2::eye(2),
}
}
#[must_use]
pub fn pauli_x() -> Array2<Complex64> {
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 1]] = Complex64::new(0.5, 0.0);
matrix[[1, 0]] = Complex64::new(0.5, 0.0);
matrix
}
#[must_use]
pub fn pauli_y() -> Array2<Complex64> {
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 1]] = Complex64::new(0.0, -0.5);
matrix[[1, 0]] = Complex64::new(0.0, 0.5);
matrix
}
#[must_use]
pub fn pauli_z() -> Array2<Complex64> {
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = Complex64::new(0.5, 0.0);
matrix[[1, 1]] = Complex64::new(-0.5, 0.0);
matrix
}
#[inline]
#[must_use]
pub fn rotation_x(theta: f64) -> Self {
let (s, c) = (theta / 2.0).sin_cos();
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = Complex64::new(c, 0.0);
matrix[[0, 1]] = Complex64::new(0.0, s);
matrix[[1, 0]] = Complex64::new(0.0, s);
matrix[[1, 1]] = Complex64::new(c, 0.0);
Self { matrix }
}
#[inline]
#[must_use]
pub fn rotation_y(theta: f64) -> Self {
let (s, c) = (theta / 2.0).sin_cos();
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = Complex64::new(c, 0.0);
matrix[[0, 1]] = Complex64::new(s, 0.0);
matrix[[1, 0]] = Complex64::new(-s, 0.0);
matrix[[1, 1]] = Complex64::new(c, 0.0);
Self { matrix }
}
#[inline]
#[must_use]
pub fn rotation_z(theta: f64) -> Self {
let (s, c) = (theta / 2.0).sin_cos();
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = Complex64::new(c, s);
matrix[[0, 1]] = Complex64::new(0.0, 0.0);
matrix[[1, 0]] = Complex64::new(0.0, 0.0);
matrix[[1, 1]] = Complex64::new(c, -s);
Self { matrix }
}
#[cfg(feature = "rand")]
#[must_use]
pub fn random_haar<R: rand::Rng>(rng: &mut R) -> Self {
use rand_distr::{Distribution, StandardNormal};
const MIN_NORM: f64 = 1e-10;
loop {
let a: f64 = StandardNormal.sample(rng);
let b: f64 = StandardNormal.sample(rng);
let c: f64 = StandardNormal.sample(rng);
let d: f64 = StandardNormal.sample(rng);
let norm = (a * a + b * b + c * c + d * d).sqrt();
if norm < MIN_NORM {
continue;
}
let a = a / norm;
let b = b / norm;
let c = c / norm;
let d = d / norm;
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = Complex64::new(a, b);
matrix[[0, 1]] = Complex64::new(c, d);
matrix[[1, 0]] = Complex64::new(-c, d);
matrix[[1, 1]] = Complex64::new(a, -b);
return Self { matrix };
}
}
#[must_use]
pub fn inverse(&self) -> Self {
let matrix = self.matrix.t().mapv(|z| z.conj());
Self { matrix }
}
#[must_use]
pub fn conjugate_transpose(&self) -> Self {
self.inverse()
}
#[inline]
#[must_use]
pub fn act_on_vector(&self, v: &[Complex64; 2]) -> [Complex64; 2] {
let m = &self.matrix;
[
m[[0, 0]] * v[0] + m[[0, 1]] * v[1],
m[[1, 0]] * v[0] + m[[1, 1]] * v[1],
]
}
#[must_use]
pub fn trace(&self) -> Complex64 {
self.matrix[[0, 0]] + self.matrix[[1, 1]]
}
#[must_use]
pub fn distance_to_identity(&self) -> f64 {
let trace_val = self.trace();
let raw_cos_half = trace_val.re / 2.0;
debug_assert!(
raw_cos_half.abs() <= 1.0 + UNITARITY_VIOLATION_THRESHOLD,
"SU(2) unitarity violation: |cos(θ/2)| = {:.10} > 1 + ε. \
Matrix has drifted from the group manifold.",
raw_cos_half.abs()
);
let cos_half_angle = raw_cos_half.clamp(-1.0, 1.0);
2.0 * cos_half_angle.acos()
}
#[must_use]
pub fn angle_and_axis(&self) -> (f64, [f64; 3]) {
let cos_half = self.matrix[[0, 0]].re.clamp(-1.0, 1.0);
let angle = 2.0 * cos_half.acos();
if angle < 1e-10 {
return (angle, [0.0, 0.0, 1.0]);
}
let sin_half = (angle / 2.0).sin();
if sin_half.abs() < 1e-10 {
return (angle, [0.0, 0.0, 1.0]);
}
let n_z = self.matrix[[0, 0]].im / sin_half;
let n_x = self.matrix[[0, 1]].im / sin_half;
let n_y = self.matrix[[0, 1]].re / sin_half;
let norm = (n_x * n_x + n_y * n_y + n_z * n_z).sqrt();
if norm < 1e-10 {
return (angle, [0.0, 0.0, 1.0]);
}
(angle, [n_x / norm, n_y / norm, n_z / norm])
}
#[must_use]
pub fn verify_unitarity(&self, tolerance: f64) -> bool {
let u_dagger = self.matrix.t().mapv(|z: Complex64| z.conj());
let product = u_dagger.dot(&self.matrix);
let identity = Array2::eye(2);
let diff_norm: f64 = product
.iter()
.zip(identity.iter())
.map(|(a, b): (&Complex64, &Complex64)| (a - b).norm_sqr())
.sum::<f64>()
.sqrt();
if diff_norm >= tolerance {
return false;
}
let det =
self.matrix[[0, 0]] * self.matrix[[1, 1]] - self.matrix[[0, 1]] * self.matrix[[1, 0]];
(det - Complex64::new(1.0, 0.0)).norm() < tolerance
}
#[must_use]
pub fn to_matrix(&self) -> [[Complex64; 2]; 2] {
[
[self.matrix[[0, 0]], self.matrix[[0, 1]]],
[self.matrix[[1, 0]], self.matrix[[1, 1]]],
]
}
#[must_use]
pub fn from_matrix(arr: [[Complex64; 2]; 2]) -> Self {
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = arr[0][0];
matrix[[0, 1]] = arr[0][1];
matrix[[1, 0]] = arr[1][0];
matrix[[1, 1]] = arr[1][1];
Self { matrix }
}
pub fn log_stable(&self) -> crate::error::LogResult<Su2Algebra> {
use crate::error::LogError;
let sin_nx = self.matrix[[0, 1]].im;
let sin_ny = self.matrix[[0, 1]].re;
let sin_nz = self.matrix[[0, 0]].im;
let sin_half_theta = (sin_nx * sin_nx + sin_ny * sin_ny + sin_nz * sin_nz).sqrt();
let raw_cos_half = self.matrix[[0, 0]].re;
if (raw_cos_half.abs() - 1.0).max(0.0) > UNITARITY_VIOLATION_THRESHOLD
&& sin_half_theta < UNITARITY_VIOLATION_THRESHOLD
{
let norm_check = raw_cos_half * raw_cos_half + sin_half_theta * sin_half_theta;
if (norm_check - 1.0).abs() > 1e-6 {
return Err(LogError::NumericalInstability {
reason: format!(
"SU(2) unitarity violation: cos²(θ/2) + sin²(θ/2) = {:.10} ≠ 1",
norm_check
),
});
}
}
let half_theta = sin_half_theta.atan2(raw_cos_half);
let theta = 2.0 * half_theta;
if sin_half_theta < 1e-15 {
return Ok(Su2Algebra::zero());
}
let scale = theta / sin_half_theta;
Ok(Su2Algebra([scale * sin_nx, scale * sin_ny, scale * sin_nz]))
}
pub fn log_with_condition(&self) -> crate::error::ConditionedLogResult<Su2Algebra> {
use crate::error::{LogCondition, LogError};
let sin_nx = self.matrix[[0, 1]].im;
let sin_ny = self.matrix[[0, 1]].re;
let sin_nz = self.matrix[[0, 0]].im;
let sin_half_theta = (sin_nx * sin_nx + sin_ny * sin_ny + sin_nz * sin_nz).sqrt();
let raw_cos_half = self.matrix[[0, 0]].re;
let norm_check = raw_cos_half * raw_cos_half + sin_half_theta * sin_half_theta;
if (norm_check - 1.0).abs() > 1e-6 {
return Err(LogError::NumericalInstability {
reason: format!(
"SU(2) unitarity violation: cos²(θ/2) + sin²(θ/2) = {:.10} ≠ 1",
norm_check
),
});
}
let half_theta = sin_half_theta.atan2(raw_cos_half);
let theta = 2.0 * half_theta;
let condition = LogCondition::from_angle(theta);
if sin_half_theta < 1e-15 {
return Ok((Su2Algebra::zero(), condition));
}
let scale = theta / sin_half_theta;
let result = Su2Algebra([scale * sin_nx, scale * sin_ny, scale * sin_nz]);
Ok((result, condition))
}
}
impl Mul<&SU2> for &SU2 {
type Output = SU2;
fn mul(self, rhs: &SU2) -> SU2 {
SU2 {
matrix: self.matrix.dot(&rhs.matrix),
}
}
}
impl Mul<&SU2> for SU2 {
type Output = SU2;
fn mul(self, rhs: &SU2) -> SU2 {
&self * rhs
}
}
impl MulAssign<&SU2> for SU2 {
fn mul_assign(&mut self, rhs: &SU2) {
self.matrix = self.matrix.dot(&rhs.matrix);
}
}
impl approx::AbsDiffEq for Su2Algebra {
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 Su2Algebra {
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 Su2Algebra {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"su(2)[{:.4}, {:.4}, {:.4}]",
self.0[0], self.0[1], self.0[2]
)
}
}
impl fmt::Display for SU2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let dist = self.distance_to_identity();
write!(f, "SU(2)(θ={:.4})", dist)
}
}
impl LieGroup for SU2 {
const MATRIX_DIM: usize = 2;
type Algebra = Su2Algebra;
fn identity() -> Self {
Self::identity() }
fn compose(&self, other: &Self) -> Self {
self * other
}
fn inverse(&self) -> Self {
Self::inverse(self) }
fn conjugate_transpose(&self) -> Self {
Self::conjugate_transpose(self) }
fn distance_to_identity(&self) -> f64 {
Self::distance_to_identity(self) }
fn exp(tangent: &Su2Algebra) -> Self {
let angle = tangent.norm();
if angle < SMALL_ANGLE_EXP_THRESHOLD {
return Self::identity();
}
let axis = tangent.scale(1.0 / angle);
let (s, c) = (angle / 2.0).sin_cos();
let mut matrix = Array2::zeros((2, 2));
matrix[[0, 0]] = Complex64::new(c, s * axis.0[2]);
matrix[[0, 1]] = Complex64::new(s * axis.0[1], s * axis.0[0]);
matrix[[1, 0]] = Complex64::new(-s * axis.0[1], s * axis.0[0]);
matrix[[1, 1]] = Complex64::new(c, -s * axis.0[2]);
Self { matrix }
}
fn log(&self) -> crate::error::LogResult<Su2Algebra> {
use crate::error::LogError;
let tr = self.trace();
let raw_cos_half = tr.re / 2.0;
if raw_cos_half.abs() > 1.0 + UNITARITY_VIOLATION_THRESHOLD {
return Err(LogError::NumericalInstability {
reason: format!(
"SU(2) unitarity violation: |cos(θ/2)| = {:.10} > 1 + ε. \
Matrix has drifted from the group manifold.",
raw_cos_half.abs()
),
});
}
let cos_half_theta = raw_cos_half.clamp(-1.0, 1.0);
let half_theta = cos_half_theta.acos();
let theta = 2.0 * half_theta;
const SMALL_ANGLE_THRESHOLD: f64 = 1e-10;
if theta.abs() < SMALL_ANGLE_THRESHOLD {
return Ok(Su2Algebra::zero());
}
let sin_half_theta = (half_theta).sin();
if sin_half_theta.abs() < SIN_HALF_THETA_THRESHOLD {
return Err(LogError::NumericalInstability {
reason: "sin(θ/2) too small for reliable axis extraction".to_string(),
});
}
let nx = self.matrix[[0, 1]].im / sin_half_theta;
let ny = self.matrix[[0, 1]].re / sin_half_theta;
let nz = self.matrix[[0, 0]].im / sin_half_theta;
Ok(Su2Algebra([theta * nx, theta * ny, theta * nz]))
}
fn adjoint_action(&self, algebra_element: &Su2Algebra) -> Su2Algebra {
let [a, b, c] = algebra_element.0;
let i = Complex64::new(0.0, 1.0);
let mut x_matrix = Array2::zeros((2, 2));
x_matrix[[0, 0]] = i * c; x_matrix[[0, 1]] = Complex64::new(b, a); x_matrix[[1, 0]] = Complex64::new(-b, a); x_matrix[[1, 1]] = -i * c;
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);
let a_prime = result[[0, 1]].im; let b_prime = result[[0, 1]].re; let c_prime = result[[0, 0]].im;
Su2Algebra([a_prime, b_prime, c_prime])
}
}
use crate::traits::{Compact, SemiSimple, Simple};
impl Compact for SU2 {}
impl Simple for SU2 {}
impl SemiSimple for SU2 {}
impl TracelessByConstruction for Su2Algebra {}
impl AntiHermitianByConstruction for Su2Algebra {}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_identity() {
let id = SU2::identity();
assert!(id.verify_unitarity(1e-10));
assert_relative_eq!(id.distance_to_identity(), 0.0, epsilon = 1e-10);
}
#[test]
fn test_rotation_unitarity() {
let u = SU2::rotation_x(0.5);
assert!(u.verify_unitarity(1e-10));
}
#[test]
fn test_inverse() {
let u = SU2::rotation_y(1.2);
let u_inv = u.inverse();
let product = &u * &u_inv;
assert_relative_eq!(product.distance_to_identity(), 0.0, epsilon = 1e-8);
}
#[test]
fn test_group_multiplication() {
let u1 = SU2::rotation_x(0.3);
let u2 = SU2::rotation_y(0.7);
let product = &u1 * &u2;
assert!(product.verify_unitarity(1e-10));
}
#[test]
fn test_action_on_vector() {
let id = SU2::identity();
let v = [Complex64::new(1.0, 0.0), Complex64::new(0.0, 1.0)];
let result = id.act_on_vector(&v);
assert_relative_eq!(result[0].re, v[0].re, epsilon = 1e-10);
assert_relative_eq!(result[1].im, v[1].im, epsilon = 1e-10);
}
#[test]
fn test_rotation_preserves_norm() {
let u = SU2::rotation_z(2.5);
let v = [Complex64::new(3.0, 4.0), Complex64::new(1.0, 2.0)];
let norm_before = v[0].norm_sqr() + v[1].norm_sqr();
let rotated = u.act_on_vector(&v);
let norm_after = rotated[0].norm_sqr() + rotated[1].norm_sqr();
assert_relative_eq!(norm_before, norm_after, epsilon = 1e-10);
}
#[test]
fn test_lie_group_identity() {
use crate::traits::LieGroup;
let g = SU2::rotation_x(1.5);
let e = SU2::identity();
let g_times_e = g.compose(&e);
assert_relative_eq!(g_times_e.distance(&g), 0.0, epsilon = 1e-10);
let e_times_g = e.compose(&g);
assert_relative_eq!(e_times_g.distance(&g), 0.0, epsilon = 1e-10);
}
#[test]
fn test_lie_group_inverse() {
use crate::traits::LieGroup;
let g = SU2::rotation_y(2.3);
let g_inv = g.inverse();
let right_product = g.compose(&g_inv);
assert!(
right_product.is_near_identity(1e-7),
"Right inverse failed: distance = {}",
right_product.distance_to_identity()
);
let left_product = g_inv.compose(&g);
assert!(
left_product.is_near_identity(1e-7),
"Left inverse failed: distance = {}",
left_product.distance_to_identity()
);
}
#[test]
fn test_lie_group_associativity() {
use crate::traits::LieGroup;
let g1 = SU2::rotation_x(0.5);
let g2 = SU2::rotation_y(0.8);
let g3 = SU2::rotation_z(1.2);
let left_assoc = g1.compose(&g2).compose(&g3);
let right_assoc = g1.compose(&g2.compose(&g3));
assert_relative_eq!(left_assoc.distance(&right_assoc), 0.0, epsilon = 1e-10);
}
#[test]
fn test_lie_group_distance_symmetry() {
let g = SU2::rotation_x(1.8);
let g_inv = g.inverse();
let d1 = g.distance_to_identity();
let d2 = g_inv.distance_to_identity();
assert_relative_eq!(d1, d2, epsilon = 1e-10);
}
#[test]
fn test_lie_group_is_near_identity() {
use crate::traits::LieGroup;
let e = SU2::identity();
assert!(
e.is_near_identity(1e-10),
"Identity should be near identity"
);
let almost_e = SU2::rotation_x(1e-12);
assert!(
almost_e.is_near_identity(1e-10),
"Small rotation should be near identity"
);
let g = SU2::rotation_y(0.5);
assert!(
!g.is_near_identity(1e-10),
"Large rotation should not be near identity"
);
}
#[test]
fn test_lie_group_generic_algorithm() {
use crate::traits::LieGroup;
fn parallel_transport<G: LieGroup>(path: &[G]) -> G {
path.iter().fold(G::identity(), |acc, g| acc.compose(g))
}
let path = vec![
SU2::rotation_x(0.1),
SU2::rotation_y(0.2),
SU2::rotation_z(0.3),
];
let holonomy = parallel_transport(&path);
assert!(holonomy.verify_unitarity(1e-10));
assert!(holonomy.distance_to_identity() > 0.1);
}
#[cfg(feature = "proptest")]
use proptest::prelude::*;
#[cfg(feature = "proptest")]
fn arb_su2() -> impl Strategy<Value = SU2> {
use std::f64::consts::TAU;
let alpha = 0.0..TAU;
let beta = 0.0..TAU;
let gamma = 0.0..TAU;
(alpha, beta, gamma).prop_map(|(a, b, c)| {
SU2::rotation_z(a)
.compose(&SU2::rotation_y(b))
.compose(&SU2::rotation_x(c))
})
}
#[cfg(feature = "proptest")]
proptest! {
#[test]
fn prop_identity_axiom(g in arb_su2()) {
let e = SU2::identity();
let left = e.compose(&g);
prop_assert!(
left.distance(&g) < 1e-7,
"Left identity failed: e·g != g, distance = {}",
left.distance(&g)
);
let right = g.compose(&e);
prop_assert!(
right.distance(&g) < 1e-7,
"Right identity failed: g·e != g, distance = {}",
right.distance(&g)
);
}
#[test]
fn prop_inverse_axiom(g in arb_su2()) {
let g_inv = g.inverse();
let right_product = g.compose(&g_inv);
prop_assert!(
right_product.is_near_identity(1e-7),
"Right inverse failed: g·g⁻¹ != e, distance = {}",
right_product.distance_to_identity()
);
let left_product = g_inv.compose(&g);
prop_assert!(
left_product.is_near_identity(1e-7),
"Left inverse failed: g⁻¹·g != e, distance = {}",
left_product.distance_to_identity()
);
}
#[test]
fn prop_associativity(g1 in arb_su2(), g2 in arb_su2(), g3 in arb_su2()) {
let left_assoc = g1.compose(&g2).compose(&g3);
let right_assoc = g1.compose(&g2.compose(&g3));
prop_assert!(
left_assoc.distance(&right_assoc) < 1e-7,
"Associativity failed: (g₁·g₂)·g₃ != g₁·(g₂·g₃), distance = {}",
left_assoc.distance(&right_assoc)
);
}
#[test]
fn prop_inverse_continuity(g in arb_su2()) {
let epsilon = 0.01;
let perturbation = SU2::rotation_x(epsilon);
let g_perturbed = g.compose(&perturbation);
let inv_distance = g.inverse().distance(&g_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(g1 in arb_su2(), g2 in arb_su2()) {
let product = g1.compose(&g2);
prop_assert!(
product.verify_unitarity(1e-10),
"Composition violated unitarity"
);
let inv = g1.inverse();
prop_assert!(
inv.verify_unitarity(1e-10),
"Inverse violated unitarity"
);
}
#[test]
fn prop_adjoint_homomorphism(
g1 in arb_su2(),
g2 in arb_su2(),
x in arb_su2_algebra()
) {
let g_composed = g1.compose(&g2);
let left = g_composed.adjoint_action(&x);
let ad_g2_x = g2.adjoint_action(&x);
let right = g1.adjoint_action(&ad_g2_x);
let diff = left.add(&right.scale(-1.0));
prop_assert!(
diff.norm() < 1e-7,
"Adjoint homomorphism failed: Ad_{{g₁∘g₂}}(X) != Ad_{{g₁}}(Ad_{{g₂}}(X)), diff norm = {}",
diff.norm()
);
}
#[test]
fn prop_adjoint_identity(x in arb_su2_algebra()) {
let e = SU2::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_e(X) != X, diff norm = {}",
diff.norm()
);
}
#[test]
fn prop_adjoint_bracket_preservation(
g in arb_su2(),
x in arb_su2_algebra(),
y in arb_su2_algebra()
) {
use crate::traits::LieAlgebra;
let bracket_xy = x.bracket(&y);
let left = g.adjoint_action(&bracket_xy);
let ad_x = g.adjoint_action(&x);
let ad_y = g.adjoint_action(&y);
let right = ad_x.bracket(&ad_y);
let diff = left.add(&right.scale(-1.0));
prop_assert!(
diff.norm() < 1e-6,
"Bracket preservation failed: Ad_g([X,Y]) != [Ad_g(X), Ad_g(Y)], diff norm = {}",
diff.norm()
);
}
#[test]
fn prop_jacobi_identity(
x in arb_su2_algebra(),
y in arb_su2_algebra(),
z in arb_su2_algebra()
) {
use crate::traits::LieAlgebra;
let yz = y.bracket(&z);
let term1 = x.bracket(&yz);
let zx = z.bracket(&x);
let term2 = y.bracket(&zx);
let xy = x.bracket(&y);
let term3 = z.bracket(&xy);
let sum = term1.add(&term2).add(&term3);
prop_assert!(
sum.norm() < 1e-10,
"Jacobi identity failed: ||[X,[Y,Z]] + [Y,[Z,X]] + [Z,[X,Y]]|| = {:.2e}",
sum.norm()
);
}
#[test]
fn prop_adjoint_inverse(g in arb_su2(), x in arb_su2_algebra()) {
let ad_g_x = g.adjoint_action(&x);
let g_inv = g.inverse();
let result = g_inv.adjoint_action(&ad_g_x);
let diff = result.add(&x.scale(-1.0));
prop_assert!(
diff.norm() < 1e-7,
"Inverse property failed: Ad_{{g⁻¹}}(Ad_g(X)) != X, diff norm = {}",
diff.norm()
);
}
}
#[cfg(feature = "proptest")]
fn arb_su2_algebra() -> impl Strategy<Value = Su2Algebra> {
use proptest::prelude::*;
use std::f64::consts::PI;
((-PI..PI), (-PI..PI), (-PI..PI)).prop_map(|(a, b, c)| Su2Algebra([a, b, c]))
}
#[test]
#[cfg(feature = "rand")]
fn test_random_haar_unitarity() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
for _ in 0..100 {
let g = SU2::random_haar(&mut rng);
assert!(
g.verify_unitarity(1e-10),
"Random Haar element should be unitary"
);
}
}
#[test]
fn test_bracket_bilinearity() {
use crate::traits::LieAlgebra;
let x = Su2Algebra([1.0, 0.0, 0.0]);
let y = Su2Algebra([0.0, 1.0, 0.0]);
let z = Su2Algebra([0.0, 0.0, 1.0]);
let alpha = 2.5;
let lhs = x.scale(alpha).add(&y).bracket(&z);
let rhs = x.bracket(&z).scale(alpha).add(&y.bracket(&z));
for i in 0..3 {
assert!(
(lhs.0[i] - rhs.0[i]).abs() < 1e-14,
"Left bilinearity failed at component {}: {} vs {}",
i,
lhs.0[i],
rhs.0[i]
);
}
let lhs = z.bracket(&x.scale(alpha).add(&y));
let rhs = z.bracket(&x).scale(alpha).add(&z.bracket(&y));
for i in 0..3 {
assert!(
(lhs.0[i] - rhs.0[i]).abs() < 1e-14,
"Right bilinearity failed at component {}: {} vs {}",
i,
lhs.0[i],
rhs.0[i]
);
}
}
#[test]
fn test_jacobi_identity_random() {
use crate::traits::LieAlgebra;
use rand::SeedableRng;
use rand_distr::{Distribution, StandardNormal};
let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
for _ in 0..100 {
let x = Su2Algebra([
StandardNormal.sample(&mut rng),
StandardNormal.sample(&mut rng),
StandardNormal.sample(&mut rng),
]);
let y = Su2Algebra([
StandardNormal.sample(&mut rng),
StandardNormal.sample(&mut rng),
StandardNormal.sample(&mut rng),
]);
let z = Su2Algebra([
StandardNormal.sample(&mut rng),
StandardNormal.sample(&mut rng),
StandardNormal.sample(&mut rng),
]);
let yz = y.bracket(&z);
let term1 = x.bracket(&yz);
let zx = z.bracket(&x);
let term2 = y.bracket(&zx);
let xy = x.bracket(&y);
let term3 = z.bracket(&xy);
let sum = term1.add(&term2).add(&term3);
assert!(
sum.norm() < 1e-10,
"Jacobi identity failed: ||[X,[Y,Z]] + [Y,[Z,X]] + [Z,[X,Y]]|| = {:.2e}",
sum.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(54321);
let dist = Uniform::new(-2.0, 2.0);
for _ in 0..100 {
let x = Su2Algebra([
dist.sample(&mut rng),
dist.sample(&mut rng),
dist.sample(&mut rng),
]);
let g = SU2::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]
#[cfg(feature = "rand")]
fn test_log_exp_roundtrip() {
use crate::traits::LieGroup;
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(98765);
for _ in 0..100 {
let g = SU2::random_haar(&mut rng);
let x = g.log().expect("log should succeed for valid SU(2) element");
let g_recovered = SU2::exp(&x);
let diff = g.compose(&g_recovered.inverse()).distance_to_identity();
assert!(
diff < 1e-7,
"exp(log(g)) should equal g: diff = {:.2e}",
diff
);
}
}
#[test]
#[cfg(feature = "rand")]
fn test_random_haar_distribution() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(123);
let samples: Vec<SU2> = (0..1000).map(|_| SU2::random_haar(&mut rng)).collect();
let mean_distance: f64 =
samples.iter().map(SU2::distance_to_identity).sum::<f64>() / samples.len() as f64;
assert!(
mean_distance > 2.5 && mean_distance < 3.5,
"Mean distance from identity should be ~π, got {}",
mean_distance
);
let far_from_identity = samples
.iter()
.filter(|g| g.distance_to_identity() > std::f64::consts::PI / 2.0)
.count();
assert!(
far_from_identity > 100,
"Should have many elements far from identity, got {}",
far_from_identity
);
}
#[test]
fn test_adjoint_action_simple() {
use crate::traits::LieGroup;
let e = SU2::identity();
let x = Su2Algebra([1.0, 0.0, 0.0]);
let result = e.adjoint_action(&x);
println!("Identity test: X = {:?}, Ad_e(X) = {:?}", x, result);
assert!((result.0[0] - x.0[0]).abs() < 1e-10);
assert!((result.0[1] - x.0[1]).abs() < 1e-10);
assert!((result.0[2] - x.0[2]).abs() < 1e-10);
let g = SU2::rotation_z(std::f64::consts::FRAC_PI_2);
let x_basis = Su2Algebra([1.0, 0.0, 0.0]);
let rotated = g.adjoint_action(&x_basis);
println!(
"Rotation test: X = {:?}, Ad_{{Rz(π/2)}}(X) = {:?}",
x_basis, rotated
);
}
#[test]
fn test_su2_casimir_scalar() {
use crate::representation::Spin;
use crate::Casimir;
let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&Spin::ZERO);
assert_eq!(c2, 0.0, "Casimir of scalar representation should be 0");
}
#[test]
fn test_su2_casimir_spinor() {
use crate::representation::Spin;
use crate::Casimir;
let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&Spin::HALF);
assert_eq!(c2, 0.75, "Casimir of spinor representation should be 3/4");
}
#[test]
fn test_su2_casimir_vector() {
use crate::representation::Spin;
use crate::Casimir;
let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&Spin::ONE);
assert_eq!(c2, 2.0, "Casimir of vector representation should be 2");
}
#[test]
fn test_su2_casimir_j_three_halves() {
use crate::representation::Spin;
use crate::Casimir;
let j_three_halves = Spin::from_half_integer(3);
let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&j_three_halves);
assert_eq!(c2, 3.75, "Casimir for j=3/2 should be 15/4 = 3.75");
}
#[test]
fn test_su2_casimir_formula() {
use crate::representation::Spin;
use crate::Casimir;
for two_j in 0..10 {
let spin = Spin::from_half_integer(two_j);
let j = spin.value();
let c2 = Su2Algebra::quadratic_casimir_eigenvalue(&spin);
let expected = j * (j + 1.0);
assert_relative_eq!(c2, expected, epsilon = 1e-10);
}
}
#[test]
fn test_su2_rank() {
use crate::Casimir;
assert_eq!(Su2Algebra::rank(), 1, "SU(2) should have rank 1");
assert_eq!(
Su2Algebra::num_casimirs(),
1,
"SU(2) should have 1 Casimir operator"
);
}
#[test]
fn test_log_stable_identity() {
let e = SU2::identity();
let log_e = e.log_stable().expect("log of identity should succeed");
assert!(log_e.norm() < 1e-10, "log(I) should be zero");
}
#[test]
fn test_log_stable_small_rotation() {
let theta = 0.1;
let g = SU2::rotation_x(theta);
let log_g = g
.log_stable()
.expect("log of small rotation should succeed");
assert!((log_g.0[0] - theta).abs() < 1e-10);
assert!(log_g.0[1].abs() < 1e-10);
assert!(log_g.0[2].abs() < 1e-10);
}
#[test]
fn test_log_stable_large_rotation() {
let theta = 2.5; let g = SU2::rotation_y(theta);
let log_g = g.log_stable().expect("log of rotation < π should succeed");
assert!(log_g.0[0].abs() < 1e-10);
assert!((log_g.0[1] - theta).abs() < 1e-10);
assert!(log_g.0[2].abs() < 1e-10);
}
#[test]
fn test_log_stable_vs_log_consistency() {
use crate::traits::{LieAlgebra, LieGroup};
for theta in [0.1, 0.5, 1.0, 1.5, 2.0, 2.5, std::f64::consts::PI] {
let g = SU2::rotation_z(theta);
let log_standard = g.log().expect("log should succeed");
let log_stable = g.log_stable().expect("log_stable should succeed");
let diff = log_standard.add(&log_stable.scale(-1.0)).norm();
assert!(
diff < 1e-10,
"log and log_stable should agree for θ = {}: diff = {:.2e}",
theta,
diff
);
}
}
#[test]
fn test_log_with_condition_returns_condition() {
let theta = 1.0;
let g = SU2::rotation_x(theta);
let (log_g, cond) = g
.log_with_condition()
.expect("log_with_condition should succeed");
assert!(
cond.is_well_conditioned(),
"θ = 1 should be well-conditioned"
);
assert!(
(cond.angle() - theta).abs() < 1e-10,
"reported angle should match"
);
assert!((log_g.0[0] - theta).abs() < 1e-10);
}
#[test]
fn test_log_with_condition_near_pi() {
let theta = std::f64::consts::PI - 0.01;
let g = SU2::rotation_z(theta);
let (log_g, cond) = g
.log_with_condition()
.expect("log_with_condition should return best-effort");
assert!(
cond.is_well_conditioned(),
"θ ≈ π should be numerically well-conditioned: κ = {}",
cond.condition_number()
);
assert!(
(cond.distance_to_cut_locus() - (std::f64::consts::PI + 0.01)).abs() < 1e-10,
"distance to cut locus should be ≈ π + 0.01: got {}",
cond.distance_to_cut_locus()
);
assert!((log_g.0[2] - theta).abs() < 1e-6);
}
#[test]
fn test_log_condition_from_angle() {
use crate::{LogCondition, LogQuality};
let cond_small = LogCondition::from_angle(0.5);
assert_eq!(cond_small.quality(), LogQuality::Good);
assert!(cond_small.condition_number() > 2.0 && cond_small.condition_number() < 10.0);
let cond_half_pi = LogCondition::from_angle(std::f64::consts::FRAC_PI_2);
assert_eq!(cond_half_pi.quality(), LogQuality::Excellent);
assert!(cond_half_pi.is_well_conditioned());
let cond_near_pi = LogCondition::from_angle(std::f64::consts::PI - 0.001);
assert!(
cond_near_pi.is_well_conditioned(),
"Near π should be numerically stable: κ = {}",
cond_near_pi.condition_number()
);
let cond_near_zero = LogCondition::from_angle(0.001);
assert!(
!cond_near_zero.is_well_conditioned(),
"Near zero should have poor conditioning: κ = {}",
cond_near_zero.condition_number()
);
}
#[test]
fn test_log_stable_roundtrip() {
use crate::traits::{LieAlgebra, LieGroup};
use rand::SeedableRng;
use rand_distr::{Distribution, Uniform};
let mut rng = rand::rngs::StdRng::seed_from_u64(99999);
let dist = Uniform::new(-2.5, 2.5);
for _ in 0..100 {
let x = Su2Algebra([
dist.sample(&mut rng),
dist.sample(&mut rng),
dist.sample(&mut rng),
]);
if x.norm() > std::f64::consts::PI - 0.2 {
continue;
}
let g = SU2::exp(&x);
let x_recovered = g.log_stable().expect("log_stable should succeed");
let diff = x.add(&x_recovered.scale(-1.0));
assert!(
diff.norm() < 1e-8,
"log_stable(exp(X)) should equal X: ||diff|| = {:.2e}",
diff.norm()
);
}
}
#[test]
fn test_log_quality_display() {
use crate::LogQuality;
assert_eq!(format!("{}", LogQuality::Excellent), "Excellent");
assert_eq!(format!("{}", LogQuality::Good), "Good");
assert_eq!(format!("{}", LogQuality::Acceptable), "Acceptable");
assert_eq!(format!("{}", LogQuality::Poor), "Poor");
assert_eq!(format!("{}", LogQuality::AtSingularity), "AtSingularity");
}
}