use crate::constants::{GAMMA, HBAR, MU_0};
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
pub enum LatticeGeometry {
Square,
Honeycomb,
Triangular,
Custom(usize),
}
impl LatticeGeometry {
pub fn n_bands(&self) -> usize {
match self {
LatticeGeometry::Square => 1,
LatticeGeometry::Honeycomb => 2,
LatticeGeometry::Triangular => 1,
LatticeGeometry::Custom(n) => *n,
}
}
pub fn lattice_constant(&self) -> f64 {
5e-10 }
}
#[derive(Debug, Clone)]
pub struct Magnon {
pub frequency: f64,
pub wavevector: (f64, f64),
pub band_index: usize,
}
#[derive(Debug, Clone)]
pub struct SpectralMagnonSolver {
pub lattice: LatticeGeometry,
pub n_kx: usize,
pub n_ky: usize,
pub j_nn: f64,
pub h_ext: f64,
pub ms: f64,
pub alpha: f64,
}
impl SpectralMagnonSolver {
pub fn new(
lattice: LatticeGeometry,
n_kx: usize,
n_ky: usize,
j_nn: f64,
h_ext: f64,
ms: f64,
alpha: f64,
) -> Result<Self> {
if n_kx < 1 {
return Err(error::invalid_param("n_kx", "must be at least 1"));
}
if n_ky < 1 {
return Err(error::invalid_param("n_ky", "must be at least 1"));
}
if ms <= 0.0 {
return Err(error::invalid_param(
"ms",
"saturation magnetization must be positive",
));
}
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
if alpha < 0.0 {
return Err(error::invalid_param(
"alpha",
"Gilbert damping must be non-negative",
));
}
Ok(Self {
lattice,
n_kx,
n_ky,
j_nn,
h_ext,
ms,
alpha,
})
}
pub fn square_lattice_fm(j: f64, h: f64) -> Self {
Self {
lattice: LatticeGeometry::Square,
n_kx: 32,
n_ky: 32,
j_nn: j,
h_ext: h,
ms: 1.4e5,
alpha: 3e-5,
}
}
pub fn honeycomb_afm(j: f64, h: f64) -> Self {
Self {
lattice: LatticeGeometry::Honeycomb,
n_kx: 32,
n_ky: 32,
j_nn: j,
h_ext: h,
ms: 1.4e5,
alpha: 3e-5,
}
}
#[inline]
fn omega_zeeman(&self) -> f64 {
GAMMA * MU_0 * self.h_ext
}
#[inline]
fn omega_exchange(&self) -> f64 {
2.0 * self.j_nn.abs() / HBAR
}
pub fn solve_bloch(&self, kx: f64, ky: f64) -> Vec<Magnon> {
let a = self.lattice.lattice_constant();
match &self.lattice {
LatticeGeometry::Square | LatticeGeometry::Triangular => {
self.solve_bloch_square_fm(kx, ky, a)
},
LatticeGeometry::Honeycomb => self.solve_bloch_honeycomb(kx, ky, a),
LatticeGeometry::Custom(n_bands) => {
let omega_z = self.omega_zeeman();
let omega_j = self.omega_exchange();
(0..*n_bands)
.map(|i| Magnon {
frequency: omega_z + i as f64 * omega_j,
wavevector: (kx, ky),
band_index: i,
})
.collect()
},
}
}
fn solve_bloch_square_fm(&self, kx: f64, ky: f64, a: f64) -> Vec<Magnon> {
let omega_z = self.omega_zeeman();
let omega_j = self.omega_exchange();
let cos_sum = match &self.lattice {
LatticeGeometry::Triangular => {
let ax = kx * a;
let ay = ky * a;
6.0 - 2.0 * ax.cos() - 2.0 * ay.cos() - 2.0 * (ax - ay).cos()
},
_ => {
2.0 - (kx * a).cos() - (ky * a).cos()
},
};
let omega = omega_z + omega_j * cos_sum;
vec![Magnon {
frequency: omega.max(0.0),
wavevector: (kx, ky),
band_index: 0,
}]
}
fn solve_bloch_honeycomb(&self, kx: f64, ky: f64, a: f64) -> Vec<Magnon> {
let omega_z = self.omega_zeeman();
let omega_j = self.omega_exchange();
let sqrt3_half = 3.0_f64.sqrt() / 2.0;
let phi1 = kx * a;
let phi2 = kx * a * 0.5 + ky * a * sqrt3_half;
let phi3 = -kx * a * 0.5 + ky * a * sqrt3_half;
let gamma_re = phi1.cos() + phi2.cos() + phi3.cos();
let gamma_im = phi1.sin() + phi2.sin() + phi3.sin();
let gamma_abs = (gamma_re * gamma_re + gamma_im * gamma_im).sqrt();
let h00 = Complex::from_real(omega_z);
let h01 = Complex::new(gamma_re * omega_j, gamma_im * omega_j);
let h10 = h01.conj();
let h11 = Complex::from_real(omega_z);
let mat = CMatrix::from_rows(vec![vec![h00, h01], vec![h10, h11]]);
match mat {
Ok(h) => match h.hermitian_eigendecomposition() {
Ok((vals, _vecs)) => {
let mut magnons: Vec<Magnon> = vals
.into_iter()
.enumerate()
.map(|(i, freq)| Magnon {
frequency: freq.max(0.0),
wavevector: (kx, ky),
band_index: i,
})
.collect();
magnons.sort_by(|a, b| {
a.frequency
.partial_cmp(&b.frequency)
.unwrap_or(std::cmp::Ordering::Equal)
});
magnons
},
Err(_) => {
let mut m = vec![
Magnon {
frequency: (omega_z - omega_j * gamma_abs).max(0.0),
wavevector: (kx, ky),
band_index: 0,
},
Magnon {
frequency: (omega_z + omega_j * gamma_abs).max(0.0),
wavevector: (kx, ky),
band_index: 1,
},
];
m.sort_by(|a, b| {
a.frequency
.partial_cmp(&b.frequency)
.unwrap_or(std::cmp::Ordering::Equal)
});
m
},
},
Err(_) => {
vec![
Magnon {
frequency: (omega_z - omega_j * gamma_abs).max(0.0),
wavevector: (kx, ky),
band_index: 0,
},
Magnon {
frequency: (omega_z + omega_j * gamma_abs).max(0.0),
wavevector: (kx, ky),
band_index: 1,
},
]
},
}
}
pub fn density_of_states(&self, omega: f64, broadening: f64) -> f64 {
use std::f64::consts::PI;
let eta = broadening.abs().max(1.0); let band_structure = self.full_band_structure();
let n_k = self.n_kx * self.n_ky;
let total: f64 = band_structure
.iter()
.flat_map(|bands| bands.iter())
.map(|m| {
let diff = omega - m.frequency;
eta / PI / (diff * diff + eta * eta)
})
.sum();
total / n_k as f64
}
pub fn mode_decomposition(&self, spin_field: &[Vector3<f64>]) -> Result<Vec<(f64, f64)>> {
if spin_field.is_empty() {
return Err(error::invalid_param(
"spin_field",
"spin field must be non-empty",
));
}
let n_sites = spin_field.len();
let n_kx = self.n_kx;
let n_ky = self.n_ky;
let a = self.lattice.lattice_constant();
let mut results = Vec::new();
for ix in 0..n_kx {
let kx = (ix as f64 / n_kx as f64 - 0.5) * 2.0 * std::f64::consts::PI / a;
for iy in 0..n_ky {
let ky = (iy as f64 / n_ky as f64 - 0.5) * 2.0 * std::f64::consts::PI / a;
let mut amp_re = 0.0_f64;
let mut amp_im = 0.0_f64;
for (j, sv) in spin_field.iter().enumerate().take(n_sites) {
let jx = j % n_kx;
let jy = j / n_kx;
let rx = jx as f64 * a;
let ry = jy as f64 * a;
let phase = kx * rx + ky * ry;
let s_perp_re = sv.x;
let s_perp_im = sv.y;
amp_re += s_perp_re * phase.cos() + s_perp_im * phase.sin();
amp_im += s_perp_im * phase.cos() - s_perp_re * phase.sin();
}
let amp_sq = (amp_re * amp_re + amp_im * amp_im) / n_sites as f64;
let magnons = self.solve_bloch(kx, ky);
for m in magnons {
results.push((m.frequency, amp_sq));
}
}
}
Ok(results)
}
pub fn spectral_weight(&self, omega: f64, kx: f64, ky: f64, broadening: f64) -> f64 {
use std::f64::consts::PI;
let eta = broadening.abs().max(1.0);
let magnons = self.solve_bloch(kx, ky);
magnons
.iter()
.map(|m| {
let diff = omega - m.frequency;
eta / PI / (diff * diff + eta * eta)
})
.sum()
}
pub fn full_band_structure(&self) -> Vec<Vec<Magnon>> {
use std::f64::consts::PI;
let a = self.lattice.lattice_constant();
let mut band_structure = Vec::with_capacity(self.n_kx * self.n_ky);
for ix in 0..self.n_kx {
for iy in 0..self.n_ky {
let kx = (ix as f64 / self.n_kx as f64 - 0.5) * 2.0 * PI / a;
let ky = (iy as f64 / self.n_ky as f64 - 0.5) * 2.0 * PI / a;
band_structure.push(self.solve_bloch(kx, ky));
}
}
band_structure
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
fn square_fm_solver() -> SpectralMagnonSolver {
SpectralMagnonSolver::square_lattice_fm(1e-23, 0.0)
}
fn honeycomb_afm_solver() -> SpectralMagnonSolver {
SpectralMagnonSolver::honeycomb_afm(-1e-23, 0.0)
}
#[test]
fn test_new_invalid_ms() {
let result =
SpectralMagnonSolver::new(LatticeGeometry::Square, 10, 10, 1e-23, 0.0, 0.0, 1e-4);
assert!(result.is_err());
}
#[test]
fn test_new_invalid_n_kx() {
let result =
SpectralMagnonSolver::new(LatticeGeometry::Square, 0, 10, 1e-23, 0.0, 1e5, 1e-4);
assert!(result.is_err());
}
#[test]
fn test_new_invalid_h_ext() {
let result =
SpectralMagnonSolver::new(LatticeGeometry::Square, 10, 10, 1e-23, -100.0, 1e5, 1e-4);
assert!(result.is_err());
}
#[test]
fn test_square_fm_preset_valid() {
let s = square_fm_solver();
assert!(s.n_kx > 0 && s.n_ky > 0);
assert!(s.ms > 0.0);
}
#[test]
fn test_solve_bloch_square_fm_gamma_point() {
let s = square_fm_solver();
let magnons = s.solve_bloch(0.0, 0.0);
assert_eq!(magnons.len(), 1, "Square FM should have 1 band");
assert!(magnons[0].frequency >= 0.0);
let omega_z = s.omega_zeeman();
let omega_j = s.omega_exchange();
let expected = omega_z + 0.0 * omega_j; let rel_err = (magnons[0].frequency - expected).abs() / (expected.max(1.0));
assert!(
rel_err < 0.01,
"Γ-point frequency mismatch: got {:.4e}, expected {:.4e}",
magnons[0].frequency,
expected
);
}
#[test]
fn test_solve_bloch_square_fm_bz_edge() {
let s = square_fm_solver();
let a = s.lattice.lattice_constant();
let kx_edge = PI / a;
let magnons = s.solve_bloch(kx_edge, 0.0);
assert_eq!(magnons.len(), 1);
assert!(
magnons[0].frequency > s.solve_bloch(0.0, 0.0)[0].frequency,
"BZ edge frequency should be higher than Γ-point"
);
}
#[test]
fn test_solve_bloch_honeycomb_two_bands() {
let s = honeycomb_afm_solver();
let magnons = s.solve_bloch(0.0, 0.0);
assert_eq!(magnons.len(), 2, "Honeycomb should have 2 bands");
assert!(
magnons[0].frequency <= magnons[1].frequency,
"Bands should be sorted"
);
}
#[test]
fn test_solve_bloch_honeycomb_dirac_cone() {
let s = honeycomb_afm_solver();
let a = s.lattice.lattice_constant();
let kx_k = 2.0 * PI / (3.0 * a);
let ky_k = 2.0 * PI / (3.0 * a * 3.0_f64.sqrt());
let magnons = s.solve_bloch(kx_k, ky_k);
assert_eq!(magnons.len(), 2);
let gap = (magnons[1].frequency - magnons[0].frequency).abs();
assert!(magnons[0].frequency >= 0.0);
assert!(magnons[1].frequency >= 0.0);
assert!(gap >= 0.0);
}
#[test]
fn test_density_of_states_positive() {
let s = SpectralMagnonSolver::new(LatticeGeometry::Square, 8, 8, 1e-23, 0.0, 1.4e5, 3e-5)
.expect("valid");
let omega_test = 2.0 * PI * 5e9;
let broadening = 2.0 * PI * 100e6;
let dos = s.density_of_states(omega_test, broadening);
assert!(dos >= 0.0, "DOS must be non-negative: {dos}");
}
#[test]
fn test_density_of_states_total() {
let s = SpectralMagnonSolver::new(LatticeGeometry::Square, 4, 4, 1e-23, 0.0, 1.4e5, 3e-5)
.expect("valid");
let broadening = 1e9;
let total: f64 = (0..20)
.map(|i| s.density_of_states(i as f64 * 1e10, broadening))
.sum();
assert!(total > 0.0, "Integrated DOS should be positive: {total}");
}
#[test]
fn test_spectral_weight_at_mode_frequency() {
let s = square_fm_solver();
let magnons = s.solve_bloch(0.0, 0.0);
let omega_mode = magnons[0].frequency;
let broadening = 1e8;
let aw_at_mode = s.spectral_weight(omega_mode, 0.0, 0.0, broadening);
let aw_off = s.spectral_weight(omega_mode + 1e10, 0.0, 0.0, broadening);
assert!(
aw_at_mode > aw_off,
"Spectral weight should peak at mode frequency: A_mode={aw_at_mode:.4e}, A_off={aw_off:.4e}"
);
}
#[test]
fn test_full_band_structure_shape() {
let s = SpectralMagnonSolver::new(LatticeGeometry::Square, 4, 4, 1e-23, 0.0, 1.4e5, 3e-5)
.expect("valid");
let bs = s.full_band_structure();
assert_eq!(bs.len(), 16, "Should have 4×4 = 16 k-points");
assert_eq!(bs[0].len(), 1, "Square lattice has 1 band");
for bands in &bs {
for m in bands {
assert!(m.frequency >= 0.0, "All frequencies must be non-negative");
}
}
}
#[test]
fn test_mode_decomposition_empty_field() {
let s = square_fm_solver();
let result = s.mode_decomposition(&[]);
assert!(result.is_err(), "Empty spin field should return an error");
}
#[test]
fn test_mode_decomposition_uniform_field() {
let s = SpectralMagnonSolver::new(LatticeGeometry::Square, 4, 4, 1e-23, 0.0, 1.4e5, 3e-5)
.expect("valid");
let n_sites = 16;
let spin_field: Vec<Vector3<f64>> =
(0..n_sites).map(|_| Vector3::new(0.01, 0.0, 1.0)).collect();
let result = s.mode_decomposition(&spin_field).expect("valid field");
assert!(
!result.is_empty(),
"Mode decomposition should return results"
);
for (freq, amp) in &result {
assert!(*freq >= 0.0, "Frequency must be non-negative: {freq}");
assert!(*amp >= 0.0, "Amplitude must be non-negative: {amp}");
}
}
#[test]
fn test_lattice_geometry_n_bands() {
assert_eq!(LatticeGeometry::Square.n_bands(), 1);
assert_eq!(LatticeGeometry::Honeycomb.n_bands(), 2);
assert_eq!(LatticeGeometry::Triangular.n_bands(), 1);
assert_eq!(LatticeGeometry::Custom(5).n_bands(), 5);
}
}