use crate::constants::MU_0;
use crate::error::{self, Result};
use crate::vector3::Vector3;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StrainTensor {
pub components: [[f64; 3]; 3],
}
impl StrainTensor {
pub fn new(components: [[f64; 3]; 3]) -> Self {
let mut sym = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
sym[i][j] = 0.5 * (components[i][j] + components[j][i]);
}
}
Self { components: sym }
}
pub fn zero() -> Self {
Self {
components: [[0.0; 3]; 3],
}
}
pub fn uniaxial(strain: f64, axis: usize) -> Result<Self> {
if axis > 2 {
return Err(error::invalid_param("axis", "must be 0, 1, or 2"));
}
let mut components = [[0.0; 3]; 3];
components[axis][axis] = strain;
Ok(Self { components })
}
pub fn biaxial_in_plane(strain: f64, poisson_ratio: f64) -> Self {
let mut components = [[0.0; 3]; 3];
components[0][0] = strain;
components[1][1] = strain;
components[2][2] = -2.0 * poisson_ratio / (1.0 - poisson_ratio) * strain;
Self { components }
}
pub fn shear_xy(strain: f64) -> Self {
let mut components = [[0.0; 3]; 3];
components[0][1] = strain;
components[1][0] = strain;
Self { components }
}
pub fn diagonal(&self) -> (f64, f64, f64) {
(
self.components[0][0],
self.components[1][1],
self.components[2][2],
)
}
pub fn trace(&self) -> f64 {
self.components[0][0] + self.components[1][1] + self.components[2][2]
}
pub fn is_symmetric(&self, tol: f64) -> bool {
for i in 0..3 {
for j in (i + 1)..3 {
if (self.components[i][j] - self.components[j][i]).abs() > tol {
return false;
}
}
}
true
}
pub fn elastic_energy_density_isotropic(&self, lame_lambda: f64, shear_modulus: f64) -> f64 {
let tr = self.trace();
let mut eps_sq = 0.0;
for i in 0..3 {
for j in 0..3 {
eps_sq += self.components[i][j] * self.components[i][j];
}
}
0.5 * lame_lambda * tr * tr + shear_modulus * eps_sq
}
}
impl Default for StrainTensor {
fn default() -> Self {
Self::zero()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StressTensor {
pub components: [[f64; 3]; 3],
}
impl StressTensor {
pub fn new(components: [[f64; 3]; 3]) -> Self {
let mut sym = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
sym[i][j] = 0.5 * (components[i][j] + components[j][i]);
}
}
Self { components: sym }
}
pub fn zero() -> Self {
Self {
components: [[0.0; 3]; 3],
}
}
pub fn uniaxial(stress: f64, axis: usize) -> Result<Self> {
if axis > 2 {
return Err(error::invalid_param("axis", "must be 0, 1, or 2"));
}
let mut components = [[0.0; 3]; 3];
components[axis][axis] = stress;
Ok(Self { components })
}
pub fn hydrostatic(pressure: f64) -> Self {
let mut components = [[0.0; 3]; 3];
components[0][0] = -pressure;
components[1][1] = -pressure;
components[2][2] = -pressure;
Self { components }
}
pub fn trace(&self) -> f64 {
self.components[0][0] + self.components[1][1] + self.components[2][2]
}
pub fn von_mises(&self) -> f64 {
let s = self.components;
let s11 = s[0][0];
let s22 = s[1][1];
let s33 = s[2][2];
let s12 = s[0][1];
let s23 = s[1][2];
let s13 = s[0][2];
let term1 =
(s11 - s22) * (s11 - s22) + (s22 - s33) * (s22 - s33) + (s33 - s11) * (s33 - s11);
let term2 = 6.0 * (s12 * s12 + s23 * s23 + s13 * s13);
(0.5 * (term1 + term2)).sqrt()
}
}
impl Default for StressTensor {
fn default() -> Self {
Self::zero()
}
}
#[derive(Debug, Clone, Copy)]
pub struct MagnetoelasticMaterial {
pub name: &'static str,
pub b1: f64,
pub b2: f64,
pub lambda_100: f64,
pub lambda_111: f64,
pub lambda_s: f64,
pub youngs_modulus: f64,
pub ms: f64,
}
impl MagnetoelasticMaterial {
pub fn terfenol_d() -> Self {
Self {
name: "Terfenol-D",
b1: -9.38e6,
b2: -6.16e6,
lambda_100: 90.0e-6,
lambda_111: 1640.0e-6,
lambda_s: 1020.0e-6, youngs_modulus: 30.0e9,
ms: 0.8e6,
}
}
pub fn galfenol() -> Self {
Self {
name: "Galfenol",
b1: -4.0e6,
b2: -3.0e6,
lambda_100: 280.0e-6,
lambda_111: 20.0e-6,
lambda_s: 124.0e-6, youngs_modulus: 60.0e9,
ms: 1.3e6,
}
}
pub fn nickel() -> Self {
Self {
name: "Nickel",
b1: 9.38e6,
b2: 10.0e6,
lambda_100: -46.0e-6,
lambda_111: -24.0e-6,
lambda_s: -32.8e-6, youngs_modulus: 200.0e9,
ms: 0.49e6,
}
}
pub fn cofeb() -> Self {
Self {
name: "CoFeB",
b1: -3.5e6,
b2: -2.0e6,
lambda_100: 25.0e-6,
lambda_111: 25.0e-6,
lambda_s: 25.0e-6,
youngs_modulus: 160.0e9,
ms: 1.0e6,
}
}
pub fn iron() -> Self {
Self {
name: "Iron",
b1: -3.43e6,
b2: 7.83e6,
lambda_100: 21.0e-6,
lambda_111: -21.0e-6,
lambda_s: -4.2e-6, youngs_modulus: 211.0e9,
ms: 1.71e6,
}
}
pub fn compute_lambda_s(&self) -> f64 {
(2.0 / 5.0) * self.lambda_100 + (3.0 / 5.0) * self.lambda_111
}
pub fn typical_poisson_ratio(&self) -> f64 {
0.3
}
pub fn approximate_shear_modulus(&self) -> f64 {
self.youngs_modulus / (2.0 * (1.0 + self.typical_poisson_ratio()))
}
}
pub fn magnetoelastic_energy_density_cubic(
material: &MagnetoelasticMaterial,
strain: &StrainTensor,
magnetization_dir: &Vector3<f64>,
) -> Result<f64> {
let m_mag = magnetization_dir.magnitude();
if m_mag < 1e-30 {
return Err(error::invalid_param(
"magnetization_dir",
"must have non-zero magnitude",
));
}
let m = *magnetization_dir * (1.0 / m_mag);
let e = &strain.components;
let b1_term = material.b1 * (e[0][0] * m.x * m.x + e[1][1] * m.y * m.y + e[2][2] * m.z * m.z);
let b2_term = material.b2 * (e[0][1] * m.x * m.y + e[1][2] * m.y * m.z + e[0][2] * m.x * m.z);
Ok(b1_term + b2_term)
}
pub fn magnetoelastic_energy(
material: &MagnetoelasticMaterial,
strain: &StrainTensor,
magnetization_dir: &Vector3<f64>,
volume: f64,
) -> Result<f64> {
let density = magnetoelastic_energy_density_cubic(material, strain, magnetization_dir)?;
Ok(density * volume)
}
pub fn isotropic_magnetostrictive_strain(lambda_s: f64, cos_theta: f64) -> f64 {
1.5 * lambda_s * (cos_theta * cos_theta - 1.0 / 3.0)
}
pub fn magnetostrictive_strain_tensor(
material: &MagnetoelasticMaterial,
magnetization_dir: &Vector3<f64>,
) -> Result<StrainTensor> {
let m_mag = magnetization_dir.magnitude();
if m_mag < 1e-30 {
return Err(error::invalid_param(
"magnetization_dir",
"must have non-zero magnitude",
));
}
let m = *magnetization_dir * (1.0 / m_mag);
let l100 = material.lambda_100;
let l111 = material.lambda_111;
let mut c = [[0.0; 3]; 3];
c[0][0] = 1.5 * l100 * (m.x * m.x - 1.0 / 3.0);
c[1][1] = 1.5 * l100 * (m.y * m.y - 1.0 / 3.0);
c[2][2] = 1.5 * l100 * (m.z * m.z - 1.0 / 3.0);
c[0][1] = 1.5 * l111 * m.x * m.y;
c[1][0] = c[0][1];
c[0][2] = 1.5 * l111 * m.x * m.z;
c[2][0] = c[0][2];
c[1][2] = 1.5 * l111 * m.y * m.z;
c[2][1] = c[1][2];
Ok(StrainTensor { components: c })
}
pub fn saturation_strain(lambda_s: f64) -> f64 {
1.5 * lambda_s * (1.0 - 1.0 / 3.0)
}
pub fn villari_effective_field_magnitude(
material: &MagnetoelasticMaterial,
stress: f64,
) -> Result<f64> {
if material.ms.abs() < 1e-30 {
return Err(error::invalid_param(
"ms",
"saturation magnetization must be non-zero",
));
}
let h_eff = (3.0 * material.lambda_s * stress) / (MU_0 * material.ms);
Ok(h_eff)
}
pub fn villari_effective_field_vector(
material: &MagnetoelasticMaterial,
stress_tensor: &StressTensor,
magnetization_dir: &Vector3<f64>,
) -> Result<Vector3<f64>> {
let m_mag = magnetization_dir.magnitude();
if m_mag < 1e-30 {
return Err(error::invalid_param(
"magnetization_dir",
"must have non-zero magnitude",
));
}
if material.ms.abs() < 1e-30 {
return Err(error::invalid_param(
"ms",
"saturation magnetization must be non-zero",
));
}
let m = *magnetization_dir * (1.0 / m_mag);
let s = &stress_tensor.components;
let prefactor = 3.0 * material.lambda_s / (MU_0 * material.ms);
let hx = prefactor * (s[0][0] * m.x + s[0][1] * m.y + s[0][2] * m.z);
let hy = prefactor * (s[1][0] * m.x + s[1][1] * m.y + s[1][2] * m.z);
let hz = prefactor * (s[2][0] * m.x + s[2][1] * m.y + s[2][2] * m.z);
Ok(Vector3::new(hx, hy, hz))
}
pub fn stress_induced_anisotropy(lambda_s: f64, stress: f64) -> f64 {
-1.5 * lambda_s * stress
}
pub fn interlayer_magnetoelastic_coupling(
material1: &MagnetoelasticMaterial,
material2: &MagnetoelasticMaterial,
thickness1: f64,
thickness2: f64,
spacer_compliance: f64,
) -> f64 {
let strain_factor1 = 1.5 * material1.lambda_s * material1.youngs_modulus * thickness1;
let strain_factor2 = 1.5 * material2.lambda_s * material2.youngs_modulus * thickness2;
strain_factor1 * strain_factor2 * spacer_compliance
}
pub fn piezoelectric_strain(d31: f64, d33: f64, electric_field: f64) -> StrainTensor {
let eps_in_plane = d31 * electric_field;
let eps_out_of_plane = d33 * electric_field;
let mut components = [[0.0; 3]; 3];
components[0][0] = eps_in_plane;
components[1][1] = eps_in_plane;
components[2][2] = eps_out_of_plane;
StrainTensor { components }
}
#[derive(Debug, Clone, Copy)]
pub struct PiezoelectricSubstrate {
pub name: &'static str,
pub d31: f64,
pub d33: f64,
pub max_field: f64,
}
impl PiezoelectricSubstrate {
pub fn pzt() -> Self {
Self {
name: "PZT",
d31: -170.0e-12,
d33: 400.0e-12,
max_field: 2.0e6,
}
}
pub fn pmn_pt() -> Self {
Self {
name: "PMN-PT",
d31: -1000.0e-12,
d33: 2000.0e-12,
max_field: 1.0e6,
}
}
pub fn batio3() -> Self {
Self {
name: "BaTiO3",
d31: -78.0e-12,
d33: 190.0e-12,
max_field: 3.0e6,
}
}
pub fn strain_from_voltage(
&self,
voltage: f64,
substrate_thickness: f64,
) -> Result<StrainTensor> {
if substrate_thickness <= 0.0 {
return Err(error::invalid_param(
"substrate_thickness",
"must be positive",
));
}
let e_field = voltage / substrate_thickness;
Ok(piezoelectric_strain(self.d31, self.d33, e_field))
}
pub fn max_in_plane_strain(&self) -> f64 {
(self.d31 * self.max_field).abs()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nickel_magnetostriction_negative() {
let ni = MagnetoelasticMaterial::nickel();
assert!(
ni.lambda_s < 0.0,
"Nickel should have negative magnetostriction, got {}",
ni.lambda_s
);
assert!(
(ni.lambda_s - (-34.0e-6)).abs() < 5.0e-6,
"Nickel λ_s should be close to -34 ppm, got {} ppm",
ni.lambda_s * 1e6
);
}
#[test]
fn test_iron_magnetostriction_anisotropic() {
let fe = MagnetoelasticMaterial::iron();
assert!(
(fe.lambda_100 - 21.0e-6).abs() < 2.0e-6,
"Iron λ_100 should be ~21 ppm, got {} ppm",
fe.lambda_100 * 1e6
);
assert!(
(fe.lambda_111 - (-21.0e-6)).abs() < 2.0e-6,
"Iron λ_111 should be ~-21 ppm, got {} ppm",
fe.lambda_111 * 1e6
);
assert!(
fe.lambda_s.abs() < 10.0e-6,
"Iron λ_s should be small, got {} ppm",
fe.lambda_s * 1e6
);
}
#[test]
fn test_terfenol_d_largest_magnetostriction() {
let terfenol = MagnetoelasticMaterial::terfenol_d();
let galfenol = MagnetoelasticMaterial::galfenol();
let nickel = MagnetoelasticMaterial::nickel();
let cofeb = MagnetoelasticMaterial::cofeb();
let iron = MagnetoelasticMaterial::iron();
assert!(
terfenol.lambda_s.abs() > galfenol.lambda_s.abs(),
"Terfenol-D |λ_s| should exceed Galfenol"
);
assert!(
terfenol.lambda_s.abs() > nickel.lambda_s.abs(),
"Terfenol-D |λ_s| should exceed Nickel"
);
assert!(
terfenol.lambda_s.abs() > cofeb.lambda_s.abs(),
"Terfenol-D |λ_s| should exceed CoFeB"
);
assert!(
terfenol.lambda_s.abs() > iron.lambda_s.abs(),
"Terfenol-D |λ_s| should exceed Iron"
);
}
#[test]
fn test_strain_tensor_symmetry() {
let asymmetric = [[0.001, 0.002, 0.0], [0.0, 0.003, 0.004], [0.0, 0.0, 0.005]];
let tensor = StrainTensor::new(asymmetric);
assert!(
tensor.is_symmetric(1e-15),
"StrainTensor should always be symmetric after construction"
);
let expected_xy = 0.5 * (0.002 + 0.0);
assert!(
(tensor.components[0][1] - expected_xy).abs() < 1e-15,
"ε_xy should be symmetrized"
);
assert!(
(tensor.components[1][0] - expected_xy).abs() < 1e-15,
"ε_yx should equal ε_xy"
);
}
#[test]
fn test_magnetoelastic_energy_zero_strain() {
let ni = MagnetoelasticMaterial::nickel();
let zero_strain = StrainTensor::zero();
let m_dir = Vector3::new(1.0, 0.0, 0.0);
let energy = magnetoelastic_energy_density_cubic(&ni, &zero_strain, &m_dir)
.expect("should compute energy for zero strain");
assert!(
energy.abs() < 1e-20,
"Magnetoelastic energy should be zero for zero strain, got {}",
energy
);
}
#[test]
fn test_villari_effective_field_direction() {
let cofeb = MagnetoelasticMaterial::cofeb();
assert!(cofeb.lambda_s > 0.0, "CoFeB should have positive λ_s");
let tensile_x = StressTensor::uniaxial(100.0e6, 0).expect("should create uniaxial stress");
let m_along_x = Vector3::new(1.0, 0.0, 0.0);
let h_vec = villari_effective_field_vector(&cofeb, &tensile_x, &m_along_x)
.expect("should compute Villari field");
assert!(
h_vec.x > 0.0,
"Villari field x-component should be positive for positive λ_s and tensile stress along x, got {}",
h_vec.x
);
assert!(
h_vec.y.abs() < 1e-10,
"Villari field y-component should be ~0, got {}",
h_vec.y
);
assert!(
h_vec.z.abs() < 1e-10,
"Villari field z-component should be ~0, got {}",
h_vec.z
);
}
#[test]
fn test_magnetostrictive_strain_tensor_cubic() {
let fe = MagnetoelasticMaterial::iron();
let m_100 = Vector3::new(1.0, 0.0, 0.0);
let strain =
magnetostrictive_strain_tensor(&fe, &m_100).expect("should compute strain tensor");
let expected_exx = fe.lambda_100;
assert!(
(strain.components[0][0] - expected_exx).abs() < 1e-10,
"ε_xx for m along [100] should be λ_100 = {}, got {}",
expected_exx,
strain.components[0][0]
);
let expected_eyy = -fe.lambda_100 / 2.0;
assert!(
(strain.components[1][1] - expected_eyy).abs() < 1e-10,
"ε_yy for m along [100] should be -λ_100/2 = {}, got {}",
expected_eyy,
strain.components[1][1]
);
assert!(
strain.components[0][1].abs() < 1e-20,
"Shear ε_xy should be zero for m along [100]"
);
}
#[test]
fn test_piezoelectric_strain_pmn_pt() {
let pmn_pt = PiezoelectricSubstrate::pmn_pt();
let e_field = 1.0e6;
let strain = piezoelectric_strain(pmn_pt.d31, pmn_pt.d33, e_field);
let expected_in_plane = pmn_pt.d31 * e_field;
assert!(
(strain.components[0][0] - expected_in_plane).abs() < 1e-10,
"In-plane strain should be d31*E"
);
let pzt = PiezoelectricSubstrate::pzt();
assert!(
pmn_pt.d31.abs() > pzt.d31.abs(),
"PMN-PT d31 should be larger than PZT"
);
}
#[test]
fn test_stress_induced_anisotropy_sign() {
let k_sigma = stress_induced_anisotropy(25.0e-6, 100.0e6);
assert!(
k_sigma < 0.0,
"K_σ should be negative for positive λ_s and tensile stress, got {}",
k_sigma
);
let k_sigma_ni = stress_induced_anisotropy(-34.0e-6, 100.0e6);
assert!(
k_sigma_ni > 0.0,
"K_σ should be positive for negative λ_s and tensile stress, got {}",
k_sigma_ni
);
}
#[test]
fn test_polycrystalline_lambda_s_formula() {
let fe = MagnetoelasticMaterial::iron();
let computed = fe.compute_lambda_s();
let expected = (2.0 / 5.0) * fe.lambda_100 + (3.0 / 5.0) * fe.lambda_111;
assert!(
(computed - expected).abs() < 1e-15,
"compute_lambda_s should match formula"
);
}
#[test]
fn test_von_mises_stress_uniaxial() {
let sigma = 100.0e6;
let stress = StressTensor::uniaxial(sigma, 0).expect("should create uniaxial stress");
let vm = stress.von_mises();
assert!(
(vm - sigma.abs()).abs() / sigma.abs() < 1e-10,
"von Mises for uniaxial should equal |σ|, got {} vs {}",
vm,
sigma.abs()
);
}
}