use crate::altermagnet::materials::{Altermagnet, AltermagneticSymmetry};
use crate::error::{Error, Result};
use crate::math::{CMatrix, Complex};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Spin {
Up,
Down,
}
impl Spin {
#[inline]
pub fn sigma(self) -> f64 {
match self {
Spin::Up => 1.0,
Spin::Down => -1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Band {
Lower,
Upper,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SpinBands {
pub lower: f64,
pub upper: f64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AltermagnetBandModel {
pub symmetry: AltermagneticSymmetry,
pub t_kin: f64,
pub delta_hyb: f64,
pub exchange_m: f64,
pub t_am: f64,
pub lambda_soc: f64,
pub fermi_energy: f64,
pub neel_angle: f64,
pub a_lattice: f64,
}
impl AltermagnetBandModel {
#[allow(clippy::too_many_arguments)]
pub fn new(
symmetry: AltermagneticSymmetry,
t_kin: f64,
delta_hyb: f64,
exchange_m: f64,
t_am: f64,
lambda_soc: f64,
fermi_energy: f64,
neel_angle: f64,
a_lattice: f64,
) -> Result<Self> {
if t_kin <= 0.0 || t_kin.is_nan() {
return Err(Error::InvalidParameter {
param: "t_kin".to_string(),
reason: "kinetic hopping must be strictly positive".to_string(),
});
}
if delta_hyb < 0.0 {
return Err(Error::InvalidParameter {
param: "delta_hyb".to_string(),
reason: "hybridization amplitude must be non-negative".to_string(),
});
}
if exchange_m < 0.0 {
return Err(Error::InvalidParameter {
param: "exchange_m".to_string(),
reason: "exchange splitting must be non-negative".to_string(),
});
}
if t_am < 0.0 {
return Err(Error::InvalidParameter {
param: "t_am".to_string(),
reason: "altermagnetic amplitude must be non-negative".to_string(),
});
}
if a_lattice <= 0.0 || a_lattice.is_nan() {
return Err(Error::InvalidParameter {
param: "a_lattice".to_string(),
reason: "lattice constant must be strictly positive".to_string(),
});
}
if !fermi_energy.is_finite() {
return Err(Error::InvalidParameter {
param: "fermi_energy".to_string(),
reason: "Fermi energy must be finite".to_string(),
});
}
Ok(Self {
symmetry,
t_kin,
delta_hyb,
exchange_m,
t_am,
lambda_soc,
fermi_energy,
neel_angle,
a_lattice,
})
}
pub fn from_altermagnet(m: &Altermagnet) -> Self {
Self {
symmetry: m.symmetry,
t_kin: 1.0,
delta_hyb: 0.3,
exchange_m: m.spin_splitting,
t_am: m.spin_splitting,
lambda_soc: 0.0,
fermi_energy: 0.5,
neel_angle: 0.0,
a_lattice: m.lattice_constant,
}
}
pub fn mnte() -> Self {
Self::from_altermagnet(&Altermagnet::mnte())
}
pub fn crsb() -> Self {
Self::from_altermagnet(&Altermagnet::crsb())
}
pub fn ruo2() -> Self {
Self::from_altermagnet(&Altermagnet::ruo2())
}
#[inline]
fn eps0(&self, kx: f64, ky: f64) -> f64 {
let k_mag = (kx * kx + ky * ky).sqrt();
let ak = self.a_lattice * k_mag;
self.t_kin * ak * ak
}
#[inline]
fn eps_a(&self, kx: f64, ky: f64) -> f64 {
let k_mag = (kx * kx + ky * ky).sqrt();
let ak = self.a_lattice * k_mag;
let phi = ky.atan2(kx);
let n = self.symmetry.harmonic_order() as f64;
self.t_am * ak * ak * (n * (phi - self.neel_angle)).cos()
}
#[inline]
fn gamma(&self, _kx: f64, _ky: f64) -> Complex {
Complex::from_real(self.delta_hyb)
}
pub fn spin_d_vector(&self, kx: f64, ky: f64, spin: Spin) -> (f64, f64, f64) {
let sigma = spin.sigma();
let g = self.gamma(kx, ky);
let sx = (self.a_lattice * kx).sin();
let sy = (self.a_lattice * ky).sin();
let cx = (self.a_lattice * kx).cos();
let cy = (self.a_lattice * ky).cos();
let dx = g.re + sigma * self.lambda_soc * sx;
let dy = -g.im + sigma * self.lambda_soc * (cx + cy);
let dz = self.eps_a(kx, ky) - sigma * self.exchange_m + sigma * self.lambda_soc * (sx + sy);
(dx, dy, dz)
}
#[inline]
fn d_norm(&self, kx: f64, ky: f64, spin: Spin) -> f64 {
let (dx, dy, dz) = self.spin_d_vector(kx, ky, spin);
(dx * dx + dy * dy + dz * dz).sqrt()
}
pub fn hamiltonian_matrix(&self, kx: f64, ky: f64, spin: Spin) -> Result<CMatrix> {
let e0 = self.eps0(kx, ky);
let (dx, dy, dz) = self.spin_d_vector(kx, ky, spin);
CMatrix::from_rows(vec![
vec![Complex::new(e0 + dz, 0.0), Complex::new(dx, -dy)],
vec![Complex::new(dx, dy), Complex::new(e0 - dz, 0.0)],
])
}
pub fn sublattice_swap_rotation(&self, kx: f64, ky: f64) -> (f64, f64) {
let alpha = std::f64::consts::PI / f64::from(self.symmetry.harmonic_order());
let (s, c) = alpha.sin_cos();
(kx * c - ky * s, kx * s + ky * c)
}
pub fn band_energy(&self, kx: f64, ky: f64, spin: Spin, band: Band) -> f64 {
let e0 = self.eps0(kx, ky);
let dn = self.d_norm(kx, ky, spin);
match band {
Band::Upper => e0 + dn,
Band::Lower => e0 - dn,
}
}
pub fn spin_bands(&self, kx: f64, ky: f64) -> (SpinBands, SpinBands) {
let e0 = self.eps0(kx, ky);
let dn_up = self.d_norm(kx, ky, Spin::Up);
let dn_dn = self.d_norm(kx, ky, Spin::Down);
let up = SpinBands {
lower: e0 - dn_up,
upper: e0 + dn_up,
};
let down = SpinBands {
lower: e0 - dn_dn,
upper: e0 + dn_dn,
};
(up, down)
}
pub fn spin_splitting_at(&self, kx: f64, ky: f64) -> f64 {
self.d_norm(kx, ky, Spin::Up) - self.d_norm(kx, ky, Spin::Down)
}
pub fn berry_curvature(&self, kx: f64, ky: f64, spin: Spin, band: Band) -> f64 {
let k_mag = (kx * kx + ky * ky).sqrt();
let h = 1e-6 * k_mag.max(1.0);
let d = self.spin_d_vector(kx, ky, spin);
let dp_x = self.spin_d_vector(kx + h, ky, spin);
let dm_x = self.spin_d_vector(kx - h, ky, spin);
let dp_y = self.spin_d_vector(kx, ky + h, spin);
let dm_y = self.spin_d_vector(kx, ky - h, spin);
let inv = 1.0 / (2.0 * h);
let ddx = (
(dp_x.0 - dm_x.0) * inv,
(dp_x.1 - dm_x.1) * inv,
(dp_x.2 - dm_x.2) * inv,
);
let ddy = (
(dp_y.0 - dm_y.0) * inv,
(dp_y.1 - dm_y.1) * inv,
(dp_y.2 - dm_y.2) * inv,
);
let cross = (
ddx.1 * ddy.2 - ddx.2 * ddy.1,
ddx.2 * ddy.0 - ddx.0 * ddy.2,
ddx.0 * ddy.1 - ddx.1 * ddy.0,
);
let triple = d.0 * cross.0 + d.1 * cross.1 + d.2 * cross.2;
let dn = (d.0 * d.0 + d.1 * d.1 + d.2 * d.2).sqrt();
let dn3 = dn * dn * dn;
if dn3 <= f64::MIN_POSITIVE {
return 0.0;
}
let base = triple / dn3;
match band {
Band::Upper => -0.5 * base,
Band::Lower => 0.5 * base,
}
}
fn check_grid(n_grid: usize, k_max: f64) -> Result<()> {
if n_grid == 0 {
return Err(Error::InvalidParameter {
param: "n_grid".to_string(),
reason: "grid resolution must be at least 1".to_string(),
});
}
if k_max <= 0.0 || !k_max.is_finite() {
return Err(Error::InvalidParameter {
param: "k_max".to_string(),
reason: "Brillouin-zone radius must be a finite positive number".to_string(),
});
}
Ok(())
}
fn fermi_sea_berry(&self, n_grid: usize, k_max: f64, spin_weighted: bool) -> Result<f64> {
Self::check_grid(n_grid, k_max)?;
let dk = 2.0 * k_max / n_grid as f64;
let cell_area = dk * dk;
let prefactor = cell_area / (2.0 * std::f64::consts::PI).powi(2);
let k_max_sq = k_max * k_max;
let mut acc = 0.0;
for i in 0..n_grid {
let kx = -k_max + (i as f64 + 0.5) * dk;
for j in 0..n_grid {
let ky = -k_max + (j as f64 + 0.5) * dk;
if kx * kx + ky * ky > k_max_sq {
continue;
}
for spin in [Spin::Up, Spin::Down] {
let weight = if spin_weighted { spin.sigma() } else { 1.0 };
for band in [Band::Lower, Band::Upper] {
let energy = self.band_energy(kx, ky, spin, band);
if self.fermi_energy - energy >= 0.0 {
acc += weight * self.berry_curvature(kx, ky, spin, band);
}
}
}
}
}
Ok(prefactor * acc)
}
pub fn crystal_hall_conductivity(&self, n_grid: usize, k_max: f64) -> Result<f64> {
self.fermi_sea_berry(n_grid, k_max, false)
}
pub fn spin_hall_conductivity(&self, n_grid: usize, k_max: f64) -> Result<f64> {
self.fermi_sea_berry(n_grid, k_max, true)
}
pub fn net_spin_polarization(&self, n_grid: usize, k_max: f64) -> Result<f64> {
Self::check_grid(n_grid, k_max)?;
let dk = 2.0 * k_max / n_grid as f64;
let k_max_sq = k_max * k_max;
let mut numerator = 0.0;
let mut denominator = 0.0;
for i in 0..n_grid {
let kx = -k_max + (i as f64 + 0.5) * dk;
for j in 0..n_grid {
let ky = -k_max + (j as f64 + 0.5) * dk;
if kx * kx + ky * ky > k_max_sq {
continue;
}
for spin in [Spin::Up, Spin::Down] {
let sigma = spin.sigma();
for band in [Band::Lower, Band::Upper] {
let energy = self.band_energy(kx, ky, spin, band);
if self.fermi_energy - energy >= 0.0 {
numerator += sigma;
denominator += 1.0;
}
}
}
}
}
if denominator == 0.0 {
return Ok(0.0);
}
Ok(numerator / denominator)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spin_degenerate_without_exchange() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.0,
0.7,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (up, down) = model.spin_bands(0.4, 0.2);
assert!((up.lower - down.lower).abs() < 1e-12);
assert!((up.upper - down.upper).abs() < 1e-12);
}
#[test]
fn spin_degenerate_afm_limit() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.9,
0.0,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (up, down) = model.spin_bands(0.4, 0.2);
assert!((up.lower - down.lower).abs() < 1e-12);
assert!((up.upper - down.upper).abs() < 1e-12);
}
#[test]
fn splitting_vanishes_at_nodes() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let radius = 0.35;
for angle in model.symmetry.node_angles() {
let kx = radius * angle.cos();
let ky = radius * angle.sin();
assert!(
model.spin_splitting_at(kx, ky).abs() < 1e-9,
"splitting at node angle {angle} should vanish"
);
}
assert!(model.spin_splitting_at(radius, 0.0).abs() > 1e-3);
}
#[test]
fn bands_even_in_k() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (kx, ky) = (0.37, 0.21);
let (up_p, down_p) = model.spin_bands(kx, ky);
let (up_m, down_m) = model.spin_bands(-kx, -ky);
assert!((up_p.lower - up_m.lower).abs() < 1e-12);
assert!((up_p.upper - up_m.upper).abs() < 1e-12);
assert!((down_p.lower - down_m.lower).abs() < 1e-12);
assert!((down_p.upper - down_m.upper).abs() < 1e-12);
}
#[test]
fn spin_group_relation_dwave() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (kx, ky) = (0.4, 0.15);
let (up, _) = model.spin_bands(kx, ky);
let (_, down_swapped) = model.spin_bands(ky, kx);
assert!((up.lower - down_swapped.lower).abs() < 1e-12);
assert!((up.upper - down_swapped.upper).abs() < 1e-12);
}
#[test]
fn crystal_hall_zero_without_soc() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.5,
0.0,
0.2,
0.0,
1.0,
)
.expect("parameters are valid");
let hall = model
.crystal_hall_conductivity(41, 2.5)
.expect("hall integral");
assert!(
hall.abs() < 1e-12,
"charge Hall must vanish without SOC: {hall}"
);
}
#[test]
fn crystal_hall_flips_under_neel_reversal_with_soc() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.5,
0.4,
0.2,
0.0,
1.0,
)
.expect("parameters are valid");
let (n_grid, k_max) = (61, 2.5);
let hall = model
.crystal_hall_conductivity(n_grid, k_max)
.expect("hall integral");
let mut reversed = model.clone();
reversed.exchange_m = -model.exchange_m;
let hall_reversed = reversed
.crystal_hall_conductivity(n_grid, k_max)
.expect("hall integral");
assert!(
hall.abs() > 1e-9,
"crystal Hall must be nonzero with SOC: {hall}"
);
assert!(
(hall + hall_reversed).abs() < 1e-6 * (1.0 + hall.abs()),
"crystal Hall must reverse under Neel reversal: {hall} vs {hall_reversed}"
);
}
#[test]
fn soc_term_has_correct_mixed_parity() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.4,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (kx, ky) = (0.4, 0.15);
let (dx_p, dy_p, dz_p) = model.spin_d_vector(kx, ky, Spin::Up);
let (dx_m, dy_m, dz_m) = model.spin_d_vector(-kx, -ky, Spin::Up);
assert!(
(dx_p - dx_m).abs() > 1e-6,
"dx must carry an odd-in-k SOC component: dx(k)={dx_p}, dx(-k)={dx_m}"
);
assert!(
(dy_p - dy_m).abs() < 1e-9,
"dy must be even in k: dy(k)={dy_p}, dy(-k)={dy_m}"
);
assert!(
(dy_p + dy_m).abs() > 1e-6,
"dy must carry a nonzero even-in-k SOC component: dy(k)={dy_p}, dy(-k)={dy_m}"
);
assert!(
(dz_p - dz_m).abs() > 1e-6,
"dz must carry an odd-in-k SOC component: dz(k)={dz_p}, dz(-k)={dz_m}"
);
}
#[test]
fn spin_group_relation_broken_by_soc() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.4,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (kx, ky) = (0.4, 0.15);
let (up, _) = model.spin_bands(kx, ky);
let (_, down_swapped) = model.spin_bands(ky, kx);
assert!(
(up.lower - down_swapped.lower).abs() > 1e-6,
"SOC must break the spin-group relation for the lower band: {} vs {}",
up.lower,
down_swapped.lower
);
assert!(
(up.upper - down_swapped.upper).abs() > 1e-6,
"SOC must break the spin-group relation for the upper band: {} vs {}",
up.upper,
down_swapped.upper
);
}
#[test]
fn berry_curvature_pointwise_zero_without_soc_nonzero_with_soc() {
let base = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let (kx, ky) = (0.4, 0.15);
let omega_no_soc = base.berry_curvature(kx, ky, Spin::Up, Band::Lower);
assert!(
omega_no_soc.abs() < 1e-9,
"Berry curvature must vanish pointwise without SOC: {omega_no_soc}"
);
let mut with_soc = base.clone();
with_soc.lambda_soc = 0.4;
let omega_soc = with_soc.berry_curvature(kx, ky, Spin::Up, Band::Lower);
assert!(
omega_soc.abs() > 1e-6,
"Berry curvature must be clearly nonzero with SOC: {omega_soc}"
);
}
#[test]
fn hamiltonian_matrix_is_hermitian() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::GWave,
1.0,
0.3,
0.8,
0.6,
0.4,
0.5,
0.2,
1.0,
)
.expect("parameters are valid");
for &(kx, ky) in &[(0.4, 0.15), (-0.7, 0.9), (1.1, -0.3), (0.0, 0.0)] {
for spin in [Spin::Up, Spin::Down] {
let h = model
.hamiltonian_matrix(kx, ky, spin)
.expect("2x2 matrix construction cannot fail");
for i in 0..h.n() {
for j in 0..h.n() {
let hij = h.get(i, j);
let hji_conj = h.get(j, i).conj();
assert!(
(hij.re - hji_conj.re).abs() < 1e-12
&& (hij.im - hji_conj.im).abs() < 1e-12,
"H[{i}][{j}]={hij:?} != conj(H[{j}][{i}])={hji_conj:?} at k=({kx},{ky}), spin={spin:?}"
);
}
}
}
}
}
#[test]
fn hamiltonian_matrix_eigenvalues_match_band_energy() {
for lambda_soc in [0.0, 0.4] {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::GWave,
1.0,
0.3,
0.8,
0.6,
lambda_soc,
0.5,
0.2,
1.0,
)
.expect("parameters are valid");
for &(kx, ky) in &[(0.4, 0.15), (-0.7, 0.9), (1.1, -0.3)] {
for spin in [Spin::Up, Spin::Down] {
let h = model
.hamiltonian_matrix(kx, ky, spin)
.expect("valid matrix");
let (evals, _) = h
.hermitian_eigendecomposition()
.expect("2x2 Hermitian eigendecomposition always converges");
let expect_lower = model.band_energy(kx, ky, spin, Band::Lower);
let expect_upper = model.band_energy(kx, ky, spin, Band::Upper);
assert!(
(evals[0] - expect_lower).abs() < 1e-9,
"lower eigenvalue {} != band_energy {} (soc={lambda_soc}, k=({kx},{ky}), spin={spin:?})",
evals[0], expect_lower
);
assert!(
(evals[1] - expect_upper).abs() < 1e-9,
"upper eigenvalue {} != band_energy {} (soc={lambda_soc}, k=({kx},{ky}), spin={spin:?})",
evals[1], expect_upper
);
}
}
}
}
#[test]
fn berry_curvature_band_sum_to_zero() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::GWave,
1.0,
0.3,
0.8,
0.6,
0.4,
0.5,
0.2,
1.0,
)
.expect("parameters are valid");
for &(kx, ky) in &[(0.4, 0.15), (-0.7, 0.9), (1.1, -0.3)] {
for spin in [Spin::Up, Spin::Down] {
let lower = model.berry_curvature(kx, ky, spin, Band::Lower);
let upper = model.berry_curvature(kx, ky, spin, Band::Upper);
assert!(
(lower + upper).abs() < 1e-10,
"Omega_Lower + Omega_Upper = {} (expected 0) at k=({kx},{ky}), spin={spin:?}",
lower + upper
);
}
}
}
#[test]
fn berry_curvature_odd_under_combined_spin_k_neel_flip() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::DWave,
1.0,
0.3,
0.8,
0.6,
0.4,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
let mut reversed = model.clone();
reversed.exchange_m = -model.exchange_m;
for &(kx, ky) in &[(0.4, 0.15), (0.9, -0.6), (-0.2, 1.3)] {
for band in [Band::Lower, Band::Upper] {
let omega_up_k_m = model.berry_curvature(kx, ky, Spin::Up, band);
let omega_down_mk_negm = reversed.berry_curvature(-kx, -ky, Spin::Down, band);
assert!(
(omega_up_k_m + omega_down_mk_negm).abs() < 1e-8,
"Omega_Up(k;M) + Omega_Down(-k;-M) = {} (expected 0) at k=({kx},{ky}), band={band:?}",
omega_up_k_m + omega_down_mk_negm
);
let omega_down_k_m = model.berry_curvature(kx, ky, Spin::Down, band);
let omega_up_mk_negm = reversed.berry_curvature(-kx, -ky, Spin::Up, band);
assert!(
(omega_down_k_m + omega_up_mk_negm).abs() < 1e-8,
"Omega_Down(k;M) + Omega_Up(-k;-M) = {} (expected 0) at k=({kx},{ky}), band={band:?}",
omega_down_k_m + omega_up_mk_negm
);
}
}
}
#[test]
fn net_spin_polarization_vanishes_for_presets() {
for model in [AltermagnetBandModel::mnte(), AltermagnetBandModel::crsb()] {
let k_max = 0.8 / model.a_lattice;
let polarization = model
.net_spin_polarization(41, k_max)
.expect("valid grid parameters");
assert!(
polarization.abs() < 1e-9,
"{} preset should be compensated (zero net spin polarization), got {polarization}",
model.symmetry
);
}
}
#[test]
fn spin_group_relation_gwave_generalized_rotation() {
let model = AltermagnetBandModel::new(
AltermagneticSymmetry::GWave,
1.0,
0.3,
0.8,
0.6,
0.0,
0.5,
0.0,
1.0,
)
.expect("parameters are valid");
assert_eq!(model.symmetry.harmonic_order(), 4);
let (kx, ky) = (0.4, 0.15);
let (rkx, rky) = model.sublattice_swap_rotation(kx, ky);
let (up, _) = model.spin_bands(kx, ky);
let (_, down_rotated) = model.spin_bands(rkx, rky);
assert!(
(up.lower - down_rotated.lower).abs() < 1e-9,
"E_up,lower(k)={} != E_down,lower(Rk)={}",
up.lower,
down_rotated.lower
);
assert!(
(up.upper - down_rotated.upper).abs() < 1e-9,
"E_up,upper(k)={} != E_down,upper(Rk)={}",
up.upper,
down_rotated.upper
);
let (_, down_swapped) = model.spin_bands(ky, kx);
assert!(
(up.lower - down_swapped.lower).abs() > 1e-3,
"a literal (kx,ky)->(ky,kx) swap should NOT satisfy the g-wave spin-group relation"
);
}
}