#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::constants::{HBAR, KB};
use crate::error::{self, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KnbMechanism {
coupling: f64,
crystal_axis: Vector3<f64>,
}
impl KnbMechanism {
pub fn new(coupling: f64, crystal_axis: Vector3<f64>) -> Result<Self> {
if coupling <= 0.0 {
return Err(error::invalid_param(
"coupling",
"KNB spin-lattice coupling constant must be positive",
));
}
if crystal_axis.magnitude() == 0.0 {
return Err(error::invalid_param(
"crystal_axis",
"crystal axis must be a non-zero vector",
));
}
Ok(Self {
coupling,
crystal_axis,
})
}
pub fn tbmno3() -> Self {
Self {
coupling: 8.0e-4, crystal_axis: Vector3::new(0.0, 0.0, 1.0), }
}
pub fn polarization_from_spiral(
&self,
s_i: &Vector3<f64>,
s_j: &Vector3<f64>,
e_ij: &Vector3<f64>,
) -> Vector3<f64> {
let mag = e_ij.magnitude();
if mag == 0.0 {
return Vector3::zero();
}
let e_hat = Vector3::new(e_ij.x / mag, e_ij.y / mag, e_ij.z / mag);
let spin_current = s_i.cross(s_j);
let direction = e_hat.cross(&spin_current);
Vector3::new(
self.coupling * direction.x,
self.coupling * direction.y,
self.coupling * direction.z,
)
}
pub fn total_polarization_chain(
&self,
spins: &[Vector3<f64>],
lattice_vectors: &[Vector3<f64>],
) -> Result<Vector3<f64>> {
if spins.len() < 2 {
return Err(error::invalid_param(
"spins",
"chain must have at least 2 spins",
));
}
let expected_bonds = spins.len() - 1;
if lattice_vectors.len() != expected_bonds {
return Err(error::invalid_param(
"lattice_vectors",
&format!(
"expected {} bond vectors for {} spins, got {}",
expected_bonds,
spins.len(),
lattice_vectors.len()
),
));
}
let mut total = Vector3::zero();
for k in 0..expected_bonds {
let p_pair =
self.polarization_from_spiral(&spins[k], &spins[k + 1], &lattice_vectors[k]);
total = Vector3::new(total.x + p_pair.x, total.y + p_pair.y, total.z + p_pair.z);
}
Ok(total)
}
pub fn polarization_from_spin_spiral(
&self,
spiral_q: &Vector3<f64>,
rotation_axis: &Vector3<f64>,
) -> Vector3<f64> {
let q_mag = spiral_q.magnitude();
if q_mag == 0.0 {
return Vector3::zero();
}
let q_hat = Vector3::new(spiral_q.x / q_mag, spiral_q.y / q_mag, spiral_q.z / q_mag);
let cross = q_hat.cross(rotation_axis);
Vector3::new(
self.coupling * cross.x,
self.coupling * cross.y,
self.coupling * cross.z,
)
}
pub fn coupling(&self) -> f64 {
self.coupling
}
pub fn crystal_axis(&self) -> Vector3<f64> {
let n = self.crystal_axis.magnitude();
if n > 0.0 {
Vector3::new(
self.crystal_axis.x / n,
self.crystal_axis.y / n,
self.crystal_axis.z / n,
)
} else {
self.crystal_axis
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct InverseMagnetoelectric {
pub e_field: Vector3<f64>,
pub alpha: f64,
}
impl InverseMagnetoelectric {
pub fn new(e_field: Vector3<f64>, alpha: f64) -> Result<Self> {
if alpha == 0.0 {
return Err(error::invalid_param(
"alpha",
"ME coupling constant must be non-zero",
));
}
Ok(Self { e_field, alpha })
}
pub fn induced_magnetization(&self) -> Vector3<f64> {
Vector3::new(
self.alpha * self.e_field.x,
self.alpha * self.e_field.y,
self.alpha * self.e_field.z,
)
}
pub fn energy_density(&self, magnetization: &Vector3<f64>) -> f64 {
-self.alpha * self.e_field.dot(magnetization)
}
pub fn electric_field_control_of_magnetism(&self, _current_m: &Vector3<f64>) -> Vector3<f64> {
self.induced_magnetization()
}
}
pub fn spin_current_from_spiral(s1: &Vector3<f64>, s2: &Vector3<f64>) -> Vector3<f64> {
s1.cross(s2)
}
pub fn magnon_drag_contribution(temperature: f64, magnon_gap: f64, coupling: f64) -> f64 {
if temperature <= 0.0 || magnon_gap <= 0.0 {
return 0.0;
}
let exponent = HBAR * magnon_gap / (KB * temperature);
if exponent > 700.0 {
return 0.0;
}
let n_be = 1.0 / (exponent.exp() - 1.0);
coupling * n_be
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
const TOL: f64 = 1.0e-10;
#[test]
fn test_knb_cycloidal_spiral_nonzero() {
let knb = KnbMechanism::tbmno3();
let q = Vector3::new(1.0, 0.0, 0.0);
let n_hat = Vector3::new(0.0, 0.0, 1.0);
let p = knb.polarization_from_spin_spiral(&q, &n_hat);
assert!(p.magnitude() > TOL, "Cycloidal spiral must give non-zero P");
assert!(p.y < 0.0, "P_y should be negative for q=x̂, n̂=ẑ");
assert!(p.x.abs() < TOL, "P_x should be 0");
assert!(p.z.abs() < TOL, "P_z should be 0");
}
#[test]
fn test_knb_helical_spiral_zero() {
let knb = KnbMechanism::tbmno3();
let q = Vector3::new(0.0, 0.0, 1.0);
let n_hat = Vector3::new(0.0, 0.0, 1.0);
let p = knb.polarization_from_spin_spiral(&q, &n_hat);
assert!(p.magnitude() < TOL, "Helical spiral must give zero P");
}
#[test]
fn test_total_polarization_chain_afm_zero() {
let knb = KnbMechanism::tbmno3();
let spins: Vec<Vector3<f64>> = (0..6)
.map(|k| {
if k % 2 == 0 {
Vector3::new(1.0, 0.0, 0.0)
} else {
Vector3::new(-1.0, 0.0, 0.0)
}
})
.collect();
let bonds: Vec<Vector3<f64>> = (0..5).map(|_| Vector3::new(1.0, 0.0, 0.0)).collect();
let p = knb
.total_polarization_chain(&spins, &bonds)
.expect("valid chain");
assert!(p.magnitude() < TOL, "AFM chain should give zero total P");
}
#[test]
fn test_inverse_me_m_parallel_e() {
let e = Vector3::new(0.0, 0.0, 1.0e6); let alpha = 4.13e-12_f64;
let ime = InverseMagnetoelectric::new(e, alpha).expect("valid");
let m = ime.induced_magnetization();
assert!(
m.z > 0.0,
"M_z should be positive for positive alpha and E_z > 0"
);
assert!(m.x.abs() < TOL);
assert!(m.y.abs() < TOL);
}
#[test]
fn test_energy_density_negative_when_aligned() {
let e = Vector3::new(0.0, 0.0, 1.0e6);
let alpha = 4.13e-12_f64;
let ime = InverseMagnetoelectric::new(e, alpha).expect("valid");
let m = Vector3::new(0.0, 0.0, 1.0e5); let u = ime.energy_density(&m);
assert!(
u < 0.0,
"Energy should be negative for aligned E and M with α > 0; got {u}"
);
}
#[test]
fn test_spin_current_known_result() {
let s1 = Vector3::new(1.0, 0.0, 0.0);
let s2 = Vector3::new(0.0, 1.0, 0.0);
let j = spin_current_from_spiral(&s1, &s2);
assert!((j.x).abs() < TOL);
assert!((j.y).abs() < TOL);
assert!((j.z - 1.0).abs() < TOL);
}
#[test]
fn test_magnon_drag_increases_with_temperature() {
let omega_gap = KB * 10.0 / HBAR; let coupling = 1.0e-5;
let p_low = magnon_drag_contribution(5.0, omega_gap, coupling);
let p_mid = magnon_drag_contribution(50.0, omega_gap, coupling);
let p_high = magnon_drag_contribution(300.0, omega_gap, coupling);
assert!(
p_low < p_mid && p_mid < p_high,
"Magnon drag should increase with T: p_low={p_low}, p_mid={p_mid}, p_high={p_high}"
);
}
#[test]
fn test_tbmno3_crystal_axis() {
let knb = KnbMechanism::tbmno3();
let ax = knb.crystal_axis();
assert!(ax.x.abs() < TOL);
assert!(ax.y.abs() < TOL);
assert!((ax.z - 1.0).abs() < TOL);
}
#[test]
fn test_polarization_from_spin_spiral_helical_zero() {
let knb = KnbMechanism::new(1.0e-4, Vector3::new(0.0, 0.0, 1.0)).expect("valid KNB");
let q = Vector3::new(1.0, 0.0, 0.0);
let n = Vector3::new(1.0, 0.0, 0.0); let p = knb.polarization_from_spin_spiral(&q, &n);
assert!(
p.magnitude() < TOL,
"Helical (q∥n̂) must give |P|=0; got {}",
p.magnitude()
);
}
#[test]
fn test_polarization_from_spin_spiral_cycloidal_direction() {
let knb = KnbMechanism::tbmno3(); let q = Vector3::new(0.0, 1.0, 0.0); let n = Vector3::new(0.0, 0.0, 1.0); let p = knb.polarization_from_spin_spiral(&q, &n);
assert!(p.magnitude() > TOL, "Cycloidal spiral must give non-zero P");
assert!(p.x > 0.0, "P should point along +x̂ for q=ŷ, n̂=ẑ");
assert!(p.y.abs() < TOL);
assert!(p.z.abs() < TOL);
}
#[test]
fn test_knb_new_rejects_zero_coupling() {
let result = KnbMechanism::new(0.0, Vector3::new(0.0, 0.0, 1.0));
assert!(result.is_err(), "Zero coupling should be rejected");
}
#[test]
fn test_knb_new_rejects_zero_axis() {
let result = KnbMechanism::new(1.0e-4, Vector3::zero());
assert!(result.is_err(), "Zero crystal axis should be rejected");
}
#[test]
fn test_inverse_me_new_rejects_zero_alpha() {
let result = InverseMagnetoelectric::new(Vector3::new(1.0, 0.0, 0.0), 0.0);
assert!(result.is_err(), "Zero alpha should be rejected");
}
#[test]
fn test_magnon_drag_zero_temperature() {
let omega_gap = KB * 10.0 / HBAR;
let p = magnon_drag_contribution(0.0, omega_gap, 1.0e-5);
assert!(p == 0.0, "T=0 should give zero magnon drag");
}
#[test]
fn test_chain_wrong_bond_count() {
let knb = KnbMechanism::tbmno3();
let spins = vec![
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(-1.0, 0.0, 0.0),
];
let bonds = vec![
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
];
let result = knb.total_polarization_chain(&spins, &bonds);
assert!(result.is_err(), "Wrong bond count should return error");
}
#[allow(dead_code)]
const _HALF_PI: f64 = PI / 2.0;
}