use std::f64::consts::PI;
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
use crate::topomagnon::band_model::MagnonBandModel;
pub struct WilsonLoop<'a> {
pub model: &'a MagnonBandModel,
pub occupied_bands: Vec<usize>,
}
impl<'a> WilsonLoop<'a> {
pub fn new(model: &'a MagnonBandModel, occupied_bands: Vec<usize>) -> Result<Self> {
if occupied_bands.is_empty() {
return Err(error::invalid_param(
"occupied_bands",
"must list at least one band index",
));
}
let nb = model.n_bands();
for &b in &occupied_bands {
if b >= nb {
return Err(error::invalid_param(
"occupied_bands",
&format!("band index {b} exceeds n_bands-1={}", nb - 1),
));
}
}
Ok(Self {
model,
occupied_bands,
})
}
pub fn link_matrix(states_a: &[Vec<Complex>], states_b: &[Vec<Complex>]) -> CMatrix {
let n = states_a.len();
let mut mat = CMatrix::zeros(n);
for (m, state_a) in states_a.iter().enumerate() {
for (nn, state_b) in states_b.iter().enumerate() {
let mut inner = Complex::ZERO;
for j in 0..state_a.len() {
inner = inner.add(&state_a[j].conj().mul(&state_b[j]));
}
mat.set(m, nn, inner);
}
}
mat
}
pub fn wilson_unitary_y(&self, kx: f64, ny: usize) -> Result<CMatrix> {
if ny < 4 {
return Err(error::invalid_param("ny", "need at least 4 ky points"));
}
let n_occ = self.occupied_bands.len();
let mut all_states: Vec<Vec<Vec<Complex>>> = Vec::with_capacity(ny + 1);
for iy in 0..=ny {
let ky = 2.0 * PI * (iy as f64) / (ny as f64);
let (_, vecs) = self.model.diagonalize((kx, ky))?;
let occ: Vec<Vec<Complex>> = self
.occupied_bands
.iter()
.map(|&b| vecs.column(b))
.collect();
all_states.push(occ);
}
let mut w = CMatrix::eye(n_occ);
for iy in 0..ny {
let raw = Self::link_matrix(&all_states[iy], &all_states[iy + 1]);
let m = polar_unitary(&raw)?;
w = w.matmul(&m)?;
}
Ok(w)
}
pub fn wilson_unitary_x(&self, ky: f64, nx: usize) -> Result<CMatrix> {
if nx < 4 {
return Err(error::invalid_param("nx", "need at least 4 kx points"));
}
let n_occ = self.occupied_bands.len();
let mut all_states: Vec<Vec<Vec<Complex>>> = Vec::with_capacity(nx + 1);
for ix in 0..=nx {
let kx = 2.0 * PI * (ix as f64) / (nx as f64);
let (_, vecs) = self.model.diagonalize((kx, ky))?;
let occ: Vec<Vec<Complex>> = self
.occupied_bands
.iter()
.map(|&b| vecs.column(b))
.collect();
all_states.push(occ);
}
let mut w = CMatrix::eye(n_occ);
for ix in 0..nx {
let raw = Self::link_matrix(&all_states[ix], &all_states[ix + 1]);
let m = polar_unitary(&raw)?;
w = w.matmul(&m)?;
}
Ok(w)
}
pub fn wannier_centers(&self, kx: f64, ny: usize) -> Result<Vec<f64>> {
let w = self.wilson_unitary_y(kx, ny)?;
let mut phases = unitary_eig_phases(&w)?;
for ph in &mut phases {
*ph = (*ph / (2.0 * PI)).rem_euclid(1.0);
}
phases.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok(phases)
}
pub fn polarization_p_y(&self, nkx: usize, nky: usize) -> Result<f64> {
if nkx < 2 {
return Err(error::invalid_param("nkx", "need at least 2 kx points"));
}
let mut sum = 0.0_f64;
for ikx in 0..nkx {
let kx = 2.0 * PI * (ikx as f64) / (nkx as f64);
let centers = self.wannier_centers(kx, nky)?;
let avg = centers.iter().sum::<f64>() / (centers.len() as f64);
sum += avg;
}
Ok(sum / (nkx as f64))
}
pub fn polarization_p_x(&self, nkx: usize, nky: usize) -> Result<f64> {
if nky < 2 {
return Err(error::invalid_param("nky", "need at least 2 ky points"));
}
let mut sum = 0.0_f64;
for iky in 0..nky {
let ky = 2.0 * PI * (iky as f64) / (nky as f64);
let w = self.wilson_unitary_x(ky, nkx)?;
let mut phases = unitary_eig_phases(&w)?;
for ph in &mut phases {
*ph = (*ph / (2.0 * PI)).rem_euclid(1.0);
}
let avg = phases.iter().sum::<f64>() / (phases.len() as f64);
sum += avg;
}
Ok(sum / (nky as f64))
}
pub fn nested_polarization(
&self,
wannier_band_idx: usize,
nkx: usize,
nky: usize,
) -> Result<f64> {
let n_occ = self.occupied_bands.len();
if wannier_band_idx >= n_occ {
return Err(error::invalid_param(
"wannier_band_idx",
&format!("index {wannier_band_idx} exceeds n_occ-1={}", n_occ - 1),
));
}
if nkx < 4 {
return Err(error::invalid_param("nkx", "need at least 4 kx points"));
}
let mut w_nested = Complex::ONE;
for ikx in 0..nkx {
let kx = 2.0 * PI * (ikx as f64) / (nkx as f64);
let centers = self.wannier_centers(kx, nky)?;
let nu = centers[wannier_band_idx.min(centers.len() - 1)];
let phase = Complex::from_polar(1.0, 2.0 * PI * nu);
w_nested = w_nested.mul(&phase);
}
let nested_p = w_nested.phase() / (2.0 * PI);
Ok(nested_p)
}
}
fn polar_unitary(m: &CMatrix) -> Result<CMatrix> {
let n = m.n();
if n == 1 {
let z = m.get(0, 0);
let norm = z.norm();
let mut out = CMatrix::zeros(1);
if norm < 1e-15 {
out.set(0, 0, Complex::ONE);
} else {
out.set(0, 0, z.scale(1.0 / norm));
}
return Ok(out);
}
let md = m.conj_transpose();
let mdm = md.matmul(m)?;
let (evals, vecs) = mdm.hermitian_eigendecomposition()?;
let mut d_inv_sqrt = CMatrix::zeros(n);
for (i, &ev) in evals.iter().enumerate() {
let sv = ev.max(0.0).sqrt(); let val = if sv < 1e-14 { 0.0 } else { 1.0 / sv };
d_inv_sqrt.set(i, i, Complex::from_real(val));
}
let vd = vecs.conj_transpose();
let mid = vecs.matmul(&d_inv_sqrt)?;
let m_inv_sqrt = mid.matmul(&vd)?;
m.matmul(&m_inv_sqrt)
}
fn unitary_eig_phases(u: &CMatrix) -> Result<Vec<f64>> {
let n = u.n();
if n == 1 {
let z = u.get(0, 0);
return Ok(vec![z.im.atan2(z.re)]);
}
let ud = u.conj_transpose();
let h_sum = u.add(&ud)?;
let h_herm = h_sum.scale_real(0.5);
let (evals, _) = h_herm.hermitian_eigendecomposition()?;
let phases: Vec<f64> = evals
.iter()
.map(|&c| {
let c_clamped = c.clamp(-1.0, 1.0);
c_clamped.acos()
})
.collect();
Ok(phases)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topomagnon::band_model::MagnonBandModel;
fn trivial_two_band() -> MagnonBandModel {
MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.0, 0.5).unwrap()
}
fn topological_two_band() -> MagnonBandModel {
MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.5, 0.0).unwrap()
}
#[test]
fn new_rejects_empty_bands() {
let m = trivial_two_band();
assert!(WilsonLoop::new(&m, vec![]).is_err());
}
#[test]
fn new_rejects_out_of_range_band() {
let m = trivial_two_band();
assert!(WilsonLoop::new(&m, vec![0, 99]).is_err());
}
#[test]
fn new_valid() {
let m = trivial_two_band();
assert!(WilsonLoop::new(&m, vec![0]).is_ok());
assert!(WilsonLoop::new(&m, vec![0, 1]).is_ok());
}
#[test]
fn link_matrix_identity_same_states() {
let states: Vec<Vec<Complex>> = vec![
vec![Complex::ONE, Complex::ZERO],
vec![Complex::ZERO, Complex::ONE],
];
let m = WilsonLoop::link_matrix(&states, &states);
assert!((m.get(0, 0).re - 1.0).abs() < 1e-14);
assert!((m.get(1, 1).re - 1.0).abs() < 1e-14);
assert!(m.get(0, 1).norm() < 1e-14);
assert!(m.get(1, 0).norm() < 1e-14);
}
#[test]
fn wilson_unitary_y_is_unitary() {
let m = topological_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let w = wl.wilson_unitary_y(0.0, 20).unwrap();
let wd = w.conj_transpose();
let wwd = w.matmul(&wd).unwrap();
let id = CMatrix::eye(1);
let diff = wwd.sub(&id).unwrap();
assert!(
diff.frobenius_norm() < 1e-8,
"W·W† not identity: norm={}",
diff.frobenius_norm()
);
}
#[test]
fn wilson_unitary_x_is_unitary() {
let m = topological_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let w = wl.wilson_unitary_x(0.0, 20).unwrap();
let wd = w.conj_transpose();
let wwd = w.matmul(&wd).unwrap();
let id = CMatrix::eye(1);
let diff = wwd.sub(&id).unwrap();
assert!(
diff.frobenius_norm() < 1e-8,
"W_x·W_x† not identity: norm={}",
diff.frobenius_norm()
);
}
#[test]
fn wannier_centers_trivial_band() {
let m = trivial_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let centers = wl.wannier_centers(0.0, 20).unwrap();
assert_eq!(centers.len(), 1);
assert!(centers[0] >= 0.0 && centers[0] < 1.0);
}
#[test]
fn wannier_centers_sorted_ascending() {
let m = MagnonBandModel::kagome(1.0, 0.4, 0.0).unwrap();
let wl = WilsonLoop::new(&m, vec![0, 1]).unwrap();
let centers = wl.wannier_centers(0.0, 16).unwrap();
assert_eq!(centers.len(), 2);
assert!(
centers[0] <= centers[1],
"centers not sorted: {:?}",
centers
);
}
#[test]
fn wannier_centers_lie_in_unit_interval() {
let m = topological_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
for ikx in 0..8 {
let kx = 2.0 * PI * (ikx as f64) / 8.0;
let centers = wl.wannier_centers(kx, 16).unwrap();
for c in ¢ers {
assert!((0.0..1.0).contains(c), "center out of [0,1): {c}");
}
}
}
#[test]
fn polarization_p_y_finite_and_in_range() {
let m = trivial_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let py = wl.polarization_p_y(8, 16).unwrap();
assert!((0.0..1.0).contains(&py), "P_y out of range: {py}");
}
#[test]
fn polarization_p_x_finite_and_in_range() {
let m = trivial_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let px = wl.polarization_p_x(16, 8).unwrap();
assert!((0.0..1.0).contains(&px), "P_x out of range: {px}");
}
#[test]
fn nested_polarization_rejects_bad_wannier_idx() {
let m = topological_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
assert!(wl.nested_polarization(1, 8, 8).is_err());
}
#[test]
fn nested_polarization_trivial_near_zero_or_half() {
let m = trivial_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let np = wl.nested_polarization(0, 8, 8).unwrap();
let wrapped = (np + 0.5).rem_euclid(1.0) - 0.5;
assert!(
wrapped.abs() < 0.6,
"Nested polarisation out of plausible range: {np}"
);
}
#[test]
fn nested_polarization_quantised_modulo_half() {
let m = topological_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
let np = wl.nested_polarization(0, 8, 8).unwrap();
let wrapped = np.rem_euclid(1.0);
let dist_from_half_int = (wrapped * 2.0).round() / 2.0;
assert!(
(wrapped - dist_from_half_int).abs() < 0.3,
"Nested polarisation not near half-integer: {wrapped}"
);
}
#[test]
fn wilson_unitary_y_rejects_small_grid() {
let m = trivial_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
assert!(wl.wilson_unitary_y(0.0, 2).is_err());
}
#[test]
fn polarization_rejects_small_nkx() {
let m = trivial_two_band();
let wl = WilsonLoop::new(&m, vec![0]).unwrap();
assert!(wl.polarization_p_y(1, 8).is_err());
}
}