use std::f64::consts::PI;
use crate::constants::{CONDUCTANCE_QUANTUM, C_LIGHT, EPSILON_0, E_CHARGE, H_PLANCK};
use crate::error::{self, Result};
use crate::math::Complex;
use crate::topomagnon::band_model_3d::MagnonBandModel3D;
use crate::vector3::Vector3;
pub struct AxionElectrodynamics<'a> {
pub model_3d: &'a MagnonBandModel3D,
pub occupied_bands: Vec<usize>,
pub n_kx: usize,
pub n_ky: usize,
pub n_kz: usize,
}
impl<'a> AxionElectrodynamics<'a> {
pub fn new(
model_3d: &'a MagnonBandModel3D,
occupied_bands: Vec<usize>,
n_kx: usize,
n_ky: usize,
n_kz: usize,
) -> Result<Self> {
if occupied_bands.is_empty() {
return Err(error::invalid_param(
"occupied_bands",
"must list at least one band index",
));
}
let nb = model_3d.n_bands();
for &b in &occupied_bands {
if b >= nb {
return Err(error::invalid_param(
"occupied_bands",
&format!("band index {b} exceeds n_bands-1={}", nb - 1),
));
}
}
for (name, &val) in [("n_kx", &n_kx), ("n_ky", &n_ky), ("n_kz", &n_kz)] {
if val < 4 {
return Err(error::invalid_param(name, "mesh size must be at least 4"));
}
}
Ok(Self {
model_3d,
occupied_bands,
n_kx,
n_ky,
n_kz,
})
}
pub fn berry_connection_at(&self, kx: f64, ky: f64, kz: f64, direction: usize) -> f64 {
let dk = 1e-4;
let (dkx, dky, dkz) = match direction {
0 => (dk, 0.0, 0.0),
1 => (0.0, dk, 0.0),
_ => (0.0, 0.0, dk),
};
let (_, vecs0) = match self.model_3d.diagonalize_3d(kx, ky, kz) {
Ok(v) => v,
Err(_) => return 0.0,
};
let (_, vecs1) = match self.model_3d.diagonalize_3d(kx + dkx, ky + dky, kz + dkz) {
Ok(v) => v,
Err(_) => return 0.0,
};
let mut a_mu = 0.0_f64;
for &b in &self.occupied_bands {
let u0 = vecs0.column(b);
let u1 = vecs1.column(b);
let mut inner = Complex::ZERO;
for j in 0..u0.len() {
inner = inner.add(&u0[j].conj().mul(&u1[j]));
}
a_mu += inner.im / dk;
}
a_mu
}
pub fn berry_curvature_at(&self, kx: f64, ky: f64, kz: f64, mu: usize, nu: usize) -> f64 {
let dk = 1e-3;
let (dkx_mu, dky_mu, dkz_mu) = direction_delta(mu, dk);
let (dkx_nu, dky_nu, dkz_nu) = direction_delta(nu, dk);
let a_nu_fwd = self.berry_connection_at(kx + dkx_mu, ky + dky_mu, kz + dkz_mu, nu);
let a_nu_bwd = self.berry_connection_at(kx - dkx_mu, ky - dky_mu, kz - dkz_mu, nu);
let d_mu_a_nu = (a_nu_fwd - a_nu_bwd) / (2.0 * dk);
let a_mu_fwd = self.berry_connection_at(kx + dkx_nu, ky + dky_nu, kz + dkz_nu, mu);
let a_mu_bwd = self.berry_connection_at(kx - dkx_nu, ky - dky_nu, kz - dkz_nu, mu);
let d_nu_a_mu = (a_mu_fwd - a_mu_bwd) / (2.0 * dk);
d_mu_a_nu - d_nu_a_mu
}
pub fn chern_simons_form(&self) -> f64 {
let total_chern = self.compute_2d_chern_at_kz(0.0);
if total_chern.abs() % 2 == 1 {
PI
} else {
0.0
}
}
fn compute_2d_chern_at_kz(&self, kz: f64) -> i32 {
let nx = self.n_kx;
let ny = self.n_ky;
let mut states: Vec<Vec<Vec<Vec<Complex>>>> = Vec::with_capacity(nx + 1);
for ix in 0..=nx {
let kx = 2.0 * PI * (ix as f64) / (nx as f64);
let mut row = Vec::with_capacity(ny + 1);
for iy in 0..=ny {
let ky = 2.0 * PI * (iy as f64) / (ny as f64);
let cols: Vec<Vec<Complex>> = match self.model_3d.diagonalize_3d(kx, ky, kz) {
Ok((_, vecs)) => self
.occupied_bands
.iter()
.map(|&b| vecs.column(b))
.collect(),
Err(_) => self
.occupied_bands
.iter()
.map(|_| vec![Complex::ZERO; self.model_3d.n_bands()])
.collect(),
};
row.push(cols);
}
states.push(row);
}
let mut flux_sum = 0.0_f64;
for ix in 0..nx {
let ix1 = ix + 1;
for iy in 0..ny {
let iy1 = iy + 1;
let mut band_flux = 0.0_f64;
for (bi, _) in self.occupied_bands.iter().enumerate() {
let u_x = scalar_link(&states[ix][iy][bi], &states[ix1][iy][bi]);
let u_y = scalar_link(&states[ix][iy][bi], &states[ix][iy1][bi]);
let u_x_py = scalar_link(&states[ix][iy1][bi], &states[ix1][iy1][bi]);
let u_y_px = scalar_link(&states[ix1][iy][bi], &states[ix1][iy1][bi]);
let pq = u_x.mul(&u_y_px).mul(&u_x_py.conj()).mul(&u_y.conj());
band_flux += pq.phase();
}
flux_sum += band_flux;
}
}
let chern_real = flux_sum / (2.0 * PI);
chern_real.round() as i32
}
pub fn axion_angle(&self) -> f64 {
self.chern_simons_form()
}
pub fn topological_magnetoelectric_polarizability(&self) -> f64 {
let theta = self.axion_angle();
let e2_over_h = CONDUCTANCE_QUANTUM * 0.5; (theta / (2.0 * PI)) * e2_over_h
}
pub fn axion_response(
&self,
e_field: Vector3<f64>,
b_field: Vector3<f64>,
) -> (Vector3<f64>, Vector3<f64>) {
let alpha = self.topological_magnetoelectric_polarizability();
let polarization = b_field * alpha;
let magnetization = e_field * (-alpha);
(polarization, magnetization)
}
}
#[derive(Debug, Clone)]
pub struct AxionMagnonPhoton {
pub axion_coupling_g: f64,
pub cavity_freq: f64,
pub magnon_freq: f64,
pub kappa_c: f64,
pub gamma_m: f64,
pub theta_axion: f64,
}
impl AxionMagnonPhoton {
pub fn new(
axion_coupling_g: f64,
cavity_freq: f64,
magnon_freq: f64,
kappa_c: f64,
gamma_m: f64,
theta_axion: f64,
) -> Result<Self> {
for (name, v) in [
("axion_coupling_g", axion_coupling_g),
("cavity_freq", cavity_freq),
("magnon_freq", magnon_freq),
("kappa_c", kappa_c),
("gamma_m", gamma_m),
("theta_axion", theta_axion),
] {
if !v.is_finite() {
return Err(error::invalid_param(name, "must be finite"));
}
}
if kappa_c < 0.0 {
return Err(error::invalid_param("kappa_c", "must be non-negative"));
}
if gamma_m < 0.0 {
return Err(error::invalid_param("gamma_m", "must be non-negative"));
}
Ok(Self {
axion_coupling_g,
cavity_freq,
magnon_freq,
kappa_c,
gamma_m,
theta_axion,
})
}
pub fn yig_topological_cavity() -> Self {
let omega_10ghz = 2.0 * PI * 10.0e9;
Self {
axion_coupling_g: 1e-3, cavity_freq: omega_10ghz,
magnon_freq: omega_10ghz,
kappa_c: 2.0 * PI * 1.0e6, gamma_m: 2.0 * PI * 1.0e6, theta_axion: PI, }
}
pub fn effective_coupling(&self) -> f64 {
self.axion_coupling_g * self.theta_axion / PI
}
pub fn detuning(&self) -> f64 {
self.cavity_freq - self.magnon_freq
}
pub fn cooperativity(&self) -> f64 {
let g = self.effective_coupling();
let denom = self.kappa_c * self.gamma_m;
if denom < 1e-30 {
0.0
} else {
4.0 * g * g / denom
}
}
pub fn magnon_photon_conversion_efficiency(&self) -> f64 {
let g = self.effective_coupling();
let delta = self.detuning();
let total_decay = self.kappa_c + self.gamma_m;
let denom = total_decay * total_decay + delta * delta;
if denom < 1e-60 {
0.0
} else {
4.0 * g * g / denom
}
}
pub fn parity_violation_angle(&self) -> f64 {
let e2 = E_CHARGE * E_CHARGE;
(self.theta_axion / (2.0 * PI)) * e2 / (EPSILON_0 * H_PLANCK * C_LIGHT)
}
}
fn scalar_link(u_a: &[Complex], u_b: &[Complex]) -> Complex {
let mut inner = Complex::ZERO;
for (a, b) in u_a.iter().zip(u_b.iter()) {
inner = inner.add(&a.conj().mul(b));
}
let norm = inner.norm();
if norm < 1e-15 {
Complex::ONE
} else {
inner.scale(1.0 / norm)
}
}
#[inline]
fn direction_delta(dir: usize, dk: f64) -> (f64, f64, f64) {
match dir {
0 => (dk, 0.0, 0.0),
1 => (0.0, dk, 0.0),
_ => (0.0, 0.0, dk),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topomagnon::band_model_3d::MagnonBandModel3D;
fn trivial_model() -> MagnonBandModel3D {
MagnonBandModel3D::cubic_haldane(1.0, 0.0, 0.0, 0.5)
}
fn make_axion(model: &MagnonBandModel3D) -> AxionElectrodynamics<'_> {
AxionElectrodynamics::new(model, vec![0], 8, 8, 8).unwrap()
}
#[test]
fn axion_new_rejects_empty_bands() {
let m = trivial_model();
assert!(AxionElectrodynamics::new(&m, vec![], 8, 8, 8).is_err());
}
#[test]
fn axion_new_rejects_out_of_range_band() {
let m = trivial_model();
assert!(AxionElectrodynamics::new(&m, vec![5], 8, 8, 8).is_err());
}
#[test]
fn axion_new_rejects_small_mesh() {
let m = trivial_model();
assert!(AxionElectrodynamics::new(&m, vec![0], 2, 8, 8).is_err());
assert!(AxionElectrodynamics::new(&m, vec![0], 8, 2, 8).is_err());
assert!(AxionElectrodynamics::new(&m, vec![0], 8, 8, 2).is_err());
}
#[test]
fn berry_connection_finite() {
let m = trivial_model();
let ae = make_axion(&m);
let a = ae.berry_connection_at(0.1, 0.2, 0.3, 0);
assert!(a.is_finite(), "Berry connection not finite: {a}");
}
#[test]
fn berry_curvature_antisymmetric() {
let m = MagnonBandModel3D::cubic_haldane(1.0, 0.0, 0.5, 0.1);
let ae = AxionElectrodynamics::new(&m, vec![0], 6, 6, 6).unwrap();
let kx = 0.3;
let ky = 0.5;
let kz = 0.7;
let omega_xy = ae.berry_curvature_at(kx, ky, kz, 0, 1);
let omega_yx = ae.berry_curvature_at(kx, ky, kz, 1, 0);
assert!(
(omega_xy + omega_yx).abs() < 0.5,
"Berry curvature not antisymmetric: {omega_xy} vs {omega_yx}"
);
}
#[test]
fn axion_angle_is_zero_or_pi() {
let m = trivial_model();
let ae = make_axion(&m);
let theta = ae.axion_angle();
assert!(
theta.abs() < 1e-10 || (theta - PI).abs() < 1e-10,
"Axion angle not 0 or π: {theta}"
);
}
#[test]
fn trivial_model_axion_angle_zero() {
let m = MagnonBandModel3D::cubic_haldane(1.0, 0.0, 0.0, 0.5);
let ae = AxionElectrodynamics::new(&m, vec![0], 8, 8, 8).unwrap();
let theta = ae.axion_angle();
assert!(theta.abs() < 1e-10 || (theta - PI).abs() < 1e-10);
}
#[test]
fn tme_for_trivial_is_zero() {
let m = MagnonBandModel3D::cubic_haldane(1.0, 0.0, 0.0, 0.5);
let ae = AxionElectrodynamics::new(&m, vec![0], 8, 8, 8).unwrap();
if ae.axion_angle().abs() < 1e-10 {
let alpha = ae.topological_magnetoelectric_polarizability();
assert!(
alpha.abs() < 1e-10,
"α_TME should be 0 for trivial: {alpha}"
);
}
}
#[test]
fn tme_for_theta_pi_is_half_conductance_quantum() {
let expected = CONDUCTANCE_QUANTUM * 0.25;
let theta = PI;
let alpha = (theta / (2.0 * PI)) * CONDUCTANCE_QUANTUM * 0.5;
assert!(
(alpha - expected).abs() < 1e-20,
"α_TME formula error: {alpha} vs {expected}"
);
}
#[test]
fn axion_response_p_proportional_to_b() {
let m = trivial_model();
let ae = make_axion(&m);
let e = Vector3::zero();
let b = Vector3::new(0.0, 0.0, 1.0); let (p, _) = ae.axion_response(e, b);
let alpha = ae.topological_magnetoelectric_polarizability();
assert!((p.z - alpha).abs() < 1e-25, "P_z != α·B_z: {}", p.z);
}
#[test]
fn axion_response_m_antiparallel_to_e() {
let m = trivial_model();
let ae = make_axion(&m);
let e = Vector3::new(1.0, 0.0, 0.0);
let b = Vector3::zero();
let (_, mag) = ae.axion_response(e, b);
let alpha = ae.topological_magnetoelectric_polarizability();
assert!((mag.x + alpha).abs() < 1e-25, "M_x != -α·E_x: {}", mag.x);
}
#[test]
fn yig_cavity_constructible() {
let sys = AxionMagnonPhoton::yig_topological_cavity();
assert!(sys.cavity_freq > 0.0);
assert!(sys.theta_axion > 0.0);
}
#[test]
fn effective_coupling_scales_with_theta() {
let s1 = AxionMagnonPhoton::new(1.0, 1.0, 1.0, 0.1, 0.1, PI).unwrap();
let s2 = AxionMagnonPhoton::new(1.0, 1.0, 1.0, 0.1, 0.1, 0.0).unwrap();
assert!((s1.effective_coupling() - 1.0).abs() < 1e-12);
assert!(s2.effective_coupling().abs() < 1e-12);
}
#[test]
fn detuning_zero_on_resonance() {
let sys = AxionMagnonPhoton::new(0.01, 1.0e10, 1.0e10, 1e6, 1e6, PI).unwrap();
assert!(sys.detuning().abs() < 1e-6);
}
#[test]
fn cooperativity_positive() {
let sys = AxionMagnonPhoton::yig_topological_cavity();
let c = sys.cooperativity();
assert!(c >= 0.0, "Cooperativity negative: {c}");
}
#[test]
fn conversion_efficiency_on_resonance_leq_one() {
let sys = AxionMagnonPhoton::new(1.0, 1.0, 1.0, 1.0, 1.0, PI).unwrap();
let eta = sys.magnon_photon_conversion_efficiency();
assert!(
(0.0..=1.0).contains(&eta),
"Conversion efficiency out of [0,1]: {eta}"
);
}
#[test]
fn parity_violation_angle_finite() {
let sys = AxionMagnonPhoton::yig_topological_cavity();
let theta_f = sys.parity_violation_angle();
assert!(theta_f.is_finite(), "Faraday angle not finite: {theta_f}");
assert!(
theta_f >= 0.0,
"Faraday angle should be non-negative for θ>0"
);
}
#[test]
fn new_rejects_negative_decay() {
assert!(AxionMagnonPhoton::new(1.0, 1.0, 1.0, -0.1, 1.0, PI).is_err());
assert!(AxionMagnonPhoton::new(1.0, 1.0, 1.0, 1.0, -0.1, PI).is_err());
}
}