use std::f64::consts::PI;
use super::band_model::MagnonBandModel;
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EdgeSide {
Top,
Bottom,
Bulk,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EdgeMode {
pub kx: f64,
pub frequency: f64,
pub localization: f64,
pub edge: EdgeSide,
}
pub struct EdgeModes<'a> {
pub model: &'a MagnonBandModel,
pub width: usize,
}
impl<'a> EdgeModes<'a> {
pub fn new(model: &'a MagnonBandModel, width: usize) -> Result<Self> {
if width < 2 {
return Err(error::invalid_param(
"width",
"strip width must be at least 2",
));
}
if width > 60 {
return Err(error::invalid_param(
"width",
"strip width must be at most 60 (matrix size limit)",
));
}
Ok(Self { model, width })
}
pub fn solve_strip(&self, kx: f64) -> Result<Vec<EdgeMode>> {
let nb = self.model.n_bands();
let ny = self.width;
let dim = nb * ny;
if dim > crate::math::CMatrix::MAX_DIM {
return Err(error::invalid_param(
"width",
"width * n_bands exceeds CMatrix::MAX_DIM (64)",
));
}
let h_intra = self.model.hamiltonian_at((kx, 0.0))?;
let h_ky_offset = self.model.hamiltonian_at((kx, PI / (ny as f64).max(4.0)))?;
let h_inter_raw = h_ky_offset.sub(&h_intra)?;
let h_inter = h_inter_raw.scale_real(0.5);
let mut h_strip = CMatrix::zeros(dim);
for iy in 0..ny {
for i in 0..nb {
for j in 0..nb {
let cur = h_strip.get(iy * nb + i, iy * nb + j);
h_strip.set(iy * nb + i, iy * nb + j, cur.add(&h_intra.get(i, j)));
}
}
if iy + 1 < ny {
for i in 0..nb {
for j in 0..nb {
let t = h_inter.get(i, j);
let cur_up = h_strip.get(iy * nb + i, (iy + 1) * nb + j);
h_strip.set(iy * nb + i, (iy + 1) * nb + j, cur_up.add(&t));
let cur_dn = h_strip.get((iy + 1) * nb + j, iy * nb + i);
h_strip.set((iy + 1) * nb + j, iy * nb + i, cur_dn.add(&t.conj()));
}
}
}
}
let (evals, vecs) = h_strip.hermitian_eigendecomposition()?;
let mut modes = Vec::with_capacity(dim);
for (mode_idx, &e) in evals.iter().enumerate() {
let eigenvec: Vec<Complex> = (0..dim).map(|r| vecs.get(r, mode_idx)).collect();
let loc = Self::edge_localization_metric(&eigenvec);
let edge = classify_edge(&eigenvec, nb, ny);
modes.push(EdgeMode {
kx,
frequency: e,
localization: loc,
edge,
});
}
Ok(modes)
}
pub fn edge_localization_metric(eigenvector: &[Complex]) -> f64 {
let total: f64 = eigenvector.iter().map(|c| c.norm_sq()).sum();
if total < 1e-30 {
return 0.0;
}
let n = eigenvector.len();
if n < 4 {
return eigenvector.iter().map(|c| c.norm_sq()).sum::<f64>() / total;
}
let edge_weight = eigenvector[0].norm_sq()
+ eigenvector[1].norm_sq()
+ eigenvector[n - 2].norm_sq()
+ eigenvector[n - 1].norm_sq();
edge_weight / total
}
pub fn find_in_gap_modes(&self, kx: f64, gap_min: f64, gap_max: f64) -> Result<Vec<EdgeMode>> {
let all = self.solve_strip(kx)?;
Ok(all
.into_iter()
.filter(|m| m.frequency >= gap_min && m.frequency <= gap_max)
.collect())
}
pub fn dispersion_curve(
&self,
kx_min: f64,
kx_max: f64,
n_kx: usize,
) -> Result<Vec<(f64, Vec<f64>)>> {
if n_kx < 2 {
return Err(error::invalid_param("n_kx", "need at least 2 kx points"));
}
let mut result = Vec::with_capacity(n_kx);
for i in 0..n_kx {
let kx = kx_min + (kx_max - kx_min) * (i as f64) / ((n_kx - 1) as f64);
let modes = self.solve_strip(kx)?;
let evals: Vec<f64> = modes.iter().map(|m| m.frequency).collect();
result.push((kx, evals));
}
Ok(result)
}
pub fn edge_velocity(&self, kx0: f64, dkx: f64, gap_min: f64, gap_max: f64) -> Result<f64> {
if dkx <= 0.0 {
return Err(error::invalid_param("dkx", "must be positive"));
}
let modes_p = self.find_in_gap_modes(kx0 + dkx, gap_min, gap_max)?;
let modes_m = self.find_in_gap_modes(kx0 - dkx, gap_min, gap_max)?;
if modes_p.is_empty() || modes_m.is_empty() {
return Err(error::numerical_error(
"no in-gap modes found at neighbouring kx points for velocity computation",
));
}
let ep = modes_p
.iter()
.map(|m| m.frequency)
.fold(f64::INFINITY, f64::min);
let em = modes_m
.iter()
.map(|m| m.frequency)
.fold(f64::INFINITY, f64::min);
Ok((ep - em) / (2.0 * dkx))
}
}
fn classify_edge(eigenvec: &[Complex], nb: usize, ny: usize) -> EdgeSide {
let dim = eigenvec.len();
if dim == 0 || nb == 0 || ny == 0 {
return EdgeSide::Bulk;
}
let total: f64 = eigenvec.iter().map(|c| c.norm_sq()).sum();
if total < 1e-30 {
return EdgeSide::Bulk;
}
let w_bottom: f64 = eigenvec[0..nb.min(dim)].iter().map(|c| c.norm_sq()).sum();
let top_start = ((ny - 1) * nb).min(dim);
let w_top: f64 = eigenvec[top_start..dim].iter().map(|c| c.norm_sq()).sum();
let f_bottom = w_bottom / total;
let f_top = w_top / total;
let threshold = 0.25; if f_bottom > f_top && f_bottom >= threshold {
EdgeSide::Bottom
} else if f_top >= threshold {
EdgeSide::Top
} else {
EdgeSide::Bulk
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topomagnon::band_model::MagnonBandModel;
#[test]
fn strip_size_correct() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.3, 0.0).unwrap();
let em = EdgeModes::new(&m, 10).unwrap();
let modes = em.solve_strip(0.5).unwrap();
assert_eq!(modes.len(), 20, "Expected 20 modes, got {}", modes.len());
}
#[test]
fn eigenvalues_real_within_tolerance() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.2, 0.1).unwrap();
let em = EdgeModes::new(&m, 8).unwrap();
let modes = em.solve_strip(0.0).unwrap();
for mode in &modes {
assert!(
mode.frequency.is_finite(),
"Eigenvalue not finite: {}",
mode.frequency
);
}
}
#[test]
fn eigenvectors_normalized() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.3, 0.0).unwrap();
let em = EdgeModes::new(&m, 6).unwrap();
let modes = em.solve_strip(0.3).unwrap();
for mode in &modes {
assert!(
mode.localization >= 0.0 && mode.localization <= 1.0 + 1e-10,
"Localization out of [0,1]: {}",
mode.localization
);
}
}
#[test]
fn strip_has_more_modes_than_bulk() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.3, 0.0).unwrap();
let em = EdgeModes::new(&m, 8).unwrap();
let strip_modes = em.solve_strip(0.0).unwrap().len();
assert!(
strip_modes > m.n_bands(),
"Strip ({}) should have more modes than bulk ({})",
strip_modes,
m.n_bands()
);
}
#[test]
fn localization_metric_in_range_0_1() {
let m = MagnonBandModel::kagome(1.0, 0.3, 0.0).unwrap();
let em = EdgeModes::new(&m, 5).unwrap();
let modes = em.solve_strip(0.5).unwrap();
for mode in &modes {
assert!(
(0.0..=1.0 + 1e-10).contains(&mode.localization),
"Localization {} out of [0,1]",
mode.localization
);
}
}
#[test]
fn dispersion_curve_correct_kx_count() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.3, 0.0).unwrap();
let em = EdgeModes::new(&m, 6).unwrap();
let disp = em.dispersion_curve(-PI, PI, 10).unwrap();
assert_eq!(disp.len(), 10);
assert_eq!(disp[0].1.len(), 12); }
#[test]
fn edge_velocity_finite() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.5, 0.0).unwrap();
let em = EdgeModes::new(&m, 8).unwrap();
let gap_min = -1.5;
let gap_max = 1.5;
if let Ok(v) = em.edge_velocity(0.0, 0.1, gap_min, gap_max) {
assert!(v.is_finite(), "velocity not finite");
}
}
#[test]
fn width_minimum_2() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.1, 0.0).unwrap();
assert!(EdgeModes::new(&m, 1).is_err());
assert!(EdgeModes::new(&m, 2).is_ok());
}
#[test]
fn width_maximum_60() {
let m = MagnonBandModel::square_dmi(1.0, 0.0, 0.0).unwrap();
assert!(EdgeModes::new(&m, 60).is_ok());
assert!(EdgeModes::new(&m, 61).is_err());
}
#[test]
fn in_gap_filter_reduces_count() {
let m = MagnonBandModel::honeycomb_haldane(1.0, 0.0, 0.3, 0.0).unwrap();
let em = EdgeModes::new(&m, 8).unwrap();
let all_modes = em.solve_strip(0.0).unwrap();
let total = all_modes.len();
let e_min = all_modes
.iter()
.map(|m| m.frequency)
.fold(f64::INFINITY, f64::min);
let e_max = all_modes
.iter()
.map(|m| m.frequency)
.fold(f64::NEG_INFINITY, f64::max);
let mid = (e_min + e_max) / 2.0;
let width = (e_max - e_min) / 4.0;
let in_gap = em.find_in_gap_modes(0.0, mid - width, mid + width).unwrap();
assert!(
in_gap.len() <= total,
"In-gap filter should not increase mode count"
);
}
}