use std::f64::consts::{FRAC_PI_4, PI};
use crate::constants::{GAMMA, MU_0};
use crate::dynamics::llg::calc_dm_dt;
use crate::dynamics::{DormandPrince45, Integrator};
use crate::error::{invalid_param, numerical_error, Result};
use crate::mech::magnetoelastic::{
stress_induced_anisotropy, villari_effective_field_vector, MagnetoelasticMaterial,
PiezoelectricSubstrate, StrainTensor, StressTensor,
};
use crate::mech::saw::{SawMagnetoacoustics, SawSource};
use crate::vector3::Vector3;
pub const OPTIMAL_COUPLING_ANGLE_RAD: f64 = FRAC_PI_4;
pub trait StrainDrive {
fn strain_tensor_at(&self, t: f64) -> StrainTensor;
fn drive_frequency_hz(&self) -> f64;
}
#[derive(Debug, Clone)]
pub struct SawStrainDrive {
pub saw: SawSource,
pub x_probe_m: f64,
pub z_probe_m: f64,
}
impl SawStrainDrive {
pub fn new(saw: SawSource, x_probe_m: f64, z_probe_m: f64) -> Self {
Self {
saw,
x_probe_m,
z_probe_m,
}
}
}
impl StrainDrive for SawStrainDrive {
fn strain_tensor_at(&self, t: f64) -> StrainTensor {
let eps_xx = self.saw.strain_at_point(self.x_probe_m, self.z_probe_m, t);
let eps_zz = -self.saw.substrate.poisson_ratio * eps_xx;
StrainTensor::new([[eps_xx, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, eps_zz]])
}
fn drive_frequency_hz(&self) -> f64 {
self.saw.frequency_hz
}
}
#[derive(Debug, Clone)]
pub struct PiezoAcStrainDrive {
pub substrate: PiezoelectricSubstrate,
pub substrate_thickness_m: f64,
pub voltage_amplitude_v: f64,
pub frequency_hz: f64,
}
impl PiezoAcStrainDrive {
pub fn new(
substrate: PiezoelectricSubstrate,
substrate_thickness_m: f64,
voltage_amplitude_v: f64,
frequency_hz: f64,
) -> Result<Self> {
if substrate_thickness_m <= 0.0 {
return Err(invalid_param(
"substrate_thickness_m",
"substrate thickness must be positive",
));
}
if frequency_hz <= 0.0 {
return Err(invalid_param(
"frequency_hz",
"AC drive frequency must be positive",
));
}
let peak_field = voltage_amplitude_v.abs() / substrate_thickness_m;
if peak_field > substrate.max_field {
return Err(invalid_param(
"voltage_amplitude_v",
"peak field (voltage_amplitude_v / substrate_thickness_m) exceeds the substrate's rated maximum field",
));
}
Ok(Self {
substrate,
substrate_thickness_m,
voltage_amplitude_v,
frequency_hz,
})
}
}
impl StrainDrive for PiezoAcStrainDrive {
fn strain_tensor_at(&self, t: f64) -> StrainTensor {
let voltage_t = self.voltage_amplitude_v * (2.0 * PI * self.frequency_hz * t).sin();
self.substrate
.strain_from_voltage(voltage_t, self.substrate_thickness_m)
.unwrap_or_else(|_| StrainTensor::zero())
}
fn drive_frequency_hz(&self) -> f64 {
self.frequency_hz
}
}
#[derive(Debug, Clone, Copy)]
pub struct StrainDrivenStepSample {
pub time_s: f64,
pub magnetization: Vector3<f64>,
pub cone_angle_rad: f64,
}
#[derive(Debug, Clone)]
pub struct StrainDrivenTrajectory {
pub samples: Vec<StrainDrivenStepSample>,
pub max_cone_angle_rad: f64,
pub max_raw_norm_deviation: f64,
pub mean_power_areal_density_w_per_m2: f64,
}
#[derive(Debug, Clone)]
pub struct StrainDrivenLlgDriver<S: StrainDrive> {
pub strain_drive: S,
pub villari_material: MagnetoelasticMaterial,
pub gamma: f64,
pub alpha: f64,
pub anisotropy_k: f64,
pub h_ext_a_per_m: f64,
pub film_thickness_m: f64,
pub coupling_angle_rad: f64,
}
impl<S: StrainDrive> StrainDrivenLlgDriver<S> {
#[allow(clippy::too_many_arguments)]
pub fn new(
strain_drive: S,
villari_material: MagnetoelasticMaterial,
gamma: f64,
alpha: f64,
anisotropy_k: f64,
h_ext_a_per_m: f64,
film_thickness_m: f64,
coupling_angle_rad: f64,
) -> Result<Self> {
if villari_material.ms <= 0.0 {
return Err(invalid_param(
"villari_material.ms",
"saturation magnetization must be positive",
));
}
if gamma <= 0.0 {
return Err(invalid_param(
"gamma",
"gyromagnetic ratio must be positive",
));
}
if alpha.is_nan() || alpha <= 0.0 {
return Err(invalid_param(
"alpha",
"Gilbert damping must be strictly positive for a driven-dissipative response",
));
}
if h_ext_a_per_m < 0.0 {
return Err(invalid_param(
"h_ext_a_per_m",
"external bias field must be non-negative (out-of-plane-saturation convention)",
));
}
if film_thickness_m <= 0.0 {
return Err(invalid_param(
"film_thickness_m",
"film thickness must be positive",
));
}
if !coupling_angle_rad.is_finite() {
return Err(invalid_param("coupling_angle_rad", "must be finite"));
}
Ok(Self {
strain_drive,
villari_material,
gamma,
alpha,
anisotropy_k,
h_ext_a_per_m,
film_thickness_m,
coupling_angle_rad,
})
}
fn tilted_stress_tensor(&self, eps_xx: f64, eps_zz: f64) -> StressTensor {
let youngs_modulus_pa = self.villari_material.youngs_modulus;
let sigma_xx_saw = youngs_modulus_pa * eps_xx;
let sigma_zz_saw = youngs_modulus_pa * eps_zz;
let cos_phi = self.coupling_angle_rad.cos();
let sin_phi = self.coupling_angle_rad.sin();
let sigma_xx = sigma_xx_saw * cos_phi * cos_phi + sigma_zz_saw * sin_phi * sin_phi;
let sigma_zz = sigma_xx_saw * sin_phi * sin_phi + sigma_zz_saw * cos_phi * cos_phi;
let sigma_xz = (sigma_xx_saw - sigma_zz_saw) * sin_phi * cos_phi;
StressTensor::new([
[sigma_xx, 0.0, sigma_xz],
[0.0, 0.0, 0.0],
[sigma_xz, 0.0, sigma_zz],
])
}
fn total_h_eff_tesla(&self, m: Vector3<f64>, t: f64) -> Vector3<f64> {
let strain = self.strain_drive.strain_tensor_at(t);
let (eps_xx, _eps_yy, eps_zz) = strain.diagonal();
let stress = self.tilted_stress_tensor(eps_xx, eps_zz);
let h_villari_a_per_m = villari_effective_field_vector(&self.villari_material, &stress, &m)
.unwrap_or_else(|_| Vector3::zero());
let h_bias_a_per_m = Vector3::unit_z() * self.h_ext_a_per_m;
let h_k_a_per_m = 2.0 * self.anisotropy_k / (MU_0 * self.villari_material.ms);
let h_aniso_a_per_m = Vector3::unit_z() * (h_k_a_per_m * m.z);
(h_bias_a_per_m + h_aniso_a_per_m + h_villari_a_per_m) * MU_0
}
fn rhs(&self, m: Vector3<f64>, t: f64) -> Vector3<f64> {
let h_eff_tesla = self.total_h_eff_tesla(m, t);
calc_dm_dt(m, h_eff_tesla, self.gamma, self.alpha)
}
fn dissipation_power_density_w_per_m3(
&self,
m: Vector3<f64>,
h_eff_tesla: Vector3<f64>,
) -> f64 {
let cross = m.cross(&h_eff_tesla);
self.villari_material.ms * self.gamma * self.alpha / (1.0 + self.alpha * self.alpha)
* cross.dot(&cross)
}
pub fn induced_anisotropy_constant_j_per_m3(&self, t: f64) -> f64 {
let strain = self.strain_drive.strain_tensor_at(t);
let (eps_xx, _eps_yy, _eps_zz) = strain.diagonal();
let sigma_xx = self.villari_material.youngs_modulus * eps_xx;
stress_induced_anisotropy(self.villari_material.lambda_s, sigma_xx)
}
pub fn evolve(
&self,
m0: Vector3<f64>,
dt_s: f64,
n_periods: u32,
) -> Result<StrainDrivenTrajectory> {
if dt_s <= 0.0 {
return Err(invalid_param("dt_s", "time step must be positive"));
}
if n_periods == 0 {
return Err(invalid_param(
"n_periods",
"must evolve for at least one full drive period",
));
}
if m0.magnitude() < 1e-30 {
return Err(invalid_param(
"m0",
"initial magnetization must have non-zero magnitude",
));
}
let period_s = 1.0 / self.strain_drive.drive_frequency_hz();
let total_time_s = period_s * f64::from(n_periods);
let n_steps = (total_time_s / dt_s).ceil().max(1.0) as usize;
let last_period_start_s = total_time_s - period_s;
let mut m = m0.normalize();
let mut t = 0.0_f64;
let mut samples = Vec::with_capacity(n_steps + 1);
let mut max_cone_angle_rad = cone_angle_rad(m);
let mut max_raw_norm_deviation = 0.0_f64;
let mut dissipated_energy_areal_j_per_m2 = 0.0_f64;
samples.push(StrainDrivenStepSample {
time_s: t,
magnetization: m,
cone_angle_rad: max_cone_angle_rad,
});
let rhs = |state: &[Vector3<f64>], time: f64| -> Vec<Vector3<f64>> {
let m_state = state.first().copied().unwrap_or_else(Vector3::zero);
vec![self.rhs(m_state, time)]
};
for _ in 0..n_steps {
let state = [m];
let mut integrator = DormandPrince45::new();
let output = integrator.step(&state, t, dt_s, &rhs)?;
let m_raw = output
.new_state
.first()
.copied()
.ok_or_else(|| numerical_error("integrator returned an empty state vector"))?;
let raw_dev = (m_raw.magnitude() - 1.0).abs();
if raw_dev > max_raw_norm_deviation {
max_raw_norm_deviation = raw_dev;
}
m = m_raw.normalize();
t += dt_s;
let cone = cone_angle_rad(m);
if cone > max_cone_angle_rad {
max_cone_angle_rad = cone;
}
if t > last_period_start_s {
let h_eff_tesla = self.total_h_eff_tesla(m, t);
let p_vol = self.dissipation_power_density_w_per_m3(m, h_eff_tesla);
dissipated_energy_areal_j_per_m2 += p_vol * self.film_thickness_m * dt_s;
}
samples.push(StrainDrivenStepSample {
time_s: t,
magnetization: m,
cone_angle_rad: cone,
});
}
let mean_power_areal_density_w_per_m2 = dissipated_energy_areal_j_per_m2 / period_s;
Ok(StrainDrivenTrajectory {
samples,
max_cone_angle_rad,
max_raw_norm_deviation,
mean_power_areal_density_w_per_m2,
})
}
}
impl StrainDrivenLlgDriver<SawStrainDrive> {
pub fn from_saw_magnetoacoustics(
device: &SawMagnetoacoustics,
villari_material: MagnetoelasticMaterial,
x_probe_m: f64,
z_probe_m: f64,
coupling_angle_rad: f64,
) -> Result<Self> {
let strain_drive = SawStrainDrive::new(device.saw.clone(), x_probe_m, z_probe_m);
Self::new(
strain_drive,
villari_material,
GAMMA,
device.material.alpha,
device.material.anisotropy_k,
device.h_ext,
device.film_thickness_m,
coupling_angle_rad,
)
}
}
#[inline]
fn cone_angle_rad(m: Vector3<f64>) -> f64 {
m.z.clamp(-1.0, 1.0).acos()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mech::saw::{MagnetoelasticMaterial as SawMaterial, PiezoSubstrate};
fn nickel_saw_device(saw: SawSource, h_ext: f64) -> SawMagnetoacoustics {
SawMagnetoacoustics::new(saw, SawMaterial::nickel(), 20.0e-9, h_ext)
.expect("valid SawMagnetoacoustics parameters")
}
#[test]
fn test_saw_strain_drive_matches_source_directly() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw.clone(), 1.0e-6, 0.0);
let (eps_xx, eps_yy, eps_zz) = drive.strain_tensor_at(1.0e-10).diagonal();
let expected_xx = saw.strain_at_point(1.0e-6, 0.0, 1.0e-10);
assert!(
(eps_xx - expected_xx).abs() < 1.0e-20,
"SawStrainDrive eps_xx should match SawSource::strain_at_point exactly"
);
assert_eq!(eps_yy, 0.0, "SAW model has no eps_yy component");
let expected_zz = -saw.substrate.poisson_ratio * expected_xx;
assert!(
(eps_zz - expected_zz).abs() < 1.0e-20,
"SawStrainDrive eps_zz should follow the Poisson relation"
);
}
#[test]
fn test_saw_strain_drive_frequency_matches_source() {
let saw = SawSource::linbo3_3ghz();
let freq = saw.frequency_hz;
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
assert_eq!(drive.drive_frequency_hz(), freq);
}
#[test]
fn test_piezo_ac_strain_drive_zero_at_t_zero() {
let drive = PiezoAcStrainDrive::new(PiezoelectricSubstrate::pzt(), 1.0e-3, 100.0, 1.0e6)
.expect("valid piezo AC drive");
let (eps_xx, _, _) = drive.strain_tensor_at(0.0).diagonal();
assert!(
eps_xx.abs() < 1.0e-25,
"sin(0) drive should give zero strain at t=0, got {eps_xx}"
);
}
#[test]
fn test_piezo_ac_strain_drive_matches_static_formula_at_quarter_period() {
let substrate = PiezoelectricSubstrate::pzt();
let thickness = 1.0e-3;
let voltage_amplitude = 100.0;
let freq = 1.0e6;
let drive = PiezoAcStrainDrive::new(substrate, thickness, voltage_amplitude, freq)
.expect("valid piezo AC drive");
let quarter_period = 1.0 / (4.0 * freq);
let (eps_xx, _, eps_zz) = drive.strain_tensor_at(quarter_period).diagonal();
let expected = substrate
.strain_from_voltage(voltage_amplitude, thickness)
.expect("valid static strain");
assert!(
(eps_xx - expected.components[0][0]).abs() < 1.0e-12,
"AC piezo strain at peak should match static strain_from_voltage"
);
assert!((eps_zz - expected.components[2][2]).abs() < 1.0e-12);
}
#[test]
fn test_piezo_ac_strain_drive_rejects_excessive_field() {
let substrate = PiezoelectricSubstrate::pzt();
let result = PiezoAcStrainDrive::new(substrate, 1.0e-9, 1.0, 1.0e6);
assert!(result.is_err(), "excessive peak field should be rejected");
}
#[test]
fn test_piezo_ac_strain_drive_rejects_non_positive_frequency() {
let result = PiezoAcStrainDrive::new(PiezoelectricSubstrate::pzt(), 1.0e-3, 10.0, 0.0);
assert!(result.is_err(), "zero frequency should be rejected");
}
#[test]
fn test_driver_rejects_non_positive_alpha() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let result = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::nickel(),
GAMMA,
0.0, 5.0e3,
0.0,
20.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
);
assert!(result.is_err(), "alpha = 0 should be rejected");
}
#[test]
fn test_driver_rejects_negative_bias_field() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let result = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::nickel(),
GAMMA,
0.02,
5.0e3,
-1.0,
20.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
);
assert!(result.is_err(), "negative bias field should be rejected");
}
#[test]
fn test_driver_rejects_non_positive_film_thickness() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let result = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::nickel(),
GAMMA,
0.02,
5.0e3,
0.0,
0.0,
OPTIMAL_COUPLING_ANGLE_RAD,
);
assert!(result.is_err(), "zero film thickness should be rejected");
}
#[test]
fn test_evolve_rejects_non_positive_dt() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let driver = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::nickel(),
GAMMA,
0.045,
5.0e3,
0.0,
20.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid driver");
let result = driver.evolve(Vector3::unit_z(), 0.0, 10);
assert!(result.is_err(), "dt=0 should be rejected");
}
#[test]
fn test_evolve_rejects_zero_periods() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let driver = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::nickel(),
GAMMA,
0.045,
5.0e3,
0.0,
20.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid driver");
let result = driver.evolve(Vector3::unit_z(), 1.0e-12, 0);
assert!(result.is_err(), "n_periods=0 should be rejected");
}
#[test]
fn test_norm_conservation_of_raw_integration() {
let saw = SawSource::new(PiezoSubstrate::linbo3(), 1.0e9, 1.0e-6).expect("valid SAW");
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let driver = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::nickel(),
GAMMA,
0.045,
5.0e3,
1.0e4,
20.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid driver");
let trajectory = driver
.evolve(Vector3::unit_z(), 2.0e-12, 10)
.expect("evolve should succeed");
assert!(
trajectory.max_raw_norm_deviation < 1.0e-3,
"raw (pre-renormalization) integrator output should stay close to unit norm, got deviation {:.3e}",
trajectory.max_raw_norm_deviation
);
for sample in &trajectory.samples {
assert!(
(sample.magnetization.magnitude() - 1.0).abs() < 1.0e-10,
"renormalized sample should have unit magnitude"
);
}
}
fn build_resonance_pair() -> (
StrainDrivenLlgDriver<SawStrainDrive>,
StrainDrivenLlgDriver<SawStrainDrive>,
SawMagnetoacoustics,
SawMagnetoacoustics,
) {
let saw = SawSource::new(PiezoSubstrate::linbo3(), 1.0e9, 1.0e-6).expect("valid SAW");
let helper = nickel_saw_device(saw.clone(), 0.0);
let h_res = helper.acoustic_fmr_condition_h_ext();
let mat = SawMaterial::nickel();
let h_off = h_res + 5.0 * mat.anisotropy_field() + 2.0e4;
let device_on = nickel_saw_device(saw.clone(), h_res);
let device_off = nickel_saw_device(saw, h_off);
let driver_on = StrainDrivenLlgDriver::from_saw_magnetoacoustics(
&device_on,
MagnetoelasticMaterial::nickel(),
0.0,
0.0,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid on-resonance driver");
let driver_off = StrainDrivenLlgDriver::from_saw_magnetoacoustics(
&device_off,
MagnetoelasticMaterial::nickel(),
0.0,
0.0,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid off-resonance driver");
(driver_on, driver_off, device_on, device_off)
}
#[test]
fn test_resonant_response_exceeds_off_resonant() {
let (driver_on, driver_off, _device_on, _device_off) = build_resonance_pair();
let dt_s = 2.0e-12;
let n_periods = 30;
let m0 = Vector3::unit_z();
let trajectory_on = driver_on
.evolve(m0, dt_s, n_periods)
.expect("on-resonance evolution should succeed");
let trajectory_off = driver_off
.evolve(m0, dt_s, n_periods)
.expect("off-resonance evolution should succeed");
assert!(
trajectory_on.max_cone_angle_rad > 2.0 * trajectory_off.max_cone_angle_rad,
"on-resonance cone angle ({:.3e} rad) should substantially exceed off-resonance ({:.3e} rad)",
trajectory_on.max_cone_angle_rad,
trajectory_off.max_cone_angle_rad
);
assert!(
trajectory_on.max_cone_angle_rad > 0.0,
"on-resonance drive should produce nonzero precession"
);
}
#[test]
fn test_collinear_coupling_angle_gives_negligible_response() {
let saw = SawSource::new(PiezoSubstrate::linbo3(), 1.0e9, 1.0e-6).expect("valid SAW");
let helper = nickel_saw_device(saw.clone(), 0.0);
let h_res = helper.acoustic_fmr_condition_h_ext();
let device_on = nickel_saw_device(saw, h_res);
let driver_tilted = StrainDrivenLlgDriver::from_saw_magnetoacoustics(
&device_on,
MagnetoelasticMaterial::nickel(),
0.0,
0.0,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid tilted driver");
let driver_collinear = StrainDrivenLlgDriver::from_saw_magnetoacoustics(
&device_on,
MagnetoelasticMaterial::nickel(),
0.0,
0.0,
0.0, )
.expect("valid collinear driver");
let dt_s = 2.0e-12;
let n_periods = 30;
let m0 = Vector3::unit_z();
let trajectory_tilted = driver_tilted
.evolve(m0, dt_s, n_periods)
.expect("tilted evolution should succeed");
let trajectory_collinear = driver_collinear
.evolve(m0, dt_s, n_periods)
.expect("collinear evolution should succeed");
assert!(
trajectory_tilted.max_cone_angle_rad > 10.0 * trajectory_collinear.max_cone_angle_rad,
"45-degree coupling ({:.3e} rad) should give a far larger response than collinear \
coupling ({:.3e} rad)",
trajectory_tilted.max_cone_angle_rad,
trajectory_collinear.max_cone_angle_rad
);
}
#[test]
fn test_energy_pumped_in_positive_and_trend_consistent_with_resonant_absorption() {
let (driver_on, driver_off, device_on, device_off) = build_resonance_pair();
let dt_s = 2.0e-12;
let n_periods = 30;
let m0 = Vector3::unit_z();
let trajectory_on = driver_on
.evolve(m0, dt_s, n_periods)
.expect("on-resonance evolution should succeed");
let trajectory_off = driver_off
.evolve(m0, dt_s, n_periods)
.expect("off-resonance evolution should succeed");
assert!(
trajectory_on.mean_power_areal_density_w_per_m2 > 0.0,
"on-resonance mean absorbed power should be positive, got {:.3e} W/m^2",
trajectory_on.mean_power_areal_density_w_per_m2
);
assert!(
trajectory_off.mean_power_areal_density_w_per_m2 >= 0.0,
"off-resonance mean absorbed power should be non-negative, got {:.3e} W/m^2",
trajectory_off.mean_power_areal_density_w_per_m2
);
assert!(
trajectory_on.mean_power_areal_density_w_per_m2
> 2.0 * trajectory_off.mean_power_areal_density_w_per_m2,
"on-resonance mean power ({:.3e} W/m^2) should substantially exceed off-resonance ({:.3e} W/m^2)",
trajectory_on.mean_power_areal_density_w_per_m2,
trajectory_off.mean_power_areal_density_w_per_m2
);
let p_abs_on = device_on.resonant_absorption();
let p_abs_off = device_off.resonant_absorption();
assert!(
p_abs_on > 0.0,
"analytical resonant absorption should be positive"
);
assert!(
p_abs_on > p_abs_off,
"analytical model should also predict on-resonance absorption exceeding off-resonance"
);
}
#[test]
fn test_induced_anisotropy_constant_is_finite_and_signed_consistently() {
let saw = SawSource::linbo3_1ghz();
let drive = SawStrainDrive::new(saw, 0.0, 0.0);
let driver = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::cofeb(),
GAMMA,
0.01,
2.0e2,
0.0,
20.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid driver");
let k_sigma = driver.induced_anisotropy_constant_j_per_m3(1.0e-10);
assert!(
k_sigma.is_finite(),
"induced anisotropy constant must be finite"
);
let strain = driver.strain_drive.strain_tensor_at(1.0e-10);
let (eps_xx, _, _) = strain.diagonal();
let sigma_xx = MagnetoelasticMaterial::cofeb().youngs_modulus * eps_xx;
let expected =
stress_induced_anisotropy(MagnetoelasticMaterial::cofeb().lambda_s, sigma_xx);
assert!((k_sigma - expected).abs() < 1.0e-6 * expected.abs().max(1.0));
}
#[test]
fn test_piezo_driven_dynamics_evolves_and_conserves_norm() {
let drive = PiezoAcStrainDrive::new(PiezoelectricSubstrate::pmn_pt(), 0.5e-3, 5.0, 5.0e8)
.expect("valid piezo AC drive");
let driver = StrainDrivenLlgDriver::new(
drive,
MagnetoelasticMaterial::galfenol(),
GAMMA,
0.02,
1.0e3,
5.0e3,
30.0e-9,
OPTIMAL_COUPLING_ANGLE_RAD,
)
.expect("valid piezo-driven driver");
let m0 = Vector3::unit_z();
let trajectory = driver
.evolve(m0, 5.0e-13, 20)
.expect("piezo-driven evolution should succeed");
assert!(
trajectory.max_raw_norm_deviation < 1.0e-3,
"piezo-driven raw integration should conserve norm to tight tolerance, got {:.3e}",
trajectory.max_raw_norm_deviation
);
assert!(
trajectory.max_cone_angle_rad > 0.0,
"AC piezo strain should produce genuine (nonzero) magnetization dynamics"
);
}
}