use std::collections::HashMap;
use crate::error::{self, Result};
use crate::frustrated::lattice::Xorshift64;
use crate::math::{CMatrix, Complex};
const LANCZOS_SEED: u64 = 0x5EED_1234_ABCD_0001;
const LANCZOS_TOL: f64 = 1e-11;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SpinBasisState {
pub basis: Vec<u64>,
pub amplitudes: Vec<f64>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ExactDiagonalization {
pub num_sites: usize,
pub bonds: Vec<(usize, usize)>,
pub coupling_j: f64,
}
impl ExactDiagonalization {
pub fn new(num_sites: usize, bonds: Vec<(usize, usize)>, coupling_j: f64) -> Result<Self> {
for &(i, j) in &bonds {
if i >= num_sites || j >= num_sites {
return Err(error::invalid_param(
"bonds",
"bond references a site index out of range",
));
}
if i == j {
return Err(error::invalid_param(
"bonds",
"a bond cannot connect a site to itself",
));
}
}
if coupling_j <= 0.0 {
return Err(error::invalid_param(
"coupling_j",
"RVB physics requires antiferromagnetic coupling J > 0",
));
}
Ok(Self {
num_sites,
bonds,
coupling_j,
})
}
pub fn dense_ground_energy(&self) -> Result<f64> {
Ok(self.dense_ground_state()?.0)
}
pub fn dense_ground_state(&self) -> Result<(f64, SpinBasisState)> {
let n = self.num_sites;
if n == 0 {
return Err(error::invalid_param(
"num_sites",
"num_sites must be positive",
));
}
let dim = match 1usize.checked_shl(n as u32) {
Some(d) if d <= CMatrix::MAX_DIM => d,
_ => {
return Err(error::invalid_param(
"num_sites",
&format!(
"dense exact diagonalization requires 2^num_sites <= CMatrix::MAX_DIM (64); \
num_sites={} is too large; use ground_energy_sz0/ground_state_sz0 (Lanczos) \
instead for N up to MAX_ED_SITES={}",
n,
super::MAX_ED_SITES
),
));
},
};
let mut h = vec![vec![0.0_f64; dim]; dim];
#[allow(clippy::needless_range_loop)]
for b in 0..dim {
let mut diag = 0.0_f64;
for &(i, j) in &self.bonds {
let bit_i = (b >> i) & 1;
let bit_j = (b >> j) & 1;
if bit_i == bit_j {
diag += 0.25 * self.coupling_j;
} else {
diag -= 0.25 * self.coupling_j;
let flipped = b ^ (1usize << i) ^ (1usize << j);
h[flipped][b] += 0.5 * self.coupling_j;
}
}
h[b][b] += diag;
}
let rows: Vec<Vec<Complex>> = h
.into_iter()
.map(|row| row.into_iter().map(Complex::from_real).collect())
.collect();
let h_matrix = CMatrix::from_rows(rows)?;
let (vals, vecs) = h_matrix.hermitian_eigendecomposition()?;
let ground_energy = vals[0];
let amplitudes: Vec<f64> = (0..dim).map(|row| vecs.get(row, 0).re).collect();
let basis: Vec<u64> = (0..dim as u64).collect();
Ok((ground_energy, SpinBasisState { basis, amplitudes }))
}
pub fn ground_energy_sz0(&self) -> Result<f64> {
Ok(self.ground_state_sz0()?.0)
}
pub fn ground_state_sz0(&self) -> Result<(f64, SpinBasisState)> {
let n = self.num_sites;
if n == 0 {
return Err(error::invalid_param(
"num_sites",
"num_sites must be positive",
));
}
if n > super::MAX_ED_SITES {
return Err(error::invalid_param(
"num_sites",
&format!(
"Sz=0 Lanczos exact diagonalization is limited to MAX_ED_SITES={}; got num_sites={}",
super::MAX_ED_SITES,
n
),
));
}
if n % 2 != 0 {
return Err(error::invalid_param(
"num_sites",
"the Sz=0 sector requires an even number of sites",
));
}
let basis = build_sz0_basis(n);
let dim = basis.len();
let index_of: HashMap<u64, usize> =
basis.iter().enumerate().map(|(idx, &b)| (b, idx)).collect();
let max_iter = dim.min(CMatrix::MAX_DIM);
let mut lanczos_vectors: Vec<Vec<f64>> = Vec::with_capacity(max_iter);
let mut alpha: Vec<f64> = Vec::with_capacity(max_iter);
let mut beta: Vec<f64> = Vec::with_capacity(max_iter);
let mut rng = Xorshift64::new(LANCZOS_SEED)?;
let mut v_curr: Vec<f64> = (0..dim).map(|_| rng.next_f64() - 0.5).collect();
normalize(&mut v_curr)?;
let mut v_prev = vec![0.0_f64; dim];
let mut beta_prev = 0.0_f64;
let mut prev_energy = f64::INFINITY;
for step in 0..max_iter {
lanczos_vectors.push(v_curr.clone());
let mut w =
apply_heisenberg_bonds(&basis, &index_of, &self.bonds, self.coupling_j, &v_curr);
if step > 0 {
axpy(&mut w, -beta_prev, &v_prev);
}
let alpha_k = dot(&v_curr, &w);
axpy(&mut w, -alpha_k, &v_curr);
for prev_vec in &lanczos_vectors {
let proj = dot(prev_vec, &w);
axpy(&mut w, -proj, prev_vec);
}
alpha.push(alpha_k);
let (t_vals, _) = diagonalize_tridiagonal(&alpha, &beta)?;
let current_energy = t_vals[0];
let converged = step > 0 && (current_energy - prev_energy).abs() < LANCZOS_TOL;
prev_energy = current_energy;
let beta_next = norm(&w);
let breakdown = beta_next < 1e-12;
if converged || breakdown || step == max_iter - 1 {
break;
}
beta.push(beta_next);
v_prev.copy_from_slice(&v_curr);
v_curr = w.iter().map(|x| x / beta_next).collect();
beta_prev = beta_next;
}
let (t_vals, t_vecs) = diagonalize_tridiagonal(&alpha, &beta)?;
let ground_energy = t_vals[0];
if !ground_energy.is_finite() {
return Err(error::numerical_error(
"Lanczos ground-state energy is not finite",
));
}
let m = alpha.len();
let mut amplitudes = vec![0.0_f64; dim];
for (step, lanczos_vec) in lanczos_vectors.iter().enumerate().take(m) {
let coeff = t_vecs.get(step, 0).re;
axpy(&mut amplitudes, coeff, lanczos_vec);
}
normalize(&mut amplitudes)?;
Ok((ground_energy, SpinBasisState { basis, amplitudes }))
}
pub fn total_spin_squared(&self, state: &SpinBasisState) -> Result<f64> {
let n = self.num_sites;
let all_pairs: Vec<(usize, usize)> = (0..n)
.flat_map(|i| ((i + 1)..n).map(move |j| (i, j)))
.collect();
let index_of: HashMap<u64, usize> = state
.basis
.iter()
.enumerate()
.map(|(idx, &b)| (b, idx))
.collect();
let norm_sq = dot(&state.amplitudes, &state.amplitudes);
if norm_sq < 1e-14 {
return Err(error::numerical_error(
"state has (numerically) zero norm; cannot compute <S_tot^2>",
));
}
let hv =
apply_heisenberg_bonds(&state.basis, &index_of, &all_pairs, 2.0, &state.amplitudes);
let cross_term = dot(&state.amplitudes, &hv);
let onsite_term = n as f64 * 0.75 * norm_sq;
Ok((onsite_term + cross_term) / norm_sq)
}
}
fn build_sz0_basis(n: usize) -> Vec<u64> {
let half = (n / 2) as u32;
let total = 1u64 << n; (0..total).filter(|b| b.count_ones() == half).collect()
}
fn apply_heisenberg_bonds(
basis: &[u64],
index_of: &HashMap<u64, usize>,
bonds: &[(usize, usize)],
coupling_j: f64,
v: &[f64],
) -> Vec<f64> {
let dim = basis.len();
let mut result = vec![0.0_f64; dim];
for (idx, &b) in basis.iter().enumerate() {
let mut diag = 0.0_f64;
for &(i, j) in bonds {
let bit_i = (b >> i) & 1;
let bit_j = (b >> j) & 1;
if bit_i == bit_j {
diag += 0.25 * coupling_j;
} else {
diag -= 0.25 * coupling_j;
let flipped = b ^ (1u64 << i) ^ (1u64 << j);
if let Some(&f_idx) = index_of.get(&flipped) {
result[f_idx] += 0.5 * coupling_j * v[idx];
}
}
}
result[idx] += diag * v[idx];
}
result
}
fn diagonalize_tridiagonal(alpha: &[f64], beta: &[f64]) -> Result<(Vec<f64>, CMatrix)> {
let m = alpha.len();
let mut rows = vec![vec![Complex::ZERO; m]; m];
for (i, &a) in alpha.iter().enumerate() {
rows[i][i] = Complex::from_real(a);
}
for (i, &b) in beta.iter().enumerate() {
rows[i][i + 1] = Complex::from_real(b);
rows[i + 1][i] = Complex::from_real(b);
}
let t = CMatrix::from_rows(rows)?;
t.hermitian_eigendecomposition()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn norm(v: &[f64]) -> f64 {
dot(v, v).sqrt()
}
fn normalize(v: &mut [f64]) -> Result<()> {
let n = norm(v);
if n < 1e-14 {
return Err(error::numerical_error(
"cannot normalize a (numerically) zero vector",
));
}
for x in v.iter_mut() {
*x /= n;
}
Ok(())
}
fn axpy(y: &mut [f64], alpha: f64, x: &[f64]) {
for (yi, xi) in y.iter_mut().zip(x.iter()) {
*yi += alpha * xi;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_two_site_dense_ground_energy() {
let ed = ExactDiagonalization::new(2, vec![(0, 1)], 1.0).expect("valid ED instance");
let e0 = ed
.dense_ground_energy()
.expect("dense diagonalization should succeed");
assert!((e0 - (-0.75)).abs() < 1e-10, "expected -3J/4, got {}", e0);
}
#[test]
fn test_ring4_dense_ground_energy() {
let ed = ExactDiagonalization::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)], 1.0)
.expect("valid ED instance");
let e0 = ed
.dense_ground_energy()
.expect("dense diagonalization should succeed");
assert!((e0 - (-2.0)).abs() < 1e-9, "expected -2J, got {}", e0);
}
#[test]
fn test_dense_diagonalization_rejects_large_n() {
let bonds: Vec<(usize, usize)> = (0..7).map(|i| (i, (i + 1) % 8)).collect();
let ed = ExactDiagonalization::new(8, bonds, 1.0).expect("valid ED instance (bonds only)");
assert!(
ed.dense_ground_state().is_err(),
"N=8 (2^8=256>64) must be rejected"
);
}
#[test]
fn test_dense_vs_lanczos_agreement_ring4() {
let ed = ExactDiagonalization::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)], 1.0)
.expect("valid ED instance");
let e_dense = ed.dense_ground_energy().expect("dense ED");
let e_lanczos = ed.ground_energy_sz0().expect("Lanczos ED");
assert!(
(e_dense - e_lanczos).abs() < 1e-8,
"dense ({}) and Lanczos ({}) ground energies must agree",
e_dense,
e_lanczos
);
}
#[test]
fn test_dense_vs_lanczos_agreement_ladder() {
let bonds = vec![
(0, 1),
(1, 2),
(2, 3),
(4, 5),
(5, 6),
(6, 7),
(0, 4),
(1, 5),
(2, 6),
(3, 7),
];
let ed8 = ExactDiagonalization::new(8, bonds, 1.0).expect("valid ED instance");
let e_lanczos = ed8
.ground_energy_sz0()
.expect("Lanczos ED should succeed for N=8");
assert!(e_lanczos.is_finite());
assert!(
e_lanczos < 0.0,
"AFM ground energy should be negative, got {}",
e_lanczos
);
}
#[test]
fn test_total_spin_squared_near_zero_for_singlet_ground_state() {
let ed = ExactDiagonalization::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)], 1.0)
.expect("valid ED instance");
let (_, state) = ed.dense_ground_state().expect("dense ED");
let s2 = ed.total_spin_squared(&state).expect("total spin squared");
assert!(
s2.abs() < 1e-8,
"expected S_tot^2 ~ 0 for the singlet ground state, got {}",
s2
);
}
#[test]
fn test_total_spin_squared_sz0_ground_state_near_zero() {
let ed = ExactDiagonalization::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)], 1.0)
.expect("valid ED instance");
let (_, state) = ed.ground_state_sz0().expect("Lanczos ED");
let s2 = ed.total_spin_squared(&state).expect("total spin squared");
assert!(
s2.abs() < 1e-6,
"expected S_tot^2 ~ 0 for the singlet ground state, got {}",
s2
);
}
#[test]
fn test_odd_num_sites_sz0_errors() {
let ed =
ExactDiagonalization::new(3, vec![(0, 1), (1, 2)], 1.0).expect("valid ED instance");
assert!(ed.ground_state_sz0().is_err());
}
#[test]
fn test_non_positive_coupling_errors() {
assert!(ExactDiagonalization::new(2, vec![(0, 1)], 0.0).is_err());
}
}