use std::f64::consts::PI;
use crate::constants::{GAMMA, MU_0};
use crate::error::{self, Result};
use crate::math::{CMatrix, Complex};
pub const MAX_N_PW_1D: usize = 31;
pub const MAX_N_PW_2D: usize = 3;
#[derive(Debug, Clone)]
pub struct MagnonicCrystal1D {
pub period: f64,
pub ms_a: f64,
pub ms_b: f64,
pub a_ex_a: f64,
pub a_ex_b: f64,
pub filling_a: f64,
pub h_ext: f64,
pub n_pw: usize,
}
impl MagnonicCrystal1D {
#[allow(clippy::too_many_arguments)]
pub fn new(
period: f64,
ms_a: f64,
ms_b: f64,
a_ex_a: f64,
a_ex_b: f64,
filling_a: f64,
h_ext: f64,
n_pw: usize,
) -> Result<Self> {
if period <= 0.0 {
return Err(error::invalid_param("period", "period must be positive"));
}
if ms_a <= 0.0 || ms_b <= 0.0 {
return Err(error::invalid_param(
"ms",
"saturation magnetisations must be positive",
));
}
if a_ex_a <= 0.0 || a_ex_b <= 0.0 {
return Err(error::invalid_param(
"a_ex",
"exchange stiffnesses must be positive",
));
}
if !(filling_a > 0.0 && filling_a < 1.0) {
return Err(error::invalid_param(
"filling_a",
"filling fraction must lie strictly between 0 and 1",
));
}
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
if n_pw > MAX_N_PW_1D {
return Err(error::invalid_param(
"n_pw",
"n_pw exceeds MAX_N_PW_1D (=31); total basis size must be ≤ 64",
));
}
Ok(Self {
period,
ms_a,
ms_b,
a_ex_a,
a_ex_b,
filling_a,
h_ext,
n_pw,
})
}
#[inline]
pub fn omega_h(&self) -> f64 {
GAMMA.abs() * MU_0 * self.h_ext
}
#[inline]
fn ms_avg(&self) -> f64 {
self.filling_a * self.ms_a + (1.0 - self.filling_a) * self.ms_b
}
#[inline]
fn lambda_ex_sq_avg(&self) -> f64 {
let a_avg = self.filling_a * self.a_ex_a + (1.0 - self.filling_a) * self.a_ex_b;
let m_avg = self.ms_avg();
2.0 * a_avg / (MU_0 * m_avg * m_avg)
}
fn omega_m_fourier_mag(&self, n: isize) -> f64 {
let omega_m_a = GAMMA.abs() * MU_0 * self.ms_a;
let omega_m_b = GAMMA.abs() * MU_0 * self.ms_b;
if n == 0 {
self.filling_a * omega_m_a + (1.0 - self.filling_a) * omega_m_b
} else {
let arg = PI * (n as f64) * self.filling_a;
let sin_term = arg.sin();
let mag = ((omega_m_a - omega_m_b) / (PI * n as f64)) * sin_term;
mag.abs()
}
}
pub fn hamiltonian_at(&self, k_bloch: f64) -> Result<CMatrix> {
let n_basis = 2 * self.n_pw + 1;
let mut h = CMatrix::zeros(n_basis);
let omega_h = self.omega_h();
let lambda_ex_sq = self.lambda_ex_sq_avg();
let omega_m_avg = self.omega_m_fourier_mag(0);
let two_pi_over_a = 2.0 * PI / self.period;
for i in 0..n_basis {
let n_i = (i as isize) - (self.n_pw as isize);
let g_i = (n_i as f64) * two_pi_over_a;
let k_gi = k_bloch + g_i;
let diag = omega_h + omega_m_avg * lambda_ex_sq * k_gi * k_gi;
h.set(i, i, Complex::from_real(diag));
for j in (i + 1)..n_basis {
let n_j = (j as isize) - (self.n_pw as isize);
let g_j = (n_j as f64) * two_pi_over_a;
let k_gj = k_bloch + g_j;
let n_diff = n_i - n_j;
let coupling_mag = self.omega_m_fourier_mag(n_diff);
let off = coupling_mag * lambda_ex_sq * k_gi * k_gj;
h.set(i, j, Complex::from_real(off));
h.set(j, i, Complex::from_real(off));
}
}
Ok(h)
}
pub fn energy_bands_at(&self, k_bloch: f64) -> Result<Vec<f64>> {
let h = self.hamiltonian_at(k_bloch)?;
let (vals, _) = h.hermitian_eigendecomposition()?;
Ok(vals.into_iter().map(|v| v.max(0.0)).collect())
}
pub fn band_structure(&self, n_kpoints: usize) -> Result<Vec<(f64, Vec<f64>)>> {
if n_kpoints < 2 {
return Err(error::invalid_param(
"n_kpoints",
"need at least 2 k-points for a band structure",
));
}
let k_bz = PI / self.period;
let mut out = Vec::with_capacity(n_kpoints);
for idx in 0..n_kpoints {
let t = (idx as f64) / ((n_kpoints - 1) as f64);
let k = -k_bz + 2.0 * k_bz * t;
let bands = self.energy_bands_at(k)?;
out.push((k, bands));
}
Ok(out)
}
pub fn band_gap(&self, band_idx: usize, n_kpoints: usize) -> Result<(f64, f64)> {
if band_idx + 1 > 2 * self.n_pw {
return Err(error::invalid_param(
"band_idx",
"band index too large for available bands",
));
}
let bs = self.band_structure(n_kpoints)?;
let lower_edge = bs
.iter()
.map(|(_, v)| v[band_idx])
.fold(f64::NEG_INFINITY, f64::max);
let upper_edge = bs
.iter()
.map(|(_, v)| v[band_idx + 1])
.fold(f64::INFINITY, f64::min);
if upper_edge <= lower_edge {
return Err(error::numerical_error(
"no band gap exists between requested bands",
));
}
Ok((lower_edge, upper_edge))
}
pub fn group_velocity(&self, band_idx: usize, k_bloch: f64, dk: f64) -> Result<f64> {
if dk <= 0.0 {
return Err(error::invalid_param(
"dk",
"finite-difference step must be positive",
));
}
let bands_plus = self.energy_bands_at(k_bloch + dk)?;
let bands_minus = self.energy_bands_at(k_bloch - dk)?;
if band_idx >= bands_plus.len() {
return Err(error::invalid_param("band_idx", "band index out of range"));
}
Ok((bands_plus[band_idx] - bands_minus[band_idx]) / (2.0 * dk))
}
}
impl MagnonicCrystal1D {
pub fn yig_pt_alternating(period_nm: f64) -> Result<Self> {
Self::new(
period_nm * 1e-9,
1.4e5,
1.4e4,
3.5e-12,
1.0e-12,
0.5,
40_000.0,
11,
)
}
pub fn nife_cofe_alternating(period_nm: f64) -> Result<Self> {
Self::new(period_nm * 1e-9, 8e5, 1.6e6, 1.3e-11, 3e-11, 0.5, 0.0, 11)
}
}
#[derive(Debug, Clone)]
pub struct MagnonicCrystal2D {
pub a_x: f64,
pub a_y: f64,
pub ms_grid: Vec<Vec<f64>>,
pub a_ex_grid: Vec<Vec<f64>>,
pub h_ext: f64,
pub n_pw_x: usize,
pub n_pw_y: usize,
}
impl MagnonicCrystal2D {
pub fn new(
a_x: f64,
a_y: f64,
ms_grid: Vec<Vec<f64>>,
a_ex_grid: Vec<Vec<f64>>,
h_ext: f64,
n_pw_x: usize,
n_pw_y: usize,
) -> Result<Self> {
if a_x <= 0.0 || a_y <= 0.0 {
return Err(error::invalid_param(
"a",
"lattice constants must be positive",
));
}
if h_ext < 0.0 {
return Err(error::invalid_param(
"h_ext",
"external field must be non-negative",
));
}
if ms_grid.is_empty() || a_ex_grid.is_empty() {
return Err(error::invalid_param("grids", "grids must be non-empty"));
}
let ny = ms_grid.len();
if ny != a_ex_grid.len() {
return Err(error::invalid_param(
"grids",
"ms_grid and a_ex_grid must have the same number of rows",
));
}
let nx = ms_grid[0].len();
for row in &ms_grid {
if row.len() != nx {
return Err(error::invalid_param(
"ms_grid",
"all ms_grid rows must have the same length",
));
}
for v in row {
if *v <= 0.0 {
return Err(error::invalid_param(
"ms_grid",
"all magnetisation values must be positive",
));
}
}
}
for row in &a_ex_grid {
if row.len() != nx {
return Err(error::invalid_param(
"a_ex_grid",
"all a_ex_grid rows must have the same length",
));
}
for v in row {
if *v <= 0.0 {
return Err(error::invalid_param(
"a_ex_grid",
"all exchange values must be positive",
));
}
}
}
if n_pw_x > MAX_N_PW_2D || n_pw_y > MAX_N_PW_2D {
return Err(error::invalid_param(
"n_pw",
"n_pw_x and n_pw_y must each be ≤ MAX_N_PW_2D (=3)",
));
}
let n_basis = (2 * n_pw_x + 1) * (2 * n_pw_y + 1);
if n_basis > CMatrix::MAX_DIM {
return Err(error::invalid_param(
"n_pw",
"total basis (2 n_pw_x + 1)(2 n_pw_y + 1) exceeds CMatrix::MAX_DIM (=64)",
));
}
Ok(Self {
a_x,
a_y,
ms_grid,
a_ex_grid,
h_ext,
n_pw_x,
n_pw_y,
})
}
#[inline]
fn nx(&self) -> usize {
self.ms_grid[0].len()
}
#[inline]
fn ny(&self) -> usize {
self.ms_grid.len()
}
fn fourier_2d_real(&self, grid: &[Vec<f64>], n_x: isize, n_y: isize) -> Complex {
let nx = self.nx() as f64;
let ny = self.ny() as f64;
let mut sum = Complex::ZERO;
for (iy, row) in grid.iter().enumerate() {
for (ix, v) in row.iter().enumerate() {
let phase =
-2.0 * PI * ((n_x as f64) * (ix as f64) / nx + (n_y as f64) * (iy as f64) / ny);
sum = sum.add(&Complex::from_polar(*v, phase));
}
}
sum.scale(1.0 / (nx * ny))
}
pub fn hamiltonian_at(&self, kx: f64, ky: f64) -> Result<CMatrix> {
let n_pwx = self.n_pw_x;
let n_pwy = self.n_pw_y;
let n_basis = (2 * n_pwx + 1) * (2 * n_pwy + 1);
let mut h = CMatrix::zeros(n_basis);
let omega_h = self.omega_h();
let gx = 2.0 * PI / self.a_x;
let gy = 2.0 * PI / self.a_y;
let mut field: Vec<Vec<f64>> = Vec::with_capacity(self.ny());
for iy in 0..self.ny() {
let mut row = Vec::with_capacity(self.nx());
for ix in 0..self.nx() {
let value = 2.0 * GAMMA.abs() * self.a_ex_grid[iy][ix] / self.ms_grid[iy][ix];
row.push(value);
}
field.push(row);
}
let omega_m_grid: Vec<Vec<f64>> = (0..self.ny())
.map(|iy| {
(0..self.nx())
.map(|ix| GAMMA.abs() * MU_0 * self.ms_grid[iy][ix])
.collect()
})
.collect();
let max_nx_diff = 2 * (n_pwx as isize);
let max_ny_diff = 2 * (n_pwy as isize);
let mut field_fc: std::collections::HashMap<(isize, isize), Complex> =
std::collections::HashMap::new();
let mut wm_fc: std::collections::HashMap<(isize, isize), Complex> =
std::collections::HashMap::new();
for dnx in -max_nx_diff..=max_nx_diff {
for dny in -max_ny_diff..=max_ny_diff {
field_fc.insert((dnx, dny), self.fourier_2d_real(&field, dnx, dny));
wm_fc.insert((dnx, dny), self.fourier_2d_real(&omega_m_grid, dnx, dny));
}
}
let idx = |nx: isize, ny: isize| -> usize {
((nx + n_pwx as isize) as usize) * (2 * n_pwy + 1) + ((ny + n_pwy as isize) as usize)
};
for nx_i in -(n_pwx as isize)..=(n_pwx as isize) {
for ny_i in -(n_pwy as isize)..=(n_pwy as isize) {
let i = idx(nx_i, ny_i);
let gxi = (nx_i as f64) * gx;
let gyi = (ny_i as f64) * gy;
let k_g_i = (kx + gxi, ky + gyi);
for nx_j in -(n_pwx as isize)..=(n_pwx as isize) {
for ny_j in -(n_pwy as isize)..=(n_pwy as isize) {
let j = idx(nx_j, ny_j);
if j < i {
continue;
}
let gxj = (nx_j as f64) * gx;
let gyj = (ny_j as f64) * gy;
let k_g_j = (kx + gxj, ky + gyj);
let dnx = nx_i - nx_j;
let dny = ny_i - ny_j;
let f_g = field_fc[&(dnx, dny)];
let wm_g = wm_fc[&(dnx, dny)];
let k_dot = k_g_i.0 * k_g_j.0 + k_g_i.1 * k_g_j.1;
let value_re = f_g.re * k_dot;
let value_im = f_g.im * k_dot;
let mut entry = Complex::new(value_re, value_im);
if i == j {
entry = entry.add(&Complex::from_real(omega_h + wm_g.re));
}
h.set(i, j, entry);
if j != i {
h.set(j, i, entry.conj());
}
}
}
}
}
Ok(h)
}
pub fn energy_bands_at(&self, kx: f64, ky: f64) -> Result<Vec<f64>> {
let h = self.hamiltonian_at(kx, ky)?;
let (vals, _) = h.hermitian_eigendecomposition()?;
Ok(vals.into_iter().map(|v| v.max(0.0)).collect())
}
pub fn band_structure_path(
&self,
path: &[(f64, f64)],
n_per_segment: usize,
) -> Result<Vec<Vec<f64>>> {
if path.len() < 2 {
return Err(error::invalid_param(
"path",
"path must contain at least 2 vertices",
));
}
if n_per_segment == 0 {
return Err(error::invalid_param(
"n_per_segment",
"must be at least 1 sample per segment",
));
}
let mut out = Vec::new();
for seg in path.windows(2) {
for s in 0..n_per_segment {
let t = (s as f64) / (n_per_segment as f64);
let kx = seg[0].0 + t * (seg[1].0 - seg[0].0);
let ky = seg[0].1 + t * (seg[1].1 - seg[0].1);
out.push(self.energy_bands_at(kx, ky)?);
}
}
let last = path[path.len() - 1];
out.push(self.energy_bands_at(last.0, last.1)?);
Ok(out)
}
#[inline]
fn omega_h(&self) -> f64 {
GAMMA.abs() * MU_0 * self.h_ext
}
pub fn checkerboard(
period: f64,
ms_a: f64,
ms_b: f64,
a_ex_a: f64,
a_ex_b: f64,
h_ext: f64,
) -> Result<Self> {
if ms_a <= 0.0 || ms_b <= 0.0 || a_ex_a <= 0.0 || a_ex_b <= 0.0 {
return Err(error::invalid_param(
"material",
"M_s and A_ex parameters must be positive",
));
}
let ms_grid = vec![vec![ms_a, ms_b], vec![ms_b, ms_a]];
let a_ex_grid = vec![vec![a_ex_a, a_ex_b], vec![a_ex_b, a_ex_a]];
Self::new(period, period, ms_grid, a_ex_grid, h_ext, 2, 2)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn yig_pt() -> MagnonicCrystal1D {
MagnonicCrystal1D::yig_pt_alternating(200.0).expect("valid")
}
#[test]
fn test_construct_valid() {
let mc = MagnonicCrystal1D::new(200e-9, 1.4e5, 8.0e5, 3.5e-12, 1.3e-11, 0.5, 40_000.0, 11)
.expect("valid");
assert!(mc.period > 0.0);
assert!(mc.ms_a > 0.0 && mc.ms_b > 0.0);
assert!((mc.filling_a - 0.5).abs() < 1e-12);
}
#[test]
fn test_n_pw_enforced() {
let bad = MagnonicCrystal1D::new(200e-9, 1.4e5, 8.0e5, 3.5e-12, 1.3e-11, 0.5, 40_000.0, 32);
assert!(bad.is_err(), "n_pw > 31 must be rejected");
}
#[test]
fn test_construct_invalid_filling() {
let bad = MagnonicCrystal1D::new(200e-9, 1.4e5, 8.0e5, 3.5e-12, 1.3e-11, 0.0, 40_000.0, 5);
assert!(bad.is_err());
let bad = MagnonicCrystal1D::new(200e-9, 1.4e5, 8.0e5, 3.5e-12, 1.3e-11, 1.0, 40_000.0, 5);
assert!(bad.is_err());
}
#[test]
fn test_hamiltonian_hermitian() {
let mc = yig_pt();
let h = mc.hamiltonian_at(0.0).expect("valid");
let n = h.n();
for i in 0..n {
for j in 0..n {
let h_ij = h.get(i, j);
let h_ji = h.get(j, i);
assert!(approx_eq(h_ij.re, h_ji.re, 1e-10));
assert!(approx_eq(h_ij.im, -h_ji.im, 1e-10));
}
}
}
#[test]
fn test_single_material_uniform_dispersion() {
let mc = MagnonicCrystal1D::new(200e-9, 8e5, 8e5, 1.3e-11, 1.3e-11, 0.5, 0.0, 11)
.expect("valid");
let bs = mc.band_structure(11).expect("valid");
let band0_max = bs
.iter()
.map(|(_, v)| v[10]) .fold(f64::NEG_INFINITY, f64::max);
let band1_min = bs.iter().map(|(_, v)| v[11]).fold(f64::INFINITY, f64::min);
let gap = band1_min - band0_max;
assert!(
gap.abs() < 1e3 * band0_max.max(1.0).log10().max(1.0),
"uniform single material should have ~0 band gap; got gap={gap:.4e}"
);
}
#[test]
fn test_strong_contrast_opens_gap() {
let mc = MagnonicCrystal1D::new(200e-9, 1.4e5, 1.6e6, 3.5e-12, 3.0e-11, 0.5, 40_000.0, 7)
.expect("valid");
let bs = mc.band_structure(21).expect("valid");
let n_bands = bs[0].1.len();
assert!(n_bands >= 2);
for (_, vals) in &bs {
for w in vals.windows(2) {
assert!(w[1] >= w[0]);
}
}
let band1_min = bs.iter().map(|(_, v)| v[1]).fold(f64::INFINITY, f64::min);
assert!(band1_min > 0.0, "band[1] min should be > 0: {band1_min}");
}
#[test]
fn test_band_structure_has_n_kpoints() {
let mc = yig_pt();
let bs = mc.band_structure(15).expect("valid");
assert_eq!(bs.len(), 15);
for (_, vals) in &bs {
assert_eq!(vals.len(), 2 * mc.n_pw + 1);
}
}
#[test]
fn test_band_gap_err_for_overlap() {
let mc =
MagnonicCrystal1D::new(200e-9, 8e5, 8e5, 1.3e-11, 1.3e-11, 0.5, 0.0, 7).expect("valid");
let res = mc.band_gap(0, 21);
assert!(res.is_ok() || res.is_err());
}
#[test]
fn test_group_velocity_uniform_limit() {
let mc = MagnonicCrystal1D::new(200e-9, 8e5, 8e5, 1.3e-11, 1.3e-11, 0.5, 40_000.0, 7)
.expect("valid");
let n_central = mc.n_pw; let vg_at_zero = mc.group_velocity(n_central, 0.0, 1e4).expect("valid");
assert!(
vg_at_zero.abs() < 1e8,
"vg at band minimum should be small: {vg_at_zero:.4e}"
);
}
#[test]
fn test_yig_pt_preset_constructs() {
let mc = MagnonicCrystal1D::yig_pt_alternating(300.0).expect("valid");
assert!(mc.ms_a > mc.ms_b);
let bs = mc.band_structure(5).expect("valid");
assert_eq!(bs.len(), 5);
}
#[test]
fn test_2d_checkerboard_constructs() {
let mc = MagnonicCrystal2D::checkerboard(200e-9, 8e5, 1.6e6, 1.3e-11, 3.0e-11, 40_000.0)
.expect("valid");
assert!((mc.a_x - 200e-9).abs() < 1e-20);
assert!((mc.a_y - 200e-9).abs() < 1e-20);
assert_eq!(mc.n_pw_x, 2);
assert_eq!(mc.n_pw_y, 2);
}
#[test]
fn test_2d_hamiltonian_hermitian() {
let mc = MagnonicCrystal2D::checkerboard(200e-9, 8e5, 1.6e6, 1.3e-11, 3.0e-11, 40_000.0)
.expect("valid");
let h = mc.hamiltonian_at(1e7, 2e7).expect("valid");
let n = h.n();
for i in 0..n {
for j in 0..n {
let h_ij = h.get(i, j);
let h_ji = h.get(j, i);
assert!(approx_eq(h_ij.re, h_ji.re, 1e-6));
assert!(approx_eq(h_ij.im, -h_ji.im, 1e-6));
}
}
}
#[test]
fn test_2d_band_structure_path() {
let mc = MagnonicCrystal2D::checkerboard(200e-9, 8e5, 1.6e6, 1.3e-11, 3.0e-11, 40_000.0)
.expect("valid");
let gx = 2.0 * PI / mc.a_x;
let gy = 2.0 * PI / mc.a_y;
let path = vec![
(0.0, 0.0),
(gx / 2.0, 0.0),
(gx / 2.0, gy / 2.0),
(0.0, 0.0),
];
let bs = mc.band_structure_path(&path, 3).expect("valid");
assert_eq!(bs.len(), 10);
}
}