use std::f64::consts::PI;
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
#[derive(Debug, Clone, PartialEq)]
pub enum HotiLattice {
Bbh,
BreathingKagomeTrivial,
BreathingKagomeTopological,
}
#[inline]
fn sigma_x() -> CMatrix {
let mut m = CMatrix::zeros(2);
m.set(0, 1, Complex::ONE);
m.set(1, 0, Complex::ONE);
m
}
#[inline]
fn sigma_y() -> CMatrix {
let mut m = CMatrix::zeros(2);
m.set(0, 1, Complex::new(0.0, -1.0));
m.set(1, 0, Complex::new(0.0, 1.0));
m
}
#[inline]
fn sigma_z() -> CMatrix {
let mut m = CMatrix::zeros(2);
m.set(0, 0, Complex::ONE);
m.set(1, 1, Complex::new(-1.0, 0.0));
m
}
fn kron_2x2(a: &CMatrix, b: &CMatrix) -> CMatrix {
let mut out = CMatrix::zeros(4);
for i in 0..2 {
for j in 0..2 {
let a_ij = a.get(i, j);
for k in 0..2 {
for l in 0..2 {
let val = a_ij.mul(&b.get(k, l));
out.set(2 * i + k, 2 * j + l, val);
}
}
}
}
out
}
#[derive(Debug, Clone)]
pub struct BbhModel {
pub lambda_x: f64,
pub gamma_x: f64,
pub lambda_y: f64,
pub gamma_y: f64,
}
impl BbhModel {
pub fn new(lambda_x: f64, gamma_x: f64, lambda_y: f64, gamma_y: f64) -> Result<Self> {
for (name, v) in [
("lambda_x", lambda_x),
("gamma_x", gamma_x),
("lambda_y", lambda_y),
("gamma_y", gamma_y),
] {
if !v.is_finite() {
return Err(error::invalid_param(name, "must be finite"));
}
}
Ok(Self {
lambda_x,
gamma_x,
lambda_y,
gamma_y,
})
}
pub fn topological_phase() -> Self {
Self {
lambda_x: 0.5,
gamma_x: 1.0,
lambda_y: 0.5,
gamma_y: 1.0,
}
}
pub fn trivial_phase() -> Self {
Self {
lambda_x: 1.0,
gamma_x: 0.5,
lambda_y: 1.0,
gamma_y: 0.5,
}
}
pub fn gamma_matrices() -> [CMatrix; 4] {
let sx = sigma_x();
let sy = sigma_y();
let sz = sigma_z();
let id2 = CMatrix::eye(2);
[
kron_2x2(&sx, &sz), kron_2x2(&sy, &id2), kron_2x2(&sx, &sx), kron_2x2(&sx, &sy), ]
}
pub fn hamiltonian_at(&self, kx: f64, ky: f64) -> Result<CMatrix> {
let [g1, g2, g3, g4] = Self::gamma_matrices();
let ax = self.lambda_x + self.gamma_x * kx.cos();
let bx = self.gamma_x * kx.sin();
let ay = self.lambda_y + self.gamma_y * ky.cos();
let by_ = self.gamma_y * ky.sin();
let h = g1
.scale_real(ax)
.add(&g2.scale_real(bx))?
.add(&g3.scale_real(ay))?
.add(&g4.scale_real(by_))?;
Ok(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, nkx: usize, nky: usize) -> f64 {
let mut min_positive = f64::INFINITY;
let nkx = nkx.max(4);
let nky = nky.max(4);
for ix in 0..nkx {
let kx = 2.0 * PI * ((ix as f64) + 0.5) / (nkx as f64);
for iy in 0..nky {
let ky = 2.0 * PI * ((iy as f64) + 0.5) / (nky as f64);
if let Ok(e) = self.energy_bands(kx, ky) {
let pos = e[2];
if pos < min_positive {
min_positive = pos;
}
}
}
}
if min_positive.is_infinite() {
0.0
} else {
min_positive
}
}
pub fn is_higher_order_topological(&self) -> bool {
self.gamma_x.abs() > self.lambda_x.abs() && self.gamma_y.abs() > self.lambda_y.abs()
}
pub fn quadrupole_moment(&self, nkx: usize, nky: usize) -> Result<f64> {
if nkx < 4 {
return Err(error::invalid_param("nkx", "need at least 4 kx points"));
}
if nky < 4 {
return Err(error::invalid_param("nky", "need at least 4 ky points"));
}
let occupied = [0usize, 1usize];
let mut w_nested = Complex::ONE;
for ikx in 0..nkx {
let kx = 2.0 * PI * (ikx as f64) / (nkx as f64);
let mut all_states: Vec<[Vec<Complex>; 2]> = Vec::with_capacity(nky + 1);
for iky in 0..=nky {
let ky = 2.0 * PI * (iky as f64) / (nky as f64);
let h = self.hamiltonian_at(kx, ky)?;
let (_, vecs) = h.hermitian_eigendecomposition()?;
let s0 = vecs.column(occupied[0]);
let s1 = vecs.column(occupied[1]);
all_states.push([s0, s1]);
}
let mut w_y = CMatrix::eye(2);
for iky in 0..nky {
let sa = &all_states[iky];
let sb = &all_states[iky + 1];
let mut m = CMatrix::zeros(2);
for (mu, sa_row) in sa.iter().enumerate().take(2) {
for (nu, sb_row) in sb.iter().enumerate().take(2) {
let mut inner = Complex::ZERO;
for j in 0..sa_row.len() {
inner = inner.add(&sa_row[j].conj().mul(&sb_row[j]));
}
m.set(mu, nu, inner);
}
}
w_y = w_y.matmul(&m)?;
}
let det = w_y
.get(0, 0)
.mul(&w_y.get(1, 1))
.sub(&w_y.get(0, 1).mul(&w_y.get(1, 0)));
let nu = det.phase() / (2.0 * PI);
let phase = Complex::from_polar(1.0, 2.0 * PI * nu);
w_nested = w_nested.mul(&phase);
}
let q_xy = w_nested.phase() / (2.0 * PI);
Ok(q_xy)
}
}
#[derive(Debug, Clone)]
pub struct BreathingKagomeModel {
pub t_intra: f64,
pub t_inter: f64,
pub m_mass: f64,
}
impl BreathingKagomeModel {
pub fn new(t_intra: f64, t_inter: f64, m_mass: f64) -> Result<Self> {
for (name, v) in [
("t_intra", t_intra),
("t_inter", t_inter),
("m_mass", m_mass),
] {
if !v.is_finite() {
return Err(error::invalid_param(name, "must be finite"));
}
}
Ok(Self {
t_intra,
t_inter,
m_mass,
})
}
pub fn topological_kagome() -> Self {
Self {
t_intra: 0.5,
t_inter: 1.0,
m_mass: 0.0,
}
}
pub fn trivial_kagome() -> Self {
Self {
t_intra: 1.0,
t_inter: 0.5,
m_mass: 0.0,
}
}
pub fn hamiltonian_at(&self, kx: f64, ky: f64) -> CMatrix {
let a2x = 0.5_f64; let a2y = (3.0_f64).sqrt() * 0.5;
let phi1 = kx; let phi2 = kx * a2x + ky * a2y; let phi3 = phi2 - phi1;
let p1 = Complex::from_polar(1.0, phi1);
let p2 = Complex::from_polar(1.0, phi2);
let p3 = Complex::from_polar(1.0, phi3);
let t = Complex::from_real(self.t_intra);
let ti = Complex::from_real(self.t_inter);
let h01 = t.add(&ti.mul(&p1));
let h02 = t.add(&ti.mul(&p2));
let h12 = t.add(&ti.mul(&p3));
let mut mat = CMatrix::zeros(3);
let m = Complex::from_real(self.m_mass);
mat.set(0, 0, m);
mat.set(1, 1, m);
mat.set(2, 2, m);
mat.set(0, 1, h01);
mat.set(1, 0, h01.conj());
mat.set(0, 2, h02);
mat.set(2, 0, h02.conj());
mat.set(1, 2, h12);
mat.set(2, 1, h12.conj());
mat
}
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 is_topological(&self) -> bool {
self.t_inter.abs() > self.t_intra.abs()
}
pub fn corner_polarization(&self, nkx: usize, nky: usize) -> Result<f64> {
if nkx < 4 {
return Err(error::invalid_param("nkx", "need at least 4 kx points"));
}
if nky < 4 {
return Err(error::invalid_param("nky", "need at least 4 ky points"));
}
let mut sum = 0.0_f64;
for ikx in 0..nkx {
let kx = 2.0 * PI * (ikx as f64) / (nkx as f64);
let mut w_scalar = Complex::ONE;
let mut prev_state: Option<Vec<Complex>> = None;
for iky in 0..=nky {
let ky = 2.0 * PI * (iky as f64) / (nky as f64);
let h = self.hamiltonian_at(kx, ky);
let (_, vecs) = h.hermitian_eigendecomposition()?;
let state = vecs.column(0);
if let Some(ref prev) = prev_state {
let mut inner = Complex::ZERO;
for j in 0..prev.len() {
inner = inner.add(&prev[j].conj().mul(&state[j]));
}
let norm = inner.norm();
let link = if norm < 1e-15 {
Complex::ONE
} else {
inner.scale(1.0 / norm)
};
w_scalar = w_scalar.mul(&link);
}
prev_state = Some(state);
}
let nu = w_scalar.phase() / (2.0 * PI);
let nu_mapped = nu.rem_euclid(1.0);
sum += nu_mapped;
}
Ok(sum / (nkx as f64))
}
}
#[derive(Debug, Clone)]
pub struct CornerStateSolver {
pub lx: usize,
pub ly: usize,
pub lambda_x: f64,
pub gamma_x: f64,
pub lambda_y: f64,
pub gamma_y: f64,
}
impl CornerStateSolver {
const N_ORB: usize = 4;
pub fn new(
lx: usize,
ly: usize,
lambda_x: f64,
gamma_x: f64,
lambda_y: f64,
gamma_y: f64,
) -> Result<Self> {
if lx == 0 || ly == 0 {
return Err(error::invalid_param("lx/ly", "must be at least 1"));
}
let dim = lx * ly * Self::N_ORB;
if dim > CMatrix::MAX_DIM {
return Err(error::invalid_param(
"lx*ly",
&format!("cluster dimension {dim} = {lx}×{ly}×4 exceeds CMatrix::MAX_DIM=64"),
));
}
for (name, v) in [
("lambda_x", lambda_x),
("gamma_x", gamma_x),
("lambda_y", lambda_y),
("gamma_y", gamma_y),
] {
if !v.is_finite() {
return Err(error::invalid_param(name, "must be finite"));
}
}
Ok(Self {
lx,
ly,
lambda_x,
gamma_x,
lambda_y,
gamma_y,
})
}
#[inline]
fn site(&self, ix: usize, iy: usize, alpha: usize) -> usize {
(iy * self.lx + ix) * Self::N_ORB + alpha
}
pub fn build_cluster_hamiltonian(&self) -> Result<CMatrix> {
let dim = self.lx * self.ly * Self::N_ORB;
let mut h_full = CMatrix::zeros(dim);
let [g1, g2, g3, g4] = BbhModel::gamma_matrices();
let h_onsite = g1
.scale_real(self.lambda_x)
.add(&g3.scale_real(self.lambda_y))?;
let g2_i = g2.scale(Complex::new(0.0, -1.0));
let hop_x = g1
.scale_real(self.gamma_x * 0.5)
.add(&g2_i.scale_real(self.gamma_x * 0.5))?;
let g4_i = g4.scale(Complex::new(0.0, -1.0));
let hop_y = g3
.scale_real(self.gamma_y * 0.5)
.add(&g4_i.scale_real(self.gamma_y * 0.5))?;
let hop_x_dag = hop_x.conj_transpose();
let hop_y_dag = hop_y.conj_transpose();
for iy in 0..self.ly {
for ix in 0..self.lx {
for alpha in 0..Self::N_ORB {
for beta in 0..Self::N_ORB {
let row = self.site(ix, iy, alpha);
let col = self.site(ix, iy, beta);
let v = h_full.get(row, col).add(&h_onsite.get(alpha, beta));
h_full.set(row, col, v);
}
}
if ix + 1 < self.lx {
for alpha in 0..Self::N_ORB {
for beta in 0..Self::N_ORB {
let row = self.site(ix, iy, alpha);
let col = self.site(ix + 1, iy, beta);
let v = h_full.get(row, col).add(&hop_x.get(alpha, beta));
h_full.set(row, col, v);
let v2 = h_full.get(col, row).add(&hop_x_dag.get(beta, alpha));
h_full.set(col, row, v2);
}
}
}
if iy + 1 < self.ly {
for alpha in 0..Self::N_ORB {
for beta in 0..Self::N_ORB {
let row = self.site(ix, iy, alpha);
let col = self.site(ix, iy + 1, beta);
let v = h_full.get(row, col).add(&hop_y.get(alpha, beta));
h_full.set(row, col, v);
let v2 = h_full.get(col, row).add(&hop_y_dag.get(beta, alpha));
h_full.set(col, row, v2);
}
}
}
}
}
Ok(h_full)
}
pub fn solve_finite_cluster(&self) -> Result<(Vec<f64>, CMatrix)> {
let h = self.build_cluster_hamiltonian()?;
h.hermitian_eigendecomposition()
}
pub fn corner_localization(&self, eigvec_col: usize, eigvectors: &CMatrix) -> [f64; 4] {
let corners = [
(0, 0),
(self.lx - 1, 0),
(0, self.ly - 1),
(self.lx - 1, self.ly - 1),
];
let mut weights = [0.0_f64; 4];
let mut total = 0.0_f64;
for (ci, &(cx, cy)) in corners.iter().enumerate() {
let mut w = 0.0_f64;
for alpha in 0..Self::N_ORB {
let row = self.site(cx, cy, alpha);
let amp = eigvectors.get(row, eigvec_col);
w += amp.norm_sq();
}
weights[ci] = w;
total += w;
}
if total > 1e-30 {
for w in &mut weights {
*w /= total;
}
}
weights
}
pub fn count_corner_states(&self, eigvalues: &[f64], gap_min: f64, gap_max: f64) -> usize {
eigvalues
.iter()
.filter(|&&e| e > gap_min && e < gap_max)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bbh_topological_phase_is_topological() {
let m = BbhModel::topological_phase();
assert!(m.is_higher_order_topological());
}
#[test]
fn bbh_trivial_phase_is_not_topological() {
let m = BbhModel::trivial_phase();
assert!(!m.is_higher_order_topological());
}
#[test]
fn bbh_new_rejects_nan() {
assert!(BbhModel::new(f64::NAN, 1.0, 0.5, 1.0).is_err());
assert!(BbhModel::new(0.5, f64::INFINITY, 0.5, 1.0).is_err());
}
#[test]
fn bbh_hamiltonian_hermitian() {
let m = BbhModel::topological_phase();
for kx in [-1.0, 0.0, 0.5, PI / 3.0] {
for ky in [0.0, 1.0, PI] {
let h = m.hamiltonian_at(kx, ky).unwrap();
let hd = h.conj_transpose();
let diff = h.sub(&hd).unwrap();
assert!(
diff.frobenius_norm() < 1e-12,
"H not Hermitian at ({kx},{ky}): {}",
diff.frobenius_norm()
);
}
}
}
#[test]
fn bbh_four_bands() {
let m = BbhModel::topological_phase();
let e = m.energy_bands(0.0, 0.0).unwrap();
assert_eq!(e.len(), 4);
}
#[test]
fn bbh_bands_symmetric_about_zero() {
let m = BbhModel::topological_phase();
let e = m.energy_bands(0.7, 1.2).unwrap();
assert!((e[0] + e[3]).abs() < 1e-10, "PH symmetry broken: {e:?}");
assert!((e[1] + e[2]).abs() < 1e-10, "PH symmetry broken: {e:?}");
}
#[test]
fn bbh_topological_has_gap() {
let m = BbhModel::topological_phase();
let gap = m.band_gap(10, 10);
assert!(
gap > 0.5,
"Expected gap > 0.5 for topological BBH, got {gap}"
);
}
#[test]
fn bbh_quadrupole_moment_quantised() {
let m = BbhModel::topological_phase();
let q = m.quadrupole_moment(8, 8).unwrap();
let wrapped = q.rem_euclid(1.0);
let dist = (wrapped - 0.5).abs().min(wrapped.abs());
assert!(
dist < 0.35,
"q_xy not near 0 or ±0.5: q={q:.4}, wrapped={wrapped:.4}"
);
}
#[test]
fn bbh_quadrupole_rejects_small_grid() {
let m = BbhModel::topological_phase();
assert!(m.quadrupole_moment(2, 8).is_err());
assert!(m.quadrupole_moment(8, 2).is_err());
}
#[test]
fn breathing_kagome_topological_flag() {
let m = BreathingKagomeModel::topological_kagome();
assert!(m.is_topological());
let m2 = BreathingKagomeModel::trivial_kagome();
assert!(!m2.is_topological());
}
#[test]
fn breathing_kagome_hamiltonian_hermitian() {
let m = BreathingKagomeModel::topological_kagome();
let h = m.hamiltonian_at(0.3, 0.7);
let hd = h.conj_transpose();
let diff = h.sub(&hd).unwrap();
assert!(diff.frobenius_norm() < 1e-12);
}
#[test]
fn breathing_kagome_three_bands() {
let m = BreathingKagomeModel::topological_kagome();
let e = m.energy_bands(0.0, 0.0).unwrap();
assert_eq!(e.len(), 3);
}
#[test]
fn breathing_kagome_corner_polarization_range() {
let m = BreathingKagomeModel::topological_kagome();
let cp = m.corner_polarization(8, 8).unwrap();
assert!(
(0.0..=1.0).contains(&cp),
"Corner polarisation out of [0,1]: {cp}"
);
}
#[test]
fn corner_solver_rejects_oversized_cluster() {
assert!(CornerStateSolver::new(5, 5, 0.5, 1.0, 0.5, 1.0).is_err());
}
#[test]
fn corner_solver_2x2_has_dim_16() {
let solver = CornerStateSolver::new(2, 2, 0.5, 1.0, 0.5, 1.0).unwrap();
let h = solver.build_cluster_hamiltonian().unwrap();
assert_eq!(h.n(), 16, "Expected 2*2*4=16 dimensional H");
}
#[test]
fn corner_solver_hamiltonian_hermitian() {
let solver = CornerStateSolver::new(2, 2, 0.5, 1.0, 0.5, 1.0).unwrap();
let h = solver.build_cluster_hamiltonian().unwrap();
let hd = h.conj_transpose();
let diff = h.sub(&hd).unwrap();
assert!(
diff.frobenius_norm() < 1e-10,
"Cluster H not Hermitian: norm={}",
diff.frobenius_norm()
);
}
#[test]
fn corner_solver_spectrum_symmetric() {
let solver = CornerStateSolver::new(2, 2, 0.5, 1.0, 0.5, 1.0).unwrap();
let (evals, _) = solver.solve_finite_cluster().unwrap();
let n = evals.len();
for i in 0..n / 2 {
assert!(
(evals[i] + evals[n - 1 - i]).abs() < 1e-8,
"PH symmetry violated: E[{i}]={} vs E[{}]={}",
evals[i],
n - 1 - i,
evals[n - 1 - i]
);
}
}
#[test]
fn corner_localization_sums_to_one() {
let solver = CornerStateSolver::new(2, 2, 0.5, 1.0, 0.5, 1.0).unwrap();
let (_, evecs) = solver.solve_finite_cluster().unwrap();
let w = solver.corner_localization(0, &evecs);
let total: f64 = w.iter().sum();
assert!((total - 1.0).abs() < 1e-10, "Corner weights sum={total}");
}
#[test]
fn count_corner_states_topological_cluster() {
let solver = CornerStateSolver::new(2, 2, 0.5, 1.0, 0.5, 1.0).unwrap();
let (evals, _) = solver.solve_finite_cluster().unwrap();
let n_gap = solver.count_corner_states(&evals, -0.5, 0.5);
assert!(
n_gap >= 2,
"Expected ≥2 corner states in gap, found {n_gap}"
);
}
}