use std::f64::consts::TAU;
use crate::constants::KB;
use crate::error::{self, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
pub struct ExchangeInteraction {
pub delta: Vector3<f64>,
pub j_exchange: f64,
}
impl ExchangeInteraction {
pub fn new(delta: Vector3<f64>, j_exchange: f64) -> Self {
Self { delta, j_exchange }
}
}
#[derive(Debug, Clone)]
pub struct LuttingerTisza {
interactions: Vec<ExchangeInteraction>,
spin: f64,
a_lattice: f64,
}
impl LuttingerTisza {
pub fn new(interactions: Vec<ExchangeInteraction>, spin: f64, a_lattice: f64) -> Result<Self> {
if spin <= 0.0 {
return Err(error::invalid_param(
"spin",
"spin quantum number must be positive",
));
}
if a_lattice <= 0.0 {
return Err(error::invalid_param(
"a_lattice",
"lattice constant must be positive",
));
}
if interactions.is_empty() {
return Err(error::invalid_param(
"interactions",
"at least one exchange interaction is required",
));
}
Ok(Self {
interactions,
spin,
a_lattice,
})
}
pub fn ferromagnet(j_nn: f64, a_lattice: f64, spin: f64) -> Self {
let interactions = vec![
ExchangeInteraction::new(Vector3::new(a_lattice, 0.0, 0.0), j_nn),
ExchangeInteraction::new(Vector3::new(-a_lattice, 0.0, 0.0), j_nn),
];
Self {
interactions,
spin,
a_lattice,
}
}
pub fn antiferromagnet(j_nn: f64, a_lattice: f64, spin: f64) -> Self {
let interactions = vec![
ExchangeInteraction::new(Vector3::new(a_lattice, 0.0, 0.0), j_nn),
ExchangeInteraction::new(Vector3::new(-a_lattice, 0.0, 0.0), j_nn),
];
Self {
interactions,
spin,
a_lattice,
}
}
pub fn j1j2_chain(j1: f64, j2: f64, a_lattice: f64, spin: f64) -> Self {
let a = a_lattice;
let interactions = vec![
ExchangeInteraction::new(Vector3::new(a, 0.0, 0.0), j1),
ExchangeInteraction::new(Vector3::new(-a, 0.0, 0.0), j1),
ExchangeInteraction::new(Vector3::new(2.0 * a, 0.0, 0.0), j2),
ExchangeInteraction::new(Vector3::new(-2.0 * a, 0.0, 0.0), j2),
];
Self {
interactions,
spin,
a_lattice,
}
}
pub fn exchange_fourier(&self, q: &Vector3<f64>) -> f64 {
self.interactions
.iter()
.map(|inter| inter.j_exchange * q.dot(&inter.delta).cos())
.sum()
}
pub fn find_ground_state_q(&self, n_q: usize) -> Vector3<f64> {
let n = n_q.max(2);
let bz_size = TAU / self.a_lattice;
let dq = bz_size / n as f64;
let mut max_j = f64::NEG_INFINITY;
let mut best_q = Vector3::zero();
let has_y = self.interactions.iter().any(|i| i.delta.y.abs() > 1e-30);
let has_z = self.interactions.iter().any(|i| i.delta.z.abs() > 1e-30);
let ny = if has_y { n } else { 1 };
let nz = if has_z { n } else { 1 };
for ix in 0..n {
let qx = ix as f64 * dq;
for iy in 0..ny {
let qy = if has_y { iy as f64 * dq } else { 0.0 };
for iz in 0..nz {
let qz = if has_z { iz as f64 * dq } else { 0.0 };
let q = Vector3::new(qx, qy, qz);
let j_q = self.exchange_fourier(&q);
if j_q > max_j {
max_j = j_q;
best_q = q;
}
}
}
}
best_q
}
pub fn ground_state_energy_per_spin(&self, q: &Vector3<f64>) -> f64 {
let j_q = self.exchange_fourier(q);
-j_q * self.spin * self.spin
}
pub fn is_spiral_ground_state(&self, tol: f64) -> bool {
let q_star = self.find_ground_state_q(200);
let q_x = q_star.x;
let pi_over_a = std::f64::consts::PI / self.a_lattice;
let xi_x = q_x * self.a_lattice / TAU;
let not_fm = xi_x > tol;
let not_afm = (q_x - pi_over_a).abs() > tol * pi_over_a;
not_fm && not_afm
}
pub fn ordering_temperature_estimate(&self) -> f64 {
let q_star = self.find_ground_state_q(100);
let j_q_star = self.exchange_fourier(&q_star).abs();
self.spin * (self.spin + 1.0) * j_q_star / (3.0 * KB)
}
pub fn spiral_pitch_angle(&self, q_optimal: &Vector3<f64>) -> f64 {
q_optimal.magnitude() * self.a_lattice
}
pub fn frustration_ratio(&self) -> f64 {
let mut min_dist = f64::INFINITY;
let mut max_dist = 0.0_f64;
for inter in &self.interactions {
let d = inter.delta.magnitude();
if d > 1e-30 {
min_dist = min_dist.min(d);
max_dist = max_dist.max(d);
}
}
if max_dist <= min_dist * (1.0 + 1e-6) {
return 0.0;
}
let j1_avg: f64 = self
.interactions
.iter()
.filter(|i| (i.delta.magnitude() - min_dist).abs() < 1e-10 * min_dist + 1e-30)
.map(|i| i.j_exchange)
.sum::<f64>()
/ self
.interactions
.iter()
.filter(|i| (i.delta.magnitude() - min_dist).abs() < 1e-10 * min_dist + 1e-30)
.count() as f64;
let j2_avg: f64 = self
.interactions
.iter()
.filter(|i| (i.delta.magnitude() - max_dist).abs() < 1e-10 * max_dist + 1e-30)
.map(|i| i.j_exchange)
.sum::<f64>()
/ self
.interactions
.iter()
.filter(|i| (i.delta.magnitude() - max_dist).abs() < 1e-10 * max_dist + 1e-30)
.count() as f64;
if j1_avg.abs() < 1e-30 {
return 0.0;
}
j2_avg / j1_avg
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::PI;
use super::*;
const TOL: f64 = 1e-6;
#[test]
fn test_fm_optimal_q_is_zero() {
let a = 3.0e-10_f64;
let lt = LuttingerTisza::ferromagnet(1.0e-21, a, 0.5);
let q_star = lt.find_ground_state_q(200);
let q_mag = q_star.magnitude();
let bz = TAU / a;
let xi = q_mag / bz; assert!(
xi < 0.05,
"FM (J>0) ground state should be at q≈0 (xi < 0.05), got xi = {}",
xi
);
}
#[test]
fn test_afm_optimal_q_is_pi_over_a() {
let a = 3.0e-10_f64;
let lt = LuttingerTisza::antiferromagnet(-1.0e-21, a, 0.5);
let q_star = lt.find_ground_state_q(200);
let q_x = q_star.x;
let pi_over_a = PI / a;
let rel_err = (q_x - pi_over_a).abs() / pi_over_a;
assert!(
rel_err < 0.05,
"AFM (J<0) ground state should be at q≈π/a, got q_x = {} (expected {})",
q_x,
pi_over_a
);
}
#[test]
fn test_j1j2_spiral_ground_state() {
let a = 3.0e-10_f64;
let j1 = 1.0e-21_f64;
let j2 = -0.5 * j1; let lt = LuttingerTisza::j1j2_chain(j1, j2, a, 0.5);
assert!(
lt.is_spiral_ground_state(0.05),
"J1-J2 chain with |J2|/J1=0.5 must have a spiral ground state"
);
}
#[test]
fn test_ordering_temperature_positive() {
let a = 3.0e-10_f64;
let j_nn = 1.0e-21_f64; let lt = LuttingerTisza::ferromagnet(j_nn, a, 0.5);
let t_c = lt.ordering_temperature_estimate();
assert!(t_c > 0.0, "ordering temperature must be positive");
assert!(
t_c < 1e6,
"ordering temperature should be physically reasonable, got {} K",
t_c
);
}
#[test]
fn test_frustration_ratio() {
let a = 3.0e-10_f64;
let j1 = 1.0e-21_f64;
let j2 = -0.3 * j1;
let lt = LuttingerTisza::j1j2_chain(j1, j2, a, 0.5);
let ratio = lt.frustration_ratio();
let expected = j2 / j1;
assert!(
(ratio - expected).abs() < TOL,
"frustration ratio = {}, expected {}",
ratio,
expected
);
}
#[test]
fn test_exchange_fourier_at_q_zero() {
let a = 3.0e-10_f64;
let j_nn = 2.0e-21_f64;
let lt = LuttingerTisza::ferromagnet(j_nn, a, 0.5);
let q0 = Vector3::zero();
let j_at_0 = lt.exchange_fourier(&q0);
let expected = 2.0 * j_nn;
assert!(
(j_at_0 - expected).abs() < TOL * j_nn,
"J(0) = {}, expected {}",
j_at_0,
expected
);
}
#[test]
fn test_fm_energy_negative() {
let a = 3.0e-10_f64;
let j_nn = 1.0e-21_f64;
let lt = LuttingerTisza::ferromagnet(j_nn, a, 0.5);
let q0 = Vector3::zero();
let energy = lt.ground_state_energy_per_spin(&q0);
assert!(
energy < 0.0,
"FM ground state energy must be negative, got {}",
energy
);
}
#[test]
fn test_spiral_pitch_angle() {
let a = 3.0e-10_f64;
let lt = LuttingerTisza::ferromagnet(1.0e-21, a, 0.5);
let q_fm = Vector3::zero();
let pitch_fm = lt.spiral_pitch_angle(&q_fm);
assert!(
pitch_fm.abs() < TOL,
"FM pitch angle should be 0, got {}",
pitch_fm
);
let q_afm = Vector3::new(PI / a, 0.0, 0.0);
let pitch_afm = lt.spiral_pitch_angle(&q_afm);
assert!(
(pitch_afm - PI).abs() < TOL,
"AFM pitch angle should be π, got {}",
pitch_afm
);
}
#[test]
fn test_new_validates_params() {
let inter = ExchangeInteraction::new(Vector3::new(3.0e-10, 0.0, 0.0), 1.0e-21);
assert!(LuttingerTisza::new(vec![inter.clone()], -1.0, 3.0e-10).is_err()); assert!(LuttingerTisza::new(vec![inter.clone()], 0.5, -3.0e-10).is_err()); assert!(LuttingerTisza::new(vec![], 0.5, 3.0e-10).is_err()); assert!(LuttingerTisza::new(vec![inter], 0.5, 3.0e-10).is_ok());
}
#[test]
fn test_j1j2_exchange_fourier_analytic() {
let a = 3.0e-10_f64;
let j1 = 1.0e-21_f64;
let j2 = -0.3e-21_f64;
let lt = LuttingerTisza::j1j2_chain(j1, j2, a, 0.5);
let qa = 0.4_f64; let q = Vector3::new(qa / a, 0.0, 0.0);
let j_numerical = lt.exchange_fourier(&q);
let j_analytic = 2.0 * j1 * qa.cos() + 2.0 * j2 * (2.0 * qa).cos();
assert!(
(j_numerical - j_analytic).abs() < TOL * j1.abs(),
"J(q) numerical = {}, analytic = {}",
j_numerical,
j_analytic
);
}
}