use std::f64::consts::{PI, TAU};
use crate::error::{self, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpiralType {
Cycloidal,
Helical,
Conical,
FanStructure,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpiralChirality {
Clockwise,
CounterClockwise,
Achiral,
}
#[derive(Debug, Clone)]
pub struct SpinSpiral {
pub q_vector: Vector3<f64>,
pub rotation_axis: Vector3<f64>,
pub cone_angle_rad: f64,
pub amplitude: f64,
pub spiral_type: SpiralType,
}
impl SpinSpiral {
pub fn new(
q_vector: Vector3<f64>,
rotation_axis: Vector3<f64>,
cone_angle_rad: f64,
amplitude: f64,
) -> Result<Self> {
if !(0.0..=PI).contains(&cone_angle_rad) {
return Err(error::invalid_param(
"cone_angle_rad",
"cone angle must be in [0, π]",
));
}
if amplitude <= 0.0 {
return Err(error::invalid_param(
"amplitude",
"amplitude must be positive",
));
}
if rotation_axis.magnitude_squared() < 1e-30 {
return Err(error::invalid_param(
"rotation_axis",
"rotation axis must have nonzero magnitude",
));
}
let q_hat = if q_vector.magnitude_squared() > 1e-30 {
q_vector.normalize()
} else {
Vector3::zero()
};
let axis_hat = rotation_axis.normalize();
let q_axis_dot = q_hat.dot(&axis_hat).abs();
let spiral_type = if (cone_angle_rad - PI / 2.0).abs() < 1e-9 {
if q_axis_dot < 0.1 {
SpiralType::Cycloidal
} else if q_axis_dot > 0.9 {
SpiralType::Helical
} else {
SpiralType::Cycloidal
}
} else {
SpiralType::Conical
};
Ok(Self {
q_vector,
rotation_axis: axis_hat,
cone_angle_rad,
amplitude,
spiral_type,
})
}
pub fn cycloidal(q_vector: Vector3<f64>, normal_axis: Vector3<f64>, amplitude: f64) -> Self {
let q_hat = q_vector.normalize();
let n_hat = normal_axis.normalize();
let rot_axis = q_hat.cross(&n_hat);
let rot_axis = if rot_axis.magnitude_squared() > 1e-30 {
rot_axis.normalize()
} else {
if q_hat.x.abs() < 0.9 {
Vector3::new(1.0, 0.0, 0.0).cross(&q_hat).normalize()
} else {
Vector3::new(0.0, 1.0, 0.0).cross(&q_hat).normalize()
}
};
Self {
q_vector,
rotation_axis: rot_axis,
cone_angle_rad: PI / 2.0,
amplitude,
spiral_type: SpiralType::Cycloidal,
}
}
pub fn helical(q_vector: Vector3<f64>, amplitude: f64) -> Self {
let q_hat = if q_vector.magnitude_squared() > 1e-30 {
q_vector.normalize()
} else {
Vector3::unit_z()
};
Self {
q_vector,
rotation_axis: q_hat,
cone_angle_rad: PI / 2.0,
amplitude,
spiral_type: SpiralType::Helical,
}
}
pub fn conical(
q_vector: Vector3<f64>,
rotation_axis: Vector3<f64>,
cone_angle_rad: f64,
amplitude: f64,
) -> Result<Self> {
if !(0.0..=PI).contains(&cone_angle_rad) {
return Err(error::invalid_param(
"cone_angle_rad",
"cone angle must be in [0, π]",
));
}
if amplitude <= 0.0 {
return Err(error::invalid_param(
"amplitude",
"amplitude must be positive",
));
}
if rotation_axis.magnitude_squared() < 1e-30 {
return Err(error::invalid_param(
"rotation_axis",
"rotation axis must have nonzero magnitude",
));
}
Ok(Self {
q_vector,
rotation_axis: rotation_axis.normalize(),
cone_angle_rad,
amplitude,
spiral_type: SpiralType::Conical,
})
}
pub fn terbium_manganese_oxide() -> Self {
let b_lattice = 5.84e-10_f64;
let q_b = 0.28 * TAU / b_lattice;
let q_vector = Vector3::new(0.0, q_b, 0.0);
let rotation_axis = Vector3::unit_x();
Self {
q_vector,
rotation_axis,
cone_angle_rad: PI / 2.0,
amplitude: 2.0,
spiral_type: SpiralType::Cycloidal,
}
}
fn frame_vectors(&self) -> (Vector3<f64>, Vector3<f64>) {
let e_hat = self.rotation_axis; let ref_v = if e_hat.x.abs() < 0.9 {
Vector3::unit_x()
} else {
Vector3::unit_y()
};
let proj = e_hat * ref_v.dot(&e_hat);
let e1 = (ref_v - proj).normalize();
let e2 = e_hat.cross(&e1);
(e1, e2)
}
pub fn magnetization_at(&self, r: &Vector3<f64>) -> Vector3<f64> {
let phase = self.q_vector.dot(r); let (e1, e2) = self.frame_vectors();
let sin_theta = self.cone_angle_rad.sin();
let cos_theta = self.cone_angle_rad.cos();
let planar_x = e1 * (phase.cos() * sin_theta);
let planar_y = e2 * (phase.sin() * sin_theta);
let axial = self.rotation_axis * cos_theta;
(planar_x + planar_y + axial) * self.amplitude
}
pub fn chirality(&self) -> SpiralChirality {
let q_mag = self.q_vector.magnitude();
if q_mag < 1e-30 {
return SpiralChirality::Achiral;
}
let q_hat = self.q_vector.normalize();
let r0 = Vector3::zero();
let r1 = q_hat * (PI / (2.0 * q_mag));
let m0 = self.magnetization_at(&r0);
let m1 = self.magnetization_at(&r1);
let cross = m0.cross(&m1);
let triple = cross.dot(&q_hat);
if triple > 1e-10 {
SpiralChirality::CounterClockwise
} else if triple < -1e-10 {
SpiralChirality::Clockwise
} else {
SpiralChirality::Achiral
}
}
pub fn electric_polarization_direction(&self) -> Option<Vector3<f64>> {
let q_mag = self.q_vector.magnitude();
if q_mag < 1e-30 {
return None;
}
let q_dot_e = self.q_vector.dot(&self.rotation_axis);
let p_vec = self.q_vector - self.rotation_axis * q_dot_e;
let p_mag = p_vec.magnitude();
if p_mag < 1e-30 {
None
} else {
Some(p_vec.normalize())
}
}
pub fn wavelength(&self) -> f64 {
let q_mag = self.q_vector.magnitude();
if q_mag > 1e-30 {
TAU / q_mag
} else {
f64::INFINITY
}
}
pub fn pitch(&self) -> f64 {
self.wavelength()
}
pub fn cone_angle_deg(&self) -> f64 {
self.cone_angle_rad.to_degrees()
}
pub fn is_incommensurate(&self, lattice_constant: f64) -> bool {
let q_mag = self.q_vector.magnitude();
if q_mag < 1e-30 || lattice_constant <= 0.0 {
return false;
}
let xi = q_mag * lattice_constant / TAU;
for denom in 1_u32..=20 {
for numer in 0_u32..=denom {
let frac = numer as f64 / denom as f64;
if (xi - frac).abs() < 1e-4 {
return false; }
}
}
true }
pub fn spin_structure_factor(&self, k: &Vector3<f64>) -> (f64, f64) {
let (e1, e2) = self.frame_vectors();
let sin_theta = self.cone_angle_rad.sin();
let a = self.amplitude;
let diff_plus = (*k - self.q_vector).magnitude();
let diff_minus = (*k + self.q_vector).magnitude();
let q_mag = self.q_vector.magnitude().max(1e-30);
let width = 0.01 * q_mag;
let lorentz_plus = width * width / (diff_plus * diff_plus + width * width);
let lorentz_minus = width * width / (diff_minus * diff_minus + width * width);
let perp_e1 = e1.dot(k) / k.magnitude().max(1e-30);
let perp_e2 = e2.dot(k) / k.magnitude().max(1e-30);
let transverse_fraction = (1.0 - perp_e1 * perp_e1 - perp_e2 * perp_e2).clamp(0.0, 1.0);
let base_weight = (a * sin_theta).powi(2) * 0.25 * (1.0 - transverse_fraction);
let s_plus = base_weight * lorentz_plus;
let s_minus = base_weight * lorentz_minus;
(s_plus, s_minus)
}
pub fn landau_lifshitz_energy(&self, j_nn: f64, a_lattice: f64) -> f64 {
let qx = self.q_vector.x;
let qy = self.q_vector.y;
let qz = self.q_vector.z;
let a = a_lattice;
let s = self.amplitude;
let cos_sum = (qx * a).cos()
+ (-qx * a).cos()
+ (qy * a).cos()
+ (-qy * a).cos()
+ (qz * a).cos()
+ (-qz * a).cos();
-j_nn * cos_sum * s * s
}
}
#[cfg(test)]
mod tests {
use std::f64::consts::{PI, TAU};
use super::*;
const TOL: f64 = 1e-9;
const TOL_MEDIUM: f64 = 1e-6;
#[test]
fn test_cycloidal_gives_polarization() {
let q = Vector3::new(1.0e7, 0.0, 0.0);
let normal = Vector3::unit_z();
let spiral = SpinSpiral::cycloidal(q, normal, 1.0);
let p_dir = spiral.electric_polarization_direction();
assert!(
p_dir.is_some(),
"cycloidal spiral must have finite polarization"
);
let p = p_dir.unwrap();
assert!((p.magnitude() - 1.0).abs() < TOL_MEDIUM);
}
#[test]
fn test_helical_gives_no_polarization() {
let q = Vector3::new(0.0, 0.0, 1.0e7);
let spiral = SpinSpiral::helical(q, 1.0);
let p_dir = spiral.electric_polarization_direction();
assert!(
p_dir.is_none(),
"helical spiral (q ∥ rotation_axis) must have zero polarization"
);
}
#[test]
fn test_magnetization_unit_norm() {
let q = Vector3::new(2.0e7, 1.0e7, 0.0);
let normal = Vector3::unit_z();
let spiral = SpinSpiral::cycloidal(q, normal, 1.0);
let positions = [
Vector3::zero(),
Vector3::new(1.0e-9, 0.0, 0.0),
Vector3::new(0.0, 2.0e-9, 0.0),
Vector3::new(5.0e-9, 3.0e-9, 1.0e-9),
];
for r in &positions {
let m = spiral.magnetization_at(r);
let norm = m.magnitude();
assert!(
(norm - 1.0).abs() < 1e-10,
"magnetization norm = {} at r = ({}, {}, {}), expected 1.0",
norm,
r.x,
r.y,
r.z
);
}
}
#[test]
fn test_conical_magnetization_norm() {
let q = Vector3::new(1.0e7, 0.0, 0.0);
let axis = Vector3::unit_z();
let amplitude = 2.0;
let spiral = SpinSpiral::conical(q, axis, PI / 4.0, amplitude).unwrap();
let r = Vector3::new(3.14e-10, 0.0, 0.0);
let m = spiral.magnetization_at(&r);
assert!(
(m.magnitude() - amplitude).abs() < 1e-9,
"conical spiral |M| = {}, expected {}",
m.magnitude(),
amplitude
);
}
#[test]
fn test_wavelength() {
let q_mag = 1.0e8_f64;
let q = Vector3::new(q_mag, 0.0, 0.0);
let spiral = SpinSpiral::helical(q, 1.0);
let expected = TAU / q_mag;
assert!(
(spiral.wavelength() - expected).abs() < TOL,
"wavelength = {}, expected {}",
spiral.wavelength(),
expected
);
assert!((spiral.pitch() - expected).abs() < TOL);
}
#[test]
fn test_structure_factor_peaks_at_q() {
let q_mag = 1.0e8_f64;
let q = Vector3::new(q_mag, 0.0, 0.0);
let spiral = SpinSpiral::cycloidal(q, Vector3::unit_z(), 1.0);
let (s_at_q, _) = spiral.spin_structure_factor(&q);
let k_far = Vector3::new(0.5e8, 0.0, 0.0);
let (s_far, _) = spiral.spin_structure_factor(&k_far);
assert!(
s_at_q > s_far,
"structure factor should be larger at k=q ({}) than at k far ({})",
s_at_q,
s_far
);
}
#[test]
fn test_tbmno3_q_vector() {
let spiral = SpinSpiral::terbium_manganese_oxide();
let b_lattice = 5.84e-10_f64;
let expected_q = 0.28 * TAU / b_lattice;
let q_mag = spiral.q_vector.magnitude();
let rel_err = (q_mag - expected_q).abs() / expected_q;
assert!(
rel_err < 1e-10,
"|q| = {}, expected {} (0.28 × 2π/b)",
q_mag,
expected_q
);
assert_eq!(spiral.spiral_type, SpiralType::Cycloidal);
assert!(spiral.electric_polarization_direction().is_some());
}
#[test]
fn test_fm_energy_sc_lattice() {
let a = 3.0e-10_f64;
let j_nn = 1.0e-21;
let s = 1.0;
let q = Vector3::zero();
let spiral = SpinSpiral {
q_vector: q,
rotation_axis: Vector3::unit_z(),
cone_angle_rad: 0.0, amplitude: s,
spiral_type: SpiralType::Conical,
};
let energy = spiral.landau_lifshitz_energy(j_nn, a);
let expected = -6.0 * j_nn * s * s;
assert!(
(energy - expected).abs() < TOL_MEDIUM * j_nn.abs(),
"FM energy = {}, expected {}",
energy,
expected
);
}
#[test]
fn test_afm_energy_sc_lattice() {
let a = 3.0e-10_f64;
let j_nn = 1.0e-21;
let s = 1.0;
let q_afm = PI / a;
let spiral = SpinSpiral {
q_vector: Vector3::new(q_afm, q_afm, q_afm),
rotation_axis: Vector3::unit_z(),
cone_angle_rad: PI / 2.0,
amplitude: s,
spiral_type: SpiralType::Cycloidal,
};
let energy = spiral.landau_lifshitz_energy(j_nn, a);
let expected = 6.0 * j_nn * s * s;
assert!(
(energy - expected).abs() < TOL_MEDIUM * j_nn.abs(),
"AFM energy = {}, expected {}",
energy,
expected
);
}
#[test]
fn test_cone_angle_degrees() {
let spiral = SpinSpiral::helical(Vector3::new(1.0e7, 0.0, 0.0), 1.0);
assert!((spiral.cone_angle_deg() - 90.0).abs() < TOL_MEDIUM);
}
#[test]
fn test_new_invalid_cone_angle() {
let q = Vector3::new(1.0e7, 0.0, 0.0);
let axis = Vector3::unit_z();
assert!(SpinSpiral::new(q, axis, -0.1, 1.0).is_err());
assert!(SpinSpiral::new(q, axis, PI + 0.1, 1.0).is_err());
assert!(SpinSpiral::new(q, axis, PI / 3.0, 1.0).is_ok());
}
#[test]
fn test_incommensurate() {
let a = 3.0e-10_f64;
let q_comm = Vector3::new(TAU / a, 0.0, 0.0);
let spiral_comm = SpinSpiral::helical(q_comm, 1.0);
assert!(
!spiral_comm.is_incommensurate(a),
"q = 2π/a must be commensurate"
);
let q_inc = Vector3::new(0.28 * TAU / a, 0.0, 0.0);
let spiral_inc = SpinSpiral::helical(q_inc, 1.0);
assert!(
spiral_inc.is_incommensurate(a),
"q = 0.28 × 2π/a must be incommensurate"
);
}
}