use std::f64::consts::PI;
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
use crate::topomagnon::wilson::{self, WilsonLoop};
const SQRT3: f64 = 1.732_050_808_568_877_3;
#[inline]
fn nn_phases(kx: f64, ky: f64) -> [Complex; 3] {
let p0 = Complex::ONE;
let p1 = Complex::new(0.0, kx).exp();
let p2 = Complex::new(0.0, kx * 0.5 + ky * SQRT3 * 0.5).exp();
[p0, p1, p2]
}
#[inline]
fn nn_phase_factor(kx: f64, ky: f64) -> Complex {
let [p0, p1, p2] = nn_phases(kx, ky);
p0.add(&p1).add(&p2)
}
#[inline]
fn nnn_soc_factor(kx: f64, ky: f64) -> f64 {
let phi1 = kx;
let phi2 = kx * 0.5 + ky * SQRT3 * 0.5;
2.0 * (phi1.sin() - phi2.sin() + (phi2 - phi1).sin())
}
#[inline]
fn honeycomb_reciprocal_vectors() -> ((f64, f64), (f64, f64)) {
let b1 = (2.0 * PI, -2.0 * PI / SQRT3);
let b2 = (0.0, 4.0 * PI / SQRT3);
(b1, b2)
}
#[allow(dead_code)]
#[inline]
fn honeycomb_trim_points() -> [(f64, f64); 4] {
let (b1, b2) = honeycomb_reciprocal_vectors();
[
(0.0, 0.0),
(b1.0 * 0.5, b1.1 * 0.5),
(b2.0 * 0.5, b2.1 * 0.5),
((b1.0 + b2.0) * 0.5, (b1.1 + b2.1) * 0.5),
]
}
#[inline]
fn rashba_ab_coupling(kx: f64, ky: f64, lambda_r: f64) -> Complex {
if lambda_r.abs() < 1e-15 {
return Complex::ZERO;
}
let [p0, p1, p2] = nn_phases(kx, ky);
let chi0 = Complex::new(-0.5, -SQRT3 * 0.5);
let chi1 = Complex::new(-0.5, SQRT3 * 0.5);
let chi2 = Complex::new(1.0, 0.0);
let sum = p0
.conj()
.mul(&chi0)
.add(&p1.conj().mul(&chi1))
.add(&p2.conj().mul(&chi2));
sum.scale(lambda_r).mul_i()
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct KaneMeleModel {
pub t: f64,
pub lambda_so: f64,
pub lambda_r: f64,
pub lambda_v: f64,
pub a_lattice: f64,
}
impl KaneMeleModel {
pub fn new(t: f64, lambda_so: f64, lambda_r: f64, lambda_v: f64) -> Result<Self> {
if t <= 0.0 {
return Err(error::invalid_param("t", "NN hopping must be positive"));
}
if !t.is_finite() {
return Err(error::invalid_param("t", "must be finite"));
}
if !lambda_so.is_finite() {
return Err(error::invalid_param("lambda_so", "must be finite"));
}
if !lambda_r.is_finite() {
return Err(error::invalid_param("lambda_r", "must be finite"));
}
if !lambda_v.is_finite() {
return Err(error::invalid_param("lambda_v", "must be finite"));
}
Ok(Self {
t,
lambda_so,
lambda_r,
lambda_v,
a_lattice: 2.46e-10,
})
}
pub fn graphene_with_soc(lambda_so: f64) -> Self {
Self {
t: 2.8,
lambda_so,
lambda_r: 0.0,
lambda_v: 0.0,
a_lattice: 2.46e-10,
}
}
pub fn topological_phase() -> Self {
Self {
t: 1.0,
lambda_so: 0.1,
lambda_r: 0.0,
lambda_v: 0.0,
a_lattice: 2.46e-10,
}
}
pub fn trivial_phase() -> Self {
Self {
t: 1.0,
lambda_so: 0.05,
lambda_r: 0.0,
lambda_v: 0.5,
a_lattice: 2.46e-10,
}
}
pub fn hamiltonian_at(&self, kx: f64, ky: f64) -> CMatrix {
let f_k = nn_phase_factor(kx, ky);
let g_k = nnn_soc_factor(kx, ky);
let r_ab = rashba_ab_coupling(kx, ky, self.lambda_r);
let s_ab = rashba_ab_coupling(-kx, -ky, self.lambda_r).neg();
let huu_00 = Complex::from_real(self.lambda_v + self.lambda_so * g_k);
let huu_11 = Complex::from_real(-self.lambda_v - self.lambda_so * g_k);
let huu_01 = f_k.conj().scale(-self.t);
let huu_10 = f_k.scale(-self.t);
let hdd_00 = Complex::from_real(self.lambda_v - self.lambda_so * g_k);
let hdd_11 = Complex::from_real(-self.lambda_v + self.lambda_so * g_k);
let hdd_01 = f_k.conj().scale(-self.t);
let hdd_10 = f_k.scale(-self.t);
let mut h = CMatrix::zeros(4);
h.set(0, 0, huu_00);
h.set(0, 1, huu_01);
h.set(1, 0, huu_10);
h.set(1, 1, huu_11);
h.set(2, 2, hdd_00);
h.set(2, 3, hdd_01);
h.set(3, 2, hdd_10);
h.set(3, 3, hdd_11);
h.set(0, 3, r_ab); h.set(1, 2, s_ab); h.set(2, 1, s_ab.conj()); h.set(3, 0, r_ab.conj());
h
}
pub fn energy_bands(&self, kx: f64, ky: f64) -> Result<Vec<f64>> {
let h = self.hamiltonian_at(kx, ky);
let (evals, _) = h.hermitian_eigendecomposition()?;
Ok(evals)
}
pub fn band_gap(&self) -> Result<f64> {
let n_grid = 30;
let mut min_gap = f64::INFINITY;
for ix in 0..n_grid {
let kx = -PI + 2.0 * PI * (ix as f64) / (n_grid as f64);
for iy in 0..n_grid {
let ky = -PI + 2.0 * PI * (iy as f64) / (n_grid as f64);
let evals = self.energy_bands(kx, ky)?;
let gap = evals[2] - evals[1];
if gap < min_gap {
min_gap = gap;
}
}
}
Ok(min_gap)
}
pub fn z2_invariant(&self) -> Result<i32> {
if self.lambda_r.abs() < 1e-12 {
self.z2_from_spin_chern()
} else {
self.z2_from_wilson_loop()
}
}
fn z2_from_spin_chern(&self) -> Result<i32> {
let ((b1x, b1y), (b2x, b2y)) = honeycomb_reciprocal_vectors();
let n = 30_usize; let mut states: Vec<Vec<[Complex; 2]>> = Vec::with_capacity(n + 1);
for ix in 0..=n {
let s = (ix as f64) / (n as f64);
let mut row = Vec::with_capacity(n + 1);
for iy in 0..=n {
let u = (iy as f64) / (n as f64);
let kx = s * b1x + u * b2x;
let ky = s * b1y + u * b2y;
let v = self.spin_up_lower_eigenvec(kx, ky)?;
row.push(v);
}
states.push(row);
}
let mut flux_sum = 0.0_f64;
for ix in 0..n {
let ix1 = ix + 1;
for iy in 0..n {
let iy1 = iy + 1;
let u_x = link_2(states[ix][iy], states[ix1][iy]);
let u_y = link_2(states[ix][iy], states[ix][iy1]);
let u_x_py = link_2(states[ix][iy1], states[ix1][iy1]);
let u_y_px = link_2(states[ix1][iy], states[ix1][iy1]);
let plaq = u_x.mul(&u_y_px).mul(&u_x_py.conj()).mul(&u_y.conj());
flux_sum += plaq.phase();
}
}
let chern_real = flux_sum / (2.0 * PI);
let chern_int = chern_real.round() as i32;
if (chern_real - chern_int as f64).abs() > 0.15 {
return Err(error::numerical_error(&format!(
"Spin Chern number not quantized: {chern_real:.4} (try parameter adjustment)"
)));
}
Ok(chern_int.abs() % 2)
}
fn spin_up_lower_eigenvec(&self, kx: f64, ky: f64) -> Result<[Complex; 2]> {
let g_k = nnn_soc_factor(kx, ky);
let f_k = nn_phase_factor(kx, ky);
let alpha = self.lambda_v + self.lambda_so * g_k; let beta = f_k.scale(-self.t); let beta_conj = f_k.conj().scale(-self.t);
let disc = (alpha * alpha + beta.norm_sq()).sqrt();
let e_lo = -disc;
let v = if beta.norm_sq() > 1e-28 {
let raw = [beta_conj.neg(), Complex::from_real(alpha - e_lo)];
let norm = (raw[0].norm_sq() + raw[1].norm_sq()).sqrt();
if norm < 1e-15 {
return Err(error::numerical_error(
"degenerate eigenvector in spin-up block",
));
}
[raw[0].scale(1.0 / norm), raw[1].scale(1.0 / norm)]
} else {
if alpha <= 0.0 {
[Complex::ONE, Complex::ZERO]
} else {
[Complex::ZERO, Complex::ONE]
}
};
Ok(v)
}
#[allow(dead_code)]
fn z2_from_trim_pfaffian(&self) -> Result<i32> {
let trim_points = honeycomb_trim_points();
let mut delta_product = 1.0_f64;
for &(kx, ky) in &trim_points {
let h = self.hamiltonian_at(kx, ky);
let (evals, vecs) = h.hermitian_eigendecomposition()?;
let u0: Vec<Complex> = (0..4).map(|r| vecs.get(r, 0)).collect();
let u1: Vec<Complex> = (0..4).map(|r| vecs.get(r, 1)).collect();
let gap_01 = evals[2] - evals[1];
if gap_01 < 1e-8 {
return Err(error::numerical_error(&format!(
"Band gap too small at TRIM ({kx:.3},{ky:.3}): gap={gap_01:.2e}. \
System may be at a topological phase transition."
)));
}
let theta_u0 = apply_time_reversal(&u0);
let theta_u1 = apply_time_reversal(&u1);
let m01 = inner_product(&u0, &theta_u1);
let m10 = inner_product(&u1, &theta_u0);
let pf = m01;
let det_m = m01.mul(&m10).neg();
let sqrt_det = det_m.re.abs().sqrt();
if sqrt_det < 1e-12 {
return Err(error::numerical_error(
"Sewing matrix singular at TRIM point; cannot compute Z₂",
));
}
let delta_re = pf.re / sqrt_det;
delta_product *= delta_re.signum();
}
Ok(if delta_product < 0.0 { 1 } else { 0 })
}
fn z2_from_wilson_loop(&self) -> Result<i32> {
self.z2_wilson_loop_at_resolution(120, 60)
}
fn z2_wilson_loop_at_resolution(&self, n_s: usize, n_u: usize) -> Result<i32> {
let (branch_a, branch_b) =
self.wilson_branches(n_s, n_u, |_psi: &mut Vec<Vec<Complex>>| {})?;
let last = branch_a.len() - 1;
let z2_primary = z2_parity_from_branches(&branch_a, &branch_b, 0);
let z2_other_trim = z2_parity_from_branches(&branch_a, &branch_b, last);
if z2_primary != z2_other_trim {
return Err(error::numerical_error(&format!(
"Z2 Wilson-loop self-check failed: TRIM-anchored references \
disagree (s=0 -> {z2_primary}, s=1/2 -> {z2_other_trim}) at \
n_s={n_s}, n_u={n_u} — likely too close to a transition."
)));
}
let (branch_a2, branch_b2) =
self.wilson_branches(2 * n_s, n_u, |_psi: &mut Vec<Vec<Complex>>| {})?;
let z2_doubled = z2_parity_from_branches(&branch_a2, &branch_b2, 0);
if z2_doubled != z2_primary {
return Err(error::numerical_error(&format!(
"Z2 Wilson-loop self-check failed: doubling s-resolution \
(n_s={n_s} -> {}) changes the answer ({z2_primary} -> \
{z2_doubled}) — likely too close to a transition.",
2 * n_s
)));
}
Ok(z2_primary)
}
fn wilson_branches<F>(
&self,
n_s: usize,
n_u: usize,
mut remix: F,
) -> Result<(Vec<f64>, Vec<f64>)>
where
F: FnMut(&mut Vec<Vec<Complex>>),
{
if n_s < 4 {
return Err(error::invalid_param(
"n_s",
"Wilson-loop Z2 needs at least 4 points along s",
));
}
if n_u < 4 {
return Err(error::invalid_param(
"n_u",
"Wilson-loop Z2 needs at least 4 points along u",
));
}
let (b1, b2) = honeycomb_reciprocal_vectors();
let mut branch_a = Vec::with_capacity(n_s + 1);
let mut branch_b = Vec::with_capacity(n_s + 1);
let mut prev_a = 0.0_f64;
let mut prev_b = 0.0_f64;
for i in 0..=n_s {
let s = i as f64 / (2.0 * n_s as f64);
let mut psis: Vec<Vec<Vec<Complex>>> = Vec::with_capacity(n_u);
for j in 0..n_u {
let u = j as f64 / n_u as f64;
let kx = s * b1.0 + u * b2.0;
let ky = s * b1.1 + u * b2.1;
let h = self.hamiltonian_at(kx, ky);
let (_, vecs) = h.hermitian_eigendecomposition()?;
let mut psi = vec![vecs.column(0), vecs.column(1)];
remix(&mut psi);
psis.push(psi);
}
let mut w = CMatrix::eye(2);
for j in 0..n_u {
let jp1 = (j + 1) % n_u;
let raw = WilsonLoop::link_matrix(&psis[j], &psis[jp1]);
let link = wilson::polar_unitary(&raw)?;
w = w.matmul(&link)?;
}
let (th1, th2) = wilson::unitary_2x2_eigenphases_exact(&w);
if i == 0 {
branch_a.push(th1);
branch_b.push(th2);
prev_a = th1;
prev_b = th2;
} else {
let cost_same = circular_distance(prev_a, th1) + circular_distance(prev_b, th2);
let cost_swap = circular_distance(prev_a, th2) + circular_distance(prev_b, th1);
let (next_a, next_b) = if cost_same <= cost_swap {
(th1, th2)
} else {
(th2, th1)
};
branch_a.push(next_a);
branch_b.push(next_b);
prev_a = next_a;
prev_b = next_b;
}
}
Ok((branch_a, branch_b))
}
pub fn is_topological(&self) -> bool {
self.z2_invariant().is_ok_and(|z2| z2 == 1)
}
pub fn edge_spectrum(
&self,
n_cells: usize,
kx_min: f64,
kx_max: f64,
n_kx: usize,
) -> Result<Vec<(f64, Vec<f64>)>> {
if n_cells < 2 {
return Err(error::invalid_param(
"n_cells",
"strip width must be at least 2",
));
}
let dim = n_cells * 4;
if dim > CMatrix::MAX_DIM {
return Err(error::invalid_param(
"n_cells",
"n_cells * 4 exceeds CMatrix::MAX_DIM (64); use n_cells ≤ 16",
));
}
if n_kx < 2 {
return Err(error::invalid_param("n_kx", "need at least 2 kx points"));
}
let delta_ky = PI / (n_cells.max(4) as f64);
let mut result = Vec::with_capacity(n_kx);
for i in 0..n_kx {
let kx = if n_kx == 1 {
kx_min
} else {
kx_min + (kx_max - kx_min) * (i as f64) / ((n_kx - 1) as f64)
};
let h_strip = self.build_strip_hamiltonian(kx, n_cells, delta_ky)?;
let (evals, _) = h_strip.hermitian_eigendecomposition()?;
result.push((kx, evals));
}
Ok(result)
}
fn build_strip_hamiltonian(&self, kx: f64, n_cells: usize, delta_ky: f64) -> Result<CMatrix> {
let nb = 4; let dim = n_cells * nb;
let h_intra = self.hamiltonian_at(kx, 0.0);
let h_at_dky = self.hamiltonian_at(kx, delta_ky);
let mut h_strip = CMatrix::zeros(dim);
for iy in 0..n_cells {
for i in 0..nb {
for j in 0..nb {
let row = iy * nb + i;
let col = iy * nb + j;
let cur = h_strip.get(row, col);
h_strip.set(row, col, cur.add(&h_intra.get(i, j)));
}
}
}
for iy in 0..(n_cells - 1) {
for i in 0..nb {
for j in 0..nb {
let t_ij = {
let dh = h_at_dky.get(i, j).sub(&h_intra.get(i, j));
dh.scale(0.5)
};
let r_up = iy * nb + i;
let c_up = (iy + 1) * nb + j;
let cur_up = h_strip.get(r_up, c_up);
h_strip.set(r_up, c_up, cur_up.add(&t_ij));
let r_dn = (iy + 1) * nb + i;
let c_dn = iy * nb + j;
let cur_dn = h_strip.get(r_dn, c_dn);
h_strip.set(r_dn, c_dn, cur_dn.add(&t_ij.conj()));
}
}
}
Ok(h_strip)
}
}
#[allow(dead_code)]
fn apply_time_reversal(u: &[Complex]) -> Vec<Complex> {
debug_assert_eq!(u.len(), 4);
vec![
u[2].conj(),
u[3].conj(),
u[0].conj().neg(),
u[1].conj().neg(),
]
}
#[inline]
fn wrap_to_pm_pi(x: f64) -> f64 {
(x + PI).rem_euclid(2.0 * PI) - PI
}
#[inline]
fn circular_distance(a: f64, b: f64) -> f64 {
wrap_to_pm_pi(a - b).abs()
}
#[inline]
fn short_arc_crosses(a: f64, b: f64, g: f64) -> bool {
let d = wrap_to_pm_pi(b - a);
let gp = wrap_to_pm_pi(g - a);
if d > 0.0 {
gp > 0.0 && gp <= d
} else if d < 0.0 {
gp >= d && gp < 0.0
} else {
false
}
}
#[inline]
fn gap_reference_angle(a: f64, b: f64, use_larger: bool) -> f64 {
let two_pi = 2.0 * PI;
let a = a.rem_euclid(two_pi);
let b = b.rem_euclid(two_pi);
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
let gap_inside = hi - lo;
let gap_outside = two_pi - gap_inside;
let inside_is_larger = gap_inside >= gap_outside;
let inside_mid = (lo + hi) * 0.5;
if inside_is_larger == use_larger {
inside_mid.rem_euclid(two_pi)
} else {
(inside_mid + PI).rem_euclid(two_pi)
}
}
fn z2_parity_from_branches(branch_a: &[f64], branch_b: &[f64], at_index: usize) -> i32 {
let g = gap_reference_angle(branch_a[at_index], branch_b[at_index], true);
let mut crossings: u32 = 0;
for w in branch_a.windows(2) {
if short_arc_crosses(w[0], w[1], g) {
crossings += 1;
}
}
for w in branch_b.windows(2) {
if short_arc_crosses(w[0], w[1], g) {
crossings += 1;
}
}
(crossings % 2) as i32
}
#[allow(dead_code)]
fn inner_product(a: &[Complex], b: &[Complex]) -> Complex {
debug_assert_eq!(a.len(), b.len());
a.iter()
.zip(b.iter())
.map(|(ai, bi)| ai.conj().mul(bi))
.fold(Complex::ZERO, |acc, x| acc.add(&x))
}
#[inline]
fn link_2(a: [Complex; 2], b: [Complex; 2]) -> Complex {
let inner = a[0].conj().mul(&b[0]).add(&a[1].conj().mul(&b[1]));
let norm = inner.norm();
if norm < 1e-15 {
Complex::ONE
} else {
inner.scale(1.0 / norm)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn topological_phase_has_positive_gap() {
let model = KaneMeleModel::topological_phase();
let gap = model.band_gap().unwrap();
assert!(
gap > 0.0,
"Topological phase should have positive band gap, got {gap}"
);
}
#[test]
fn trivial_phase_has_positive_gap() {
let model = KaneMeleModel::trivial_phase();
let gap = model.band_gap().unwrap();
assert!(
gap > 0.0,
"Trivial phase should also have positive band gap, got {gap}"
);
}
#[test]
fn topological_and_trivial_differ_in_z2() {
let topo = KaneMeleModel::topological_phase();
let trivial = KaneMeleModel::trivial_phase();
let z2_topo = topo.z2_invariant().unwrap();
let z2_trivial = trivial.z2_invariant().unwrap();
assert_ne!(
z2_topo, z2_trivial,
"Topological (Z₂={z2_topo}) and trivial (Z₂={z2_trivial}) should differ"
);
}
#[test]
fn hamiltonian_is_hermitian() {
let models = [
KaneMeleModel::new(1.0, 0.1, 0.0, 0.0).unwrap(),
KaneMeleModel::new(1.0, 0.1, 0.05, 0.1).unwrap(),
KaneMeleModel::new(2.8, 0.05, 0.02, 0.0).unwrap(),
];
let kpoints = [(0.0, 0.0), (0.5, 0.8), (-1.2, 1.7)];
for model in &models {
for &(kx, ky) in &kpoints {
let h = model.hamiltonian_at(kx, ky);
let hd = h.conj_transpose();
let diff = h.sub(&hd).unwrap();
assert!(
diff.frobenius_norm() < 1e-12,
"H not Hermitian at k=({kx},{ky}): ||H-H†||={:.2e}",
diff.frobenius_norm()
);
}
}
}
#[test]
fn energy_bands_returns_four_sorted_values() {
let model = KaneMeleModel::topological_phase();
let evals = model.energy_bands(0.3, 0.7).unwrap();
assert_eq!(evals.len(), 4, "Must have exactly 4 bands");
for w in evals.windows(2) {
assert!(w[0] <= w[1] + 1e-10, "Eigenvalues not sorted: {evals:?}");
}
}
#[test]
fn gamma_point_kramers_degeneracy() {
let model = KaneMeleModel::topological_phase();
let evals = model.energy_bands(0.0, 0.0).unwrap();
assert_eq!(evals.len(), 4);
assert!(
approx(evals[0], evals[1], 1e-10),
"Γ: Kramers pair 1 not degenerate: {} vs {}",
evals[0],
evals[1]
);
assert!(
approx(evals[2], evals[3], 1e-10),
"Γ: Kramers pair 2 not degenerate: {} vs {}",
evals[2],
evals[3]
);
}
#[test]
fn is_topological_classifies_correctly() {
let topo = KaneMeleModel::topological_phase();
let trivial = KaneMeleModel::trivial_phase();
assert!(
topo.is_topological(),
"topological_phase() should be classified as topological"
);
assert!(
!trivial.is_topological(),
"trivial_phase() should be classified as trivial"
);
}
#[test]
fn gap_decreases_as_lambda_v_increases() {
let lso = 0.1;
let lv_values = [0.0, 0.2, 0.4];
let mut gaps = Vec::new();
for &lv in &lv_values {
let m = KaneMeleModel::new(1.0, lso, 0.0, lv).unwrap();
gaps.push(m.band_gap().unwrap());
}
assert!(
gaps[0] > gaps[1],
"Gap should decrease with λ_v: gap(0.0)={} > gap(0.2)={} failed",
gaps[0],
gaps[1]
);
assert!(
gaps[1] > gaps[2],
"Gap should decrease with λ_v: gap(0.2)={} > gap(0.4)={} failed",
gaps[1],
gaps[2]
);
}
#[test]
fn graphene_no_soc_is_gapless() {
let model = KaneMeleModel::graphene_with_soc(0.0);
let k_x = 4.0 * PI / 3.0;
let evals = model.energy_bands(k_x, 0.0).unwrap();
let gap_at_k = evals[2] - evals[1];
assert!(
gap_at_k < 1e-6,
"Graphene without SOC: gap at K should be ~0, got {gap_at_k}"
);
}
#[test]
fn graphene_with_soc_opens_gap() {
let model = KaneMeleModel::graphene_with_soc(0.1);
let gap = model.band_gap().unwrap();
assert!(
gap > 0.0,
"SOC should open a band gap in graphene, got gap={gap}"
);
assert!(
gap > 0.1,
"Gap with λ_SO=0.1 should be substantial (>0.1 eV), got {gap}"
);
}
#[test]
fn edge_spectrum_correct_kx_count() {
let model = KaneMeleModel::topological_phase();
let n_kx = 15;
let n_cells = 6;
let result = model.edge_spectrum(n_cells, -PI, PI, n_kx).unwrap();
assert_eq!(
result.len(),
n_kx,
"Expected {n_kx} kx points, got {}",
result.len()
);
let expected_bands = 4 * n_cells;
for (kx, bands) in &result {
assert_eq!(
bands.len(),
expected_bands,
"At kx={kx:.3}: expected {expected_bands} bands, got {}",
bands.len()
);
}
}
#[test]
fn hamiltonian_is_bz_periodic() {
let model = KaneMeleModel::new(1.0, 0.1, 0.03, 0.05).unwrap();
let kx_test = 0.4;
let ky_test = 0.3;
let b1x = 2.0 * PI;
let b1y = -2.0 * PI / SQRT3;
let h_k = model.hamiltonian_at(kx_test, ky_test);
let h_kpb1 = model.hamiltonian_at(kx_test + b1x, ky_test + b1y);
let diff1 = h_k.sub(&h_kpb1).unwrap();
assert!(
diff1.frobenius_norm() < 1e-10,
"b₁ periodicity violated: ||H(k) - H(k+b₁)||={:.2e}",
diff1.frobenius_norm()
);
let b2x = 0.0;
let b2y = 4.0 * PI / SQRT3;
let h_kpb2 = model.hamiltonian_at(kx_test + b2x, ky_test + b2y);
let diff2 = h_k.sub(&h_kpb2).unwrap();
assert!(
diff2.frobenius_norm() < 1e-10,
"b₂ periodicity violated: ||H(k) - H(k+b₂)||={:.2e}",
diff2.frobenius_norm()
);
}
#[test]
fn time_reversal_symmetry() {
let model = KaneMeleModel::new(1.0, 0.08, 0.0, 0.06).unwrap();
let test_points = [(0.5, 0.3), (-0.7, 0.9), (1.1, -0.4)];
for &(kx, ky) in &test_points {
let h_k = model.hamiltonian_at(kx, ky);
let h_mk = model.hamiltonian_at(-kx, -ky);
let mut h_conj = CMatrix::zeros(4);
for i in 0..4 {
for j in 0..4 {
h_conj.set(i, j, h_k.get(i, j).conj());
}
}
let theta_sign: [f64; 4] = [1.0, 1.0, -1.0, -1.0];
let theta_map: [usize; 4] = [2, 3, 0, 1];
let mut thr = CMatrix::zeros(4);
for i in 0..4 {
for j in 0..4 {
let ai = theta_map[i];
let bj = theta_map[j];
let val = h_conj.get(ai, bj).scale(theta_sign[i] * theta_sign[j]);
thr.set(i, j, val);
}
}
let diff = thr.sub(&h_mk).unwrap();
assert!(
diff.frobenius_norm() < 1e-10,
"TRS violated at k=({kx},{ky}): ||Θ H(k)* Θ^T - H(-k)||={:.2e}",
diff.frobenius_norm()
);
}
}
#[test]
fn time_reversal_symmetry_holds_with_rashba() {
let test_points = [(0.5, 0.3), (-0.7, 0.9), (1.1, -0.4), (2.0, 0.6)];
let theta_sign: [f64; 4] = [1.0, 1.0, -1.0, -1.0];
let theta_map: [usize; 4] = [2, 3, 0, 1];
for &lambda_r in &[0.02, 0.05, 0.2] {
let model = KaneMeleModel::new(1.0, 0.08, lambda_r, 0.06).unwrap();
for &(kx, ky) in &test_points {
let h_k = model.hamiltonian_at(kx, ky);
let h_mk = model.hamiltonian_at(-kx, -ky);
let mut h_conj = CMatrix::zeros(4);
for i in 0..4 {
for j in 0..4 {
h_conj.set(i, j, h_k.get(i, j).conj());
}
}
let mut thr = CMatrix::zeros(4);
for i in 0..4 {
for j in 0..4 {
let ai = theta_map[i];
let bj = theta_map[j];
let val = h_conj.get(ai, bj).scale(theta_sign[i] * theta_sign[j]);
thr.set(i, j, val);
}
}
let diff = thr.sub(&h_mk).unwrap();
assert!(
diff.frobenius_norm() < 1e-10,
"TRS violated at k=({kx},{ky}), λ_R={lambda_r}: \
||Θ H(k)* Θ^T - H(-k)||={:.2e}",
diff.frobenius_norm()
);
}
}
}
#[test]
fn critical_rashba_ratio_closes_gap_at_dirac_point() {
let lambda_so = 0.1;
let lambda_r_crit = 2.0 * 3.0_f64.sqrt() * lambda_so;
let model = KaneMeleModel::new(1.0, lambda_so, lambda_r_crit, 0.0).unwrap();
let k_x = 4.0 * PI / 3.0;
let evals = model.energy_bands(k_x, 0.0).unwrap();
let gap_at_k = evals[2] - evals[1];
assert!(
gap_at_k < 1e-6,
"Gap at K should close at the critical Rashba ratio λ_R=2√3·λ_SO, got {gap_at_k:.3e}"
);
}
fn random_u2_matrix(rng: &mut crate::frustrated::lattice::Xorshift64) -> CMatrix {
let mut raw = CMatrix::zeros(2);
for i in 0..2 {
for j in 0..2 {
let re = 2.0 * rng.next_f64() - 1.0;
let im = 2.0 * rng.next_f64() - 1.0;
raw.set(i, j, Complex::new(re, im));
}
}
wilson::polar_unitary(&raw).unwrap()
}
fn remix_psi_columns(psi: &mut [Vec<Complex>], v: &CMatrix) {
let old0 = psi[0].clone();
let old1 = psi[1].clone();
for row in 0..old0.len() {
psi[0][row] = old0[row]
.mul(&v.get(0, 0))
.add(&old1[row].mul(&v.get(1, 0)));
psi[1][row] = old0[row]
.mul(&v.get(0, 1))
.add(&old1[row].mul(&v.get(1, 1)));
}
}
#[test]
fn honeycomb_trim_points_are_kramers_degenerate() {
let model = KaneMeleModel::new(1.0, 0.08, 0.05, 0.06).unwrap();
for &(kx, ky) in &honeycomb_trim_points() {
let evals = model.energy_bands(kx, ky).unwrap();
assert!(
approx(evals[0], evals[1], 1e-9),
"TRIM ({kx:.4},{ky:.4}): Kramers pair 1 not degenerate: {} vs {}",
evals[0],
evals[1]
);
assert!(
approx(evals[2], evals[3], 1e-9),
"TRIM ({kx:.4},{ky:.4}): Kramers pair 2 not degenerate: {} vs {}",
evals[2],
evals[3]
);
}
}
#[test]
fn apply_time_reversal_is_genuinely_antiunitary() {
let test_vectors: [[Complex; 4]; 3] = [
[
Complex::new(0.3, 0.1),
Complex::new(-0.2, 0.4),
Complex::new(0.1, -0.5),
Complex::new(0.6, 0.2),
],
[Complex::ONE, Complex::ZERO, Complex::ZERO, Complex::ZERO],
[
Complex::new(0.5, 0.5),
Complex::new(-0.5, 0.5),
Complex::new(0.5, -0.5),
Complex::new(-0.5, -0.5),
],
];
for v in &test_vectors {
let theta_v = apply_time_reversal(v);
let theta2_v = apply_time_reversal(&theta_v);
for i in 0..4 {
let expected = v[i].neg();
let diff = theta2_v[i].sub(&expected);
assert!(
diff.norm() < 1e-14,
"Theta^2 != -1 at component {i}: got {:?}, expected {:?}",
theta2_v[i],
expected
);
}
}
let model = KaneMeleModel::new(1.0, 0.08, 0.05, 0.06).unwrap();
let mut rng = crate::frustrated::lattice::Xorshift64::new(12345).unwrap();
for &(kx, ky) in &honeycomb_trim_points() {
let h = model.hamiltonian_at(kx, ky);
let (_, vecs) = h.hermitian_eigendecomposition().unwrap();
let u0: Vec<Complex> = (0..4).map(|r| vecs.get(r, 0)).collect();
let u1: Vec<Complex> = (0..4).map(|r| vecs.get(r, 1)).collect();
for _ in 0..5 {
let v = random_u2_matrix(&mut rng);
let u0p: Vec<Complex> = (0..4)
.map(|r| u0[r].mul(&v.get(0, 0)).add(&u1[r].mul(&v.get(1, 0))))
.collect();
let u1p: Vec<Complex> = (0..4)
.map(|r| u0[r].mul(&v.get(0, 1)).add(&u1[r].mul(&v.get(1, 1))))
.collect();
let theta_u0p = apply_time_reversal(&u0p);
let theta_u1p = apply_time_reversal(&u1p);
let m00 = inner_product(&u0p, &theta_u0p);
let m11 = inner_product(&u1p, &theta_u1p);
let m01 = inner_product(&u0p, &theta_u1p);
let m10 = inner_product(&u1p, &theta_u0p);
assert!(
m00.norm() < 1e-9,
"m00 not ~0 at TRIM ({kx:.3},{ky:.3}): {m00:?}"
);
assert!(
m11.norm() < 1e-9,
"m11 not ~0 at TRIM ({kx:.3},{ky:.3}): {m11:?}"
);
let sum = m01.add(&m10);
assert!(
sum.norm() < 1e-9,
"sewing matrix not antisymmetric at TRIM ({kx:.3},{ky:.3}): \
m01={m01:?} m10={m10:?}"
);
}
}
}
#[test]
fn z2_wilson_loop_matches_textbook_phase_diagram() {
let lambda_so = 0.1;
let lambda_v_crit = 3.0 * 3.0_f64.sqrt() * lambda_so;
let lambda_r = 0.05;
let sweep = [0.02, 0.1, 0.2, 0.3, 0.4, 0.45, 0.55, 0.65, 0.8, 1.2, 2.0];
for &lambda_v in &sweep {
let model = KaneMeleModel::new(1.0, lambda_so, lambda_r, lambda_v).unwrap();
let z2 = model.z2_invariant().unwrap();
let expected = if lambda_v < lambda_v_crit { 1 } else { 0 };
assert_eq!(
z2, expected,
"λ_v={lambda_v}: expected Z2={expected} (critical={lambda_v_crit:.4}), got {z2}"
);
let model_r0 = KaneMeleModel::new(1.0, lambda_so, 0.0, lambda_v).unwrap();
let z2_ref = model_r0.z2_from_spin_chern().unwrap();
assert_eq!(
z2_ref, expected,
"λ_v={lambda_v}: spin-Chern reference disagrees with textbook expectation"
);
}
}
#[test]
fn z2_wilson_loop_gauge_invariant_under_degenerate_remix() {
use crate::frustrated::lattice::Xorshift64;
let n_s = 40;
let n_u = 24;
for &(lambda_v, lambda_r, label) in &[(0.1, 0.05, "topological"), (0.8, 0.05, "trivial")] {
let model = KaneMeleModel::new(1.0, 0.1, lambda_r, lambda_v).unwrap();
let (clean_a, clean_b) = model
.wilson_branches(n_s, n_u, |_psi: &mut Vec<Vec<Complex>>| {})
.unwrap();
let z2_clean = z2_parity_from_branches(&clean_a, &clean_b, 0);
for seed in 1..=8u64 {
let mut rng = Xorshift64::new(seed).unwrap();
let (remixed_a, remixed_b) = model
.wilson_branches(n_s, n_u, |psi: &mut Vec<Vec<Complex>>| {
let v = random_u2_matrix(&mut rng);
remix_psi_columns(psi, &v);
})
.unwrap();
let z2_remixed = z2_parity_from_branches(&remixed_a, &remixed_b, 0);
assert_eq!(
z2_remixed, z2_clean,
"{label} point (λ_v={lambda_v}, λ_R={lambda_r}) not gauge \
invariant at seed={seed}: clean Z2={z2_clean}, remixed Z2={z2_remixed}"
);
}
}
}
#[test]
fn unitary_2x2_eigenphases_exact_matches_planted_eigenvalues() {
use crate::frustrated::lattice::Xorshift64;
let mut rng = Xorshift64::new(777).unwrap();
for _ in 0..20 {
let v = random_u2_matrix(&mut rng);
let theta1 = (rng.next_f64() - 0.5) * 2.0 * PI;
let theta2 = (rng.next_f64() - 0.5) * 2.0 * PI;
let mut d = CMatrix::zeros(2);
d.set(0, 0, Complex::from_polar(1.0, theta1));
d.set(1, 1, Complex::from_polar(1.0, theta2));
let vd = v.matmul(&d).unwrap();
let w = vd.matmul(&v.conj_transpose()).unwrap();
let (extracted1, extracted2) = wilson::unitary_2x2_eigenphases_exact(&w);
let (p1, p2) = (wrap_to_pm_pi(theta1), wrap_to_pm_pi(theta2));
let (e1, e2) = (wrap_to_pm_pi(extracted1), wrap_to_pm_pi(extracted2));
let matches_direct = (p1 - e1).abs() < 1e-9 && (p2 - e2).abs() < 1e-9;
let matches_swapped = (p1 - e2).abs() < 1e-9 && (p2 - e1).abs() < 1e-9;
assert!(
matches_direct || matches_swapped,
"planted (θ1={theta1:.6},θ2={theta2:.6}) -> wrapped ({p1:.6},{p2:.6}), \
extracted ({extracted1:.6},{extracted2:.6}) -> wrapped ({e1:.6},{e2:.6})"
);
}
}
#[test]
fn z2_wilson_loop_errors_rather_than_guesses_near_transition() {
let lambda_so = 0.1;
let lambda_v_crit = 3.0 * 3.0_f64.sqrt() * lambda_so;
let lambda_v = lambda_v_crit - 0.01;
let model = KaneMeleModel::new(1.0, lambda_so, 1e-9, lambda_v).unwrap();
let result = model.z2_wilson_loop_at_resolution(10, 60);
assert!(
result.is_err(),
"Expected an Err near the phase transition at low resolution, got {result:?}"
);
}
}