#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::constants::C_LIGHT;
use crate::error::{self, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum MultiferroicType {
TypeI,
TypeII,
TypeIII,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MagnetoelectricTensor {
alpha: [[f64; 3]; 3],
material_type: MultiferroicType,
polarization_s: Vector3<f64>,
magnetization_s: Vector3<f64>,
t_ferroelectric: f64,
t_magnetic: f64,
}
impl MagnetoelectricTensor {
pub fn new(
alpha: [[f64; 3]; 3],
material_type: MultiferroicType,
polarization_s: Vector3<f64>,
magnetization_s: Vector3<f64>,
t_ferroelectric: f64,
t_magnetic: f64,
) -> Result<Self> {
for (i, row) in alpha.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
if !val.is_finite() {
return Err(error::invalid_param(
"alpha",
&format!("element [{i}][{j}] = {val} is not finite"),
));
}
}
}
if t_ferroelectric <= 0.0 {
return Err(error::invalid_param(
"t_ferroelectric",
"ferroelectric transition temperature must be positive",
));
}
if t_magnetic <= 0.0 {
return Err(error::invalid_param(
"t_magnetic",
"magnetic transition temperature must be positive",
));
}
Ok(Self {
alpha,
material_type,
polarization_s,
magnetization_s,
t_ferroelectric,
t_magnetic,
})
}
pub fn bife_o3() -> Self {
let p_s_component = 0.90 / 3_f64.sqrt();
let alpha = [
[6.0e-12, 0.0, 0.0],
[0.0, 6.0e-12, 0.0],
[0.0, 0.0, 3.0e-12],
];
Self {
alpha,
material_type: MultiferroicType::TypeI,
polarization_s: Vector3::new(p_s_component, p_s_component, p_s_component),
magnetization_s: Vector3::zero(),
t_ferroelectric: 1100.0,
t_magnetic: 643.0,
}
}
pub fn tb_mn_o3() -> Self {
let p_s = 8.0e-4; let alpha = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 1.0e-9]];
Self {
alpha,
material_type: MultiferroicType::TypeII,
polarization_s: Vector3::new(0.0, 0.0, p_s),
magnetization_s: Vector3::zero(),
t_ferroelectric: 27.0,
t_magnetic: 41.0,
}
}
pub fn cr2_o3() -> Self {
let alpha = [
[1.3e-12, 0.0, 0.0],
[0.0, 1.3e-12, 0.0],
[0.0, 0.0, 4.13e-12],
];
Self {
alpha,
material_type: MultiferroicType::TypeI,
polarization_s: Vector3::zero(),
magnetization_s: Vector3::zero(),
t_ferroelectric: 307.0, t_magnetic: 307.0,
}
}
pub fn electric_polarization_from_field(&self, h_field: &Vector3<f64>) -> Vector3<f64> {
let h = [h_field.x, h_field.y, h_field.z];
let a = &self.alpha;
let px = a[0][0] * h[0] + a[0][1] * h[1] + a[0][2] * h[2];
let py = a[1][0] * h[0] + a[1][1] * h[1] + a[1][2] * h[2];
let pz = a[2][0] * h[0] + a[2][1] * h[1] + a[2][2] * h[2];
Vector3::new(px, py, pz)
}
pub fn magnetization_from_efield(&self, e_field: &Vector3<f64>) -> Vector3<f64> {
let e = [e_field.x, e_field.y, e_field.z];
let a = &self.alpha;
let mx = a[0][0] * e[0] + a[1][0] * e[1] + a[2][0] * e[2];
let my = a[0][1] * e[0] + a[1][1] * e[1] + a[2][1] * e[2];
let mz = a[0][2] * e[0] + a[1][2] * e[1] + a[2][2] * e[2];
Vector3::new(mx, my, mz)
}
pub fn is_linear_magnetoelectric(&self) -> bool {
self.alpha.iter().any(|row| row.iter().any(|&v| v != 0.0))
}
pub fn magnetoelectric_susceptibility(&self) -> f64 {
let sum_sq: f64 = self
.alpha
.iter()
.flat_map(|row| row.iter())
.map(|&v| v * v)
.sum();
sum_sq.sqrt()
}
pub fn bound_on_me_coupling(&self) -> f64 {
let chi_e = 12.0_f64;
let chi_m = 1.0e-3_f64;
(chi_e * chi_m).sqrt() / C_LIGHT
}
pub fn alpha_tensor(&self) -> [[f64; 3]; 3] {
self.alpha
}
pub fn polarization_s(&self) -> Vector3<f64> {
self.polarization_s
}
pub fn magnetization_s(&self) -> Vector3<f64> {
self.magnetization_s
}
pub fn material_type(&self) -> MultiferroicType {
self.material_type
}
pub fn t_ferroelectric(&self) -> f64 {
self.t_ferroelectric
}
pub fn t_magnetic(&self) -> f64 {
self.t_magnetic
}
pub fn is_above_ferroelectric_transition(&self, temperature: f64) -> bool {
temperature > self.t_ferroelectric
}
pub fn is_above_magnetic_transition(&self, temperature: f64) -> bool {
temperature > self.t_magnetic
}
pub fn switching_energy(&self, e_field: f64, h_field: f64) -> f64 {
let p_mag = self.polarization_s.magnitude();
let m_mag = self.magnetization_s.magnitude();
p_mag * e_field + m_mag * h_field
}
}
pub fn dzyaloshinskii_moriya_polarization(
s_i: &Vector3<f64>,
s_j: &Vector3<f64>,
bond_vector: &Vector3<f64>,
) -> Vector3<f64> {
let mag = bond_vector.magnitude();
if mag == 0.0 {
return Vector3::zero();
}
let e_ij = Vector3::new(
bond_vector.x / mag,
bond_vector.y / mag,
bond_vector.z / mag,
);
let spin_current = s_i.cross(s_j);
e_ij.cross(&spin_current)
}
pub fn exchange_striction_polarization(
s_i: &Vector3<f64>,
s_j: &Vector3<f64>,
sensitivity: f64,
) -> f64 {
sensitivity * s_i.dot(s_j)
}
pub fn toroidal_moment(
positions: &[Vector3<f64>],
moments: &[Vector3<f64>],
) -> Result<Vector3<f64>> {
if positions.is_empty() {
return Err(error::invalid_param("positions", "slice must not be empty"));
}
if positions.len() != moments.len() {
return Err(error::invalid_param(
"moments",
&format!(
"length {} does not match positions length {}",
moments.len(),
positions.len()
),
));
}
let mut sum = Vector3::zero();
for (r, m) in positions.iter().zip(moments.iter()) {
let rxm = r.cross(m);
sum = Vector3::new(sum.x + rxm.x, sum.y + rxm.y, sum.z + rxm.z);
}
let n = positions.len() as f64;
Ok(Vector3::new(
sum.x / (2.0 * n),
sum.y / (2.0 * n),
sum.z / (2.0 * n),
))
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1.0e-10;
#[test]
fn test_bife_o3_polarization_range() {
let bfo = MagnetoelectricTensor::bife_o3();
let p_mag = bfo.polarization_s().magnitude();
assert!(
(0.5..=1.0).contains(&p_mag),
"BiFeO3 |P_s| = {p_mag} C/m² is outside the expected 0.5–1.0 C/m² range"
);
assert_eq!(bfo.material_type(), MultiferroicType::TypeI);
}
#[test]
fn test_tb_mn_o3_temperature_ordering() {
let tbmno3 = MagnetoelectricTensor::tb_mn_o3();
assert!(
tbmno3.t_ferroelectric() < tbmno3.t_magnetic(),
"TbMnO3 must have T_FE ({}) < T_N ({})",
tbmno3.t_ferroelectric(),
tbmno3.t_magnetic()
);
assert_eq!(tbmno3.material_type(), MultiferroicType::TypeII);
}
#[test]
fn test_cr2_o3_electric_polarization_from_field() {
let cr2o3 = MagnetoelectricTensor::cr2_o3();
let h_z = 1.0e6; let h = Vector3::new(0.0, 0.0, h_z);
let p = cr2o3.electric_polarization_from_field(&h);
let expected_pz = 4.13e-12 * h_z;
assert!(
(p.z - expected_pz).abs() < TOL,
"P_z = {} ≠ expected {expected_pz}",
p.z
);
assert!(p.x.abs() < TOL, "P_x should be zero for z-only H");
assert!(p.y.abs() < TOL, "P_y should be zero for z-only H");
}
#[test]
fn test_inverse_me_magnetization_from_efield() {
let cr2o3 = MagnetoelectricTensor::cr2_o3();
let e_z = 1.0e6; let e = Vector3::new(0.0, 0.0, e_z);
let m = cr2o3.magnetization_from_efield(&e);
let expected_mz = 4.13e-12 * e_z;
assert!(
(m.z - expected_mz).abs() < TOL,
"M_z = {} ≠ expected {expected_mz}",
m.z
);
}
#[test]
fn test_dzyaloshinskii_moriya_polarization_direction() {
let s_i = Vector3::new(1.0, 0.0, 0.0);
let s_j = Vector3::new(0.0, 1.0, 0.0);
let bond = Vector3::new(1.0, 0.0, 0.0);
let p = dzyaloshinskii_moriya_polarization(&s_i, &s_j, &bond);
assert!(
p.magnitude() > TOL,
"DM polarization should be non-zero for this geometry"
);
assert!(
p.y < 0.0,
"DM polarization should point along -y, got y = {}",
p.y
);
assert!(p.x.abs() < TOL);
assert!(p.z.abs() < TOL);
}
#[test]
fn test_exchange_striction_antiparallel_spins() {
let s_i = Vector3::new(1.0, 0.0, 0.0);
let s_j = Vector3::new(-1.0, 0.0, 0.0);
let lambda = 1.0e-5; let dp = exchange_striction_polarization(&s_i, &s_j, lambda);
assert!(
dp < 0.0,
"Anti-parallel spins should give negative ΔP, got {dp}"
);
}
#[test]
fn test_toroidal_moment_two_site() {
let positions = vec![Vector3::new(1.0, 0.0, 0.0), Vector3::new(-1.0, 0.0, 0.0)];
let moments = vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.0, 0.0, -1.0)];
let t = toroidal_moment(&positions, &moments).expect("toroidal_moment should succeed");
assert!((t.x).abs() < TOL, "T_x = {} should be 0", t.x);
assert!((t.y - (-0.5)).abs() < TOL, "T_y = {} should be -0.5", t.y);
assert!((t.z).abs() < TOL, "T_z = {} should be 0", t.z);
}
#[test]
fn test_magnetoelectric_susceptibility_positive() {
let cr2o3 = MagnetoelectricTensor::cr2_o3();
let chi = cr2o3.magnetoelectric_susceptibility();
assert!(chi > 0.0, "ME susceptibility must be positive");
}
#[test]
fn test_bound_on_me_coupling_positive_finite() {
let cr2o3 = MagnetoelectricTensor::cr2_o3();
let bound = cr2o3.bound_on_me_coupling();
assert!(bound > 0.0, "BHS bound must be positive");
assert!(bound.is_finite(), "BHS bound must be finite");
assert!(
bound > 1.0e-12 && bound < 1.0e-6,
"BHS bound = {bound} is unreasonably large/small"
);
}
#[test]
fn test_temperature_phase_flags() {
let bfo = MagnetoelectricTensor::bife_o3();
assert!(!bfo.is_above_ferroelectric_transition(300.0));
assert!(!bfo.is_above_magnetic_transition(300.0));
assert!(!bfo.is_above_magnetic_transition(643.0)); assert!(bfo.is_above_magnetic_transition(700.0));
assert!(bfo.is_above_ferroelectric_transition(1200.0));
}
#[test]
fn test_switching_energy_bife_o3() {
let bfo = MagnetoelectricTensor::bife_o3();
let e = 1.0e6; let h = 1.0e6; let u = bfo.switching_energy(e, h);
assert!(u > 0.0, "Switching energy must be positive");
}
#[test]
fn test_from_field_zero_for_zero_alpha() {
let zero_alpha = [[0.0; 3]; 3];
let mat = MagnetoelectricTensor::new(
zero_alpha,
MultiferroicType::TypeI,
Vector3::zero(),
Vector3::zero(),
500.0,
400.0,
)
.expect("valid parameters");
let h = Vector3::new(1.0e6, 1.0e6, 1.0e6);
let p = mat.electric_polarization_from_field(&h);
assert!(
p.magnitude() < TOL,
"|P| = {} should be zero for α=0",
p.magnitude()
);
}
#[test]
fn test_type_classification() {
assert_eq!(
MagnetoelectricTensor::bife_o3().material_type(),
MultiferroicType::TypeI
);
assert_eq!(
MagnetoelectricTensor::tb_mn_o3().material_type(),
MultiferroicType::TypeII
);
assert_eq!(
MagnetoelectricTensor::cr2_o3().material_type(),
MultiferroicType::TypeI
);
}
#[test]
fn test_toroidal_moment_length_mismatch() {
let positions = vec![Vector3::new(1.0, 0.0, 0.0)];
let moments = vec![Vector3::new(0.0, 0.0, 1.0), Vector3::new(0.0, 0.0, -1.0)];
let result = toroidal_moment(&positions, &moments);
assert!(result.is_err(), "Mismatched lengths should return an error");
}
#[test]
fn test_new_rejects_nonfinite_alpha() {
let mut alpha = [[0.0; 3]; 3];
alpha[1][2] = f64::NAN;
let result = MagnetoelectricTensor::new(
alpha,
MultiferroicType::TypeI,
Vector3::zero(),
Vector3::zero(),
500.0,
300.0,
);
assert!(result.is_err(), "NaN in alpha should be rejected");
}
}