use crate::error::{self, Result};
use crate::math::Complex;
#[derive(Debug, Clone)]
pub struct CMatrix {
data: Vec<Complex>,
n: usize,
}
impl CMatrix {
pub const MAX_DIM: usize = 64;
pub fn zeros(n: usize) -> Self {
Self {
data: vec![Complex::ZERO; n * n],
n,
}
}
pub fn eye(n: usize) -> Self {
let mut m = Self::zeros(n);
for i in 0..n {
m.set(i, i, Complex::ONE);
}
m
}
pub fn from_rows(rows: Vec<Vec<Complex>>) -> Result<Self> {
let n = rows.len();
if n == 0 {
return Err(error::invalid_param("rows", "matrix must be non-empty"));
}
if n > Self::MAX_DIM {
return Err(error::invalid_param(
"n",
"matrix dimension exceeds CMatrix::MAX_DIM (64)",
));
}
for row in rows.iter() {
if row.len() != n {
return Err(error::invalid_param(
"rows",
"all rows must have length equal to the number of rows (square matrix)",
));
}
}
let mut data = Vec::with_capacity(n * n);
for row in &rows {
data.extend_from_slice(row);
}
Ok(Self { data, n })
}
#[inline]
pub fn n(&self) -> usize {
self.n
}
#[inline]
pub fn get(&self, i: usize, j: usize) -> Complex {
self.data[i * self.n + j]
}
#[inline]
pub fn set(&mut self, i: usize, j: usize, v: Complex) {
self.data[i * self.n + j] = v;
}
#[inline]
fn add_to(&mut self, i: usize, j: usize, v: Complex) {
let cur = self.get(i, j);
self.set(i, j, cur.add(&v));
}
pub fn trace(&self) -> Complex {
let mut t = Complex::ZERO;
for i in 0..self.n {
t = t.add(&self.get(i, i));
}
t
}
pub fn conj_transpose(&self) -> Self {
let mut out = Self::zeros(self.n);
for i in 0..self.n {
for j in 0..self.n {
out.set(j, i, self.get(i, j).conj());
}
}
out
}
pub fn add(&self, other: &Self) -> Result<Self> {
if self.n != other.n {
return Err(error::invalid_param(
"other",
"matrix dimensions must match for addition",
));
}
let mut out = Self::zeros(self.n);
for k in 0..self.data.len() {
out.data[k] = self.data[k].add(&other.data[k]);
}
Ok(out)
}
pub fn sub(&self, other: &Self) -> Result<Self> {
if self.n != other.n {
return Err(error::invalid_param(
"other",
"matrix dimensions must match for subtraction",
));
}
let mut out = Self::zeros(self.n);
for k in 0..self.data.len() {
out.data[k] = self.data[k].sub(&other.data[k]);
}
Ok(out)
}
pub fn scale(&self, s: Complex) -> Self {
let mut out = self.clone();
for k in 0..out.data.len() {
out.data[k] = out.data[k].mul(&s);
}
out
}
pub fn scale_real(&self, s: f64) -> Self {
self.scale(Complex::from_real(s))
}
pub fn matmul(&self, other: &Self) -> Result<Self> {
if self.n != other.n {
return Err(error::invalid_param(
"other",
"matrix dimensions must match for multiplication",
));
}
let n = self.n;
let mut out = Self::zeros(n);
for i in 0..n {
for k in 0..n {
let aik = self.get(i, k);
if aik.re == 0.0 && aik.im == 0.0 {
continue;
}
for j in 0..n {
let v = aik.mul(&other.get(k, j));
out.add_to(i, j, v);
}
}
}
Ok(out)
}
pub fn frobenius_norm(&self) -> f64 {
self.data.iter().map(|c| c.norm_sq()).sum::<f64>().sqrt()
}
pub fn inverse(&self) -> Result<Self> {
let n = self.n;
let mut aug: Vec<Vec<Complex>> = (0..n)
.map(|i| {
let mut row: Vec<Complex> = (0..n).map(|j| self.get(i, j)).collect();
for j in 0..n {
row.push(if i == j { Complex::ONE } else { Complex::ZERO });
}
row
})
.collect();
let max_elem = aug
.iter()
.flat_map(|row| row.iter().take(n))
.map(|c| c.norm())
.fold(0.0_f64, f64::max);
let pivot_thresh = 1e-14 * max_elem.max(1.0);
for col in 0..n {
let mut max_row = col;
let mut max_val = aug[col][col].norm();
for (row, aug_row) in aug.iter().enumerate().skip(col + 1) {
let v = aug_row[col].norm();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < pivot_thresh {
return Err(error::numerical_error(
"matrix is singular or nearly singular (Gauss-Jordan inverse)",
));
}
aug.swap(col, max_row);
let pivot = aug[col][col];
let pivot_inv = Complex::ONE.div(&pivot);
for v in &mut aug[col] {
*v = v.mul(&pivot_inv);
}
for row in 0..n {
if row == col {
continue;
}
let factor = aug[row][col];
if factor.re == 0.0 && factor.im == 0.0 {
continue;
}
let col_vals: Vec<Complex> = aug[col].clone();
for (dst, src) in aug[row].iter_mut().zip(col_vals.iter()) {
*dst = dst.sub(&factor.mul(src));
}
}
}
let mut result = Self::zeros(n);
for (i, aug_row) in aug.iter().enumerate() {
for (j, val) in aug_row[n..].iter().enumerate() {
result.set(i, j, *val);
}
}
Ok(result)
}
pub fn hermitian_eigendecomposition(&self) -> Result<(Vec<f64>, Self)> {
hermitian_eig_impl(self)
}
pub fn from_diagonal(diag: &[f64]) -> Self {
let n = diag.len();
let mut m = Self::zeros(n);
for (i, &v) in diag.iter().enumerate() {
m.set(i, i, Complex::from_real(v));
}
m
}
pub fn column(&self, j: usize) -> Vec<Complex> {
(0..self.n).map(|i| self.get(i, j)).collect()
}
pub fn row(&self, i: usize) -> Vec<Complex> {
(0..self.n).map(|j| self.get(i, j)).collect()
}
}
#[allow(clippy::needless_range_loop)]
fn hermitian_eig_impl(h: &CMatrix) -> Result<(Vec<f64>, CMatrix)> {
let n = h.n;
if n == 0 {
return Ok((vec![], CMatrix::zeros(0)));
}
if n == 1 {
let e = h.get(0, 0).re;
let mut v = CMatrix::zeros(1);
v.set(0, 0, Complex::ONE);
return Ok((vec![e], v));
}
let mut a: Vec<Vec<Complex>> = (0..n)
.map(|i| (0..n).map(|j| h.get(i, j)).collect())
.collect();
let mut v: Vec<Vec<Complex>> = (0..n)
.map(|i| {
(0..n)
.map(|j| if i == j { Complex::ONE } else { Complex::ZERO })
.collect()
})
.collect();
let frobenius_scale = a
.iter()
.flatten()
.map(|c| c.norm_sq())
.sum::<f64>()
.sqrt()
.max(1.0);
let tol = 1e-14 * frobenius_scale;
const MAX_SWEEPS: usize = 100;
let mut converged = false;
for _sweep in 0..MAX_SWEEPS {
let mut off_diag_sq = 0.0_f64;
for p in 0..n {
for q in (p + 1)..n {
off_diag_sq += a[p][q].norm_sq();
}
}
if off_diag_sq.sqrt() < tol {
converged = true;
break;
}
for p in 0..n {
for q in (p + 1)..n {
let apq = a[p][q];
if apq.norm() < 1e-300 {
continue;
}
let phi = apq.phase();
let phase_q = Complex::from_polar(1.0, -phi);
for row in a.iter_mut() {
row[q] = row[q].mul(&phase_q);
}
let phase_q_conj = phase_q.conj();
for col in 0..n {
a[q][col] = a[q][col].mul(&phase_q_conj);
}
for row in v.iter_mut() {
row[q] = row[q].mul(&phase_q);
}
let app = a[p][p].re;
let aqq = a[q][q].re;
let r = a[p][q].re;
if r.abs() < 1e-300 {
continue;
}
let tau = (aqq - app) / (2.0 * r);
let t = if tau >= 0.0 {
-1.0 / (tau + (1.0 + tau * tau).sqrt())
} else {
1.0 / (-tau + (1.0 + tau * tau).sqrt())
};
let c = 1.0 / (1.0 + t * t).sqrt();
let s = t * c;
a[p][p] = Complex::from_real(app + t * r);
a[q][q] = Complex::from_real(aqq - t * r);
a[p][q] = Complex::ZERO;
a[q][p] = Complex::ZERO;
for k in 0..n {
if k == p || k == q {
continue;
}
let akp = a[k][p];
let akq = a[k][q];
let new_akp = akp.scale(c).add(&akq.scale(s));
let new_akq = akq.scale(c).sub(&akp.scale(s));
a[k][p] = new_akp;
a[p][k] = new_akp.conj();
a[k][q] = new_akq;
a[q][k] = new_akq.conj();
}
for row in v.iter_mut() {
let vip = row[p];
let viq = row[q];
row[p] = vip.scale(c).add(&viq.scale(s));
row[q] = viq.scale(c).sub(&vip.scale(s));
}
}
}
}
if !converged {
return Err(crate::error::numerical_error(
"Jacobi eigenvalue sweep did not converge within 100 sweeps",
));
}
let diag: Vec<f64> = (0..n).map(|i| a[i][i].re).collect();
let mut pairs: Vec<(f64, usize)> = diag
.iter()
.copied()
.enumerate()
.map(|(i, e)| (e, i))
.collect();
pairs.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap_or(std::cmp::Ordering::Equal));
let eigenvalues: Vec<f64> = pairs.iter().map(|(e, _)| *e).collect();
let mut eigenvectors = CMatrix::zeros(n);
for (col_out, &(_, col_in)) in pairs.iter().enumerate() {
for row in 0..n {
eigenvectors.set(row, col_out, v[row][col_in]);
}
}
Ok((eigenvalues, eigenvectors))
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn cx(re: f64, im: f64) -> Complex {
Complex::new(re, im)
}
#[test]
fn test_eye_trace() {
let m = CMatrix::eye(4);
assert!((m.trace().re - 4.0).abs() < 1e-14);
assert!((m.trace().im).abs() < 1e-14);
}
#[test]
fn test_matmul_identity() {
let a = CMatrix::from_rows(vec![
vec![cx(1.0, 0.0), cx(2.0, 1.0)],
vec![cx(3.0, -1.0), cx(4.0, 0.0)],
])
.unwrap();
let i = CMatrix::eye(2);
let r = a.matmul(&i).unwrap();
for row in 0..2 {
for col in 0..2 {
let diff = r.get(row, col).sub(&a.get(row, col));
assert!(diff.norm() < 1e-13);
}
}
}
#[test]
fn test_matmul_2x2() {
let a = CMatrix::from_rows(vec![
vec![cx(1.0, 0.0), cx(0.0, 1.0)],
vec![cx(0.0, -1.0), cx(1.0, 0.0)],
])
.unwrap();
let r = a.matmul(&a).unwrap();
assert!(r.trace().re.is_finite());
}
#[test]
fn test_conj_transpose() {
let a = CMatrix::from_rows(vec![
vec![cx(1.0, 2.0), cx(3.0, 4.0)],
vec![cx(5.0, 6.0), cx(7.0, 8.0)],
])
.unwrap();
let ah = a.conj_transpose();
assert!((ah.get(0, 1).re - 5.0).abs() < 1e-14);
assert!((ah.get(0, 1).im + 6.0).abs() < 1e-14);
}
#[test]
fn test_inverse_2x2() {
let a = CMatrix::from_rows(vec![
vec![cx(2.0, 0.0), cx(1.0, 0.0)],
vec![cx(1.0, 0.0), cx(1.0, 0.0)],
])
.unwrap();
let inv = a.inverse().unwrap();
let prod = a.matmul(&inv).unwrap();
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(approx_eq(prod.get(i, j).re, expected, 1e-12));
assert!(approx_eq(prod.get(i, j).im, 0.0, 1e-12));
}
}
}
#[test]
fn test_inverse_complex() {
let a = CMatrix::from_rows(vec![
vec![cx(1.0, 1.0), cx(0.0, 1.0)],
vec![cx(0.0, -1.0), cx(1.0, -1.0)],
])
.unwrap();
let inv = a.inverse().unwrap();
let prod = a.matmul(&inv).unwrap();
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(approx_eq(prod.get(i, j).re, expected, 1e-12));
}
}
}
#[test]
fn test_inverse_singular_errors() {
let a = CMatrix::from_rows(vec![
vec![cx(1.0, 0.0), cx(2.0, 0.0)],
vec![cx(2.0, 0.0), cx(4.0, 0.0)],
])
.unwrap();
assert!(a.inverse().is_err());
}
#[test]
fn test_hermitian_eigendecomposition_identity() {
let h = CMatrix::eye(3);
let (vals, _vecs) = h.hermitian_eigendecomposition().unwrap();
for v in &vals {
assert!(approx_eq(*v, 1.0, 1e-10));
}
}
#[test]
fn test_hermitian_eigendecomposition_2x2_real() {
let h = CMatrix::from_rows(vec![
vec![cx(2.0, 0.0), cx(1.0, 0.0)],
vec![cx(1.0, 0.0), cx(2.0, 0.0)],
])
.unwrap();
let (vals, vecs) = h.hermitian_eigendecomposition().unwrap();
assert!(approx_eq(vals[0], 1.0, 1e-10));
assert!(approx_eq(vals[1], 3.0, 1e-10));
let hv = h.matmul(&vecs).unwrap();
for i in 0..2 {
let hv_i = hv.get(i, 0);
let lv_i = vecs.get(i, 0).scale(vals[0]);
assert!(approx_eq(hv_i.re, lv_i.re, 1e-9));
assert!(approx_eq(hv_i.im, lv_i.im, 1e-9));
}
}
#[test]
fn test_hermitian_eigendecomposition_complex_offdiag() {
let h = CMatrix::from_rows(vec![
vec![cx(1.0, 0.0), cx(0.0, 1.0)],
vec![cx(0.0, -1.0), cx(1.0, 0.0)],
])
.unwrap();
let (vals, _) = h.hermitian_eigendecomposition().unwrap();
assert!(approx_eq(vals[0], 0.0, 1e-9));
assert!(approx_eq(vals[1], 2.0, 1e-9));
}
#[test]
fn test_eigendecomposition_3x3_diagonal() {
let h = CMatrix::from_diagonal(&[3.0, 1.0, 2.0]);
let (vals, _) = h.hermitian_eigendecomposition().unwrap();
assert!(approx_eq(vals[0], 1.0, 1e-10));
assert!(approx_eq(vals[1], 2.0, 1e-10));
assert!(approx_eq(vals[2], 3.0, 1e-10));
}
#[test]
fn test_hermitian_eigendecomposition_satisfies_eigenvalue_equation_3x3() {
let h = CMatrix::from_rows(vec![
vec![cx(2.0, 0.0), cx(0.5, 0.0), cx(0.3, 0.0)],
vec![cx(0.5, 0.0), cx(1.5, 0.0), cx(0.2, 0.0)],
vec![cx(0.3, 0.0), cx(0.2, 0.0), cx(3.0, 0.0)],
])
.unwrap();
let (vals, vecs) = h.hermitian_eigendecomposition().unwrap();
assert!(vals[0] < vals[1] && vals[1] < vals[2]);
let hv = h.matmul(&vecs).unwrap();
#[allow(clippy::needless_range_loop)]
for col in 0..3 {
for row in 0..3 {
let lhs = hv.get(row, col);
let rhs = vecs.get(row, col).scale(vals[col]);
assert!(
approx_eq(lhs.re, rhs.re, 1e-9) && approx_eq(lhs.im, rhs.im, 1e-9),
"H v_{} != lambda_{} v_{} at row {}: {:?} vs {:?}",
col,
col,
col,
row,
lhs,
rhs
);
}
}
for a in 0..3 {
for b in 0..3 {
let dot: Complex = (0..3)
.map(|r| vecs.get(r, a).conj().mul(&vecs.get(r, b)))
.fold(Complex::ZERO, |acc, c| acc.add(&c));
let expected = if a == b { 1.0 } else { 0.0 };
assert!(approx_eq(dot.re, expected, 1e-9) && approx_eq(dot.im, 0.0, 1e-9));
}
}
}
#[test]
fn test_eigendecomposition_eigenvectors_orthonormal() {
let h = CMatrix::from_rows(vec![
vec![cx(2.0, 0.0), cx(1.0, 0.0)],
vec![cx(1.0, 0.0), cx(2.0, 0.0)],
])
.unwrap();
let (_, vecs) = h.hermitian_eigendecomposition().unwrap();
let v0: Vec<Complex> = (0..2).map(|i| vecs.get(i, 0)).collect();
let v1: Vec<Complex> = (0..2).map(|i| vecs.get(i, 1)).collect();
let dot: Complex = v0
.iter()
.zip(v1.iter())
.map(|(a, b)| a.conj().mul(b))
.fold(Complex::ZERO, |acc, x| acc.add(&x));
assert!(dot.norm() < 1e-10);
let norm0_sq: f64 = v0.iter().map(|c| c.norm_sq()).sum();
assert!(approx_eq(norm0_sq, 1.0, 1e-10));
}
#[test]
fn test_from_rows_inconsistent_size() {
let result = CMatrix::from_rows(vec![vec![cx(1.0, 0.0), cx(0.0, 0.0)], vec![cx(0.0, 0.0)]]);
assert!(result.is_err());
}
#[test]
fn test_add_and_sub() {
let a = CMatrix::eye(2);
let b = CMatrix::eye(2);
let s = a.add(&b).unwrap();
assert!((s.get(0, 0).re - 2.0).abs() < 1e-14);
let d = a.sub(&b).unwrap();
assert!((d.get(0, 0).re).abs() < 1e-14);
}
#[test]
fn test_scale() {
let m = CMatrix::eye(3);
let s = m.scale(Complex::new(2.0, 0.0));
assert!((s.get(0, 0).re - 2.0).abs() < 1e-14);
assert!((s.get(0, 1).re).abs() < 1e-14);
}
#[test]
fn test_column_and_row() {
let a = CMatrix::from_rows(vec![
vec![cx(1.0, 0.0), cx(2.0, 0.0)],
vec![cx(3.0, 0.0), cx(4.0, 0.0)],
])
.unwrap();
let col = a.column(1);
assert!((col[0].re - 2.0).abs() < 1e-14);
assert!((col[1].re - 4.0).abs() < 1e-14);
let row = a.row(0);
assert!((row[0].re - 1.0).abs() < 1e-14);
assert!((row[1].re - 2.0).abs() < 1e-14);
}
}