use std::f64::consts::PI;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[allow(unused_imports)] use crate::constants::{GAMMA, HBAR, KB, MU_0};
use crate::error::{invalid_param, Result};
use crate::vector3::Vector3;
const C_LIGHT: f64 = 2.997_924_58e8;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LaserPulseParams {
pub wavelength_m: f64,
pub peak_intensity: f64,
pub pulse_duration_s: f64,
pub fluence_j_cm2: f64,
}
impl LaserPulseParams {
pub fn new(wavelength_m: f64, peak_intensity: f64, pulse_duration_s: f64) -> Result<Self> {
if !(100e-9..=10e-6).contains(&wavelength_m) {
return Err(invalid_param(
"wavelength_m",
"must be in the range (100 nm, 10 µm)",
));
}
if peak_intensity <= 0.0 {
return Err(invalid_param("peak_intensity", "must be positive [W/m²]"));
}
if pulse_duration_s <= 0.0 {
return Err(invalid_param("pulse_duration_s", "must be positive [s]"));
}
let gaussian_area_factor = (PI / (4.0 * 2_f64.ln())).sqrt();
let fluence_j_m2 = peak_intensity * pulse_duration_s * gaussian_area_factor;
let fluence_j_cm2 = fluence_j_m2 / 1.0e4;
Ok(Self {
wavelength_m,
peak_intensity,
pulse_duration_s,
fluence_j_cm2,
})
}
pub fn ti_sapphire_standard() -> Self {
let wavelength_m = 800e-9;
let peak_intensity = 1.0e16;
let pulse_duration_s = 100e-15;
let gaussian_area_factor = (PI / (4.0 * 2_f64.ln())).sqrt();
let fluence_j_cm2 = peak_intensity * pulse_duration_s * gaussian_area_factor / 1.0e4;
Self {
wavelength_m,
peak_intensity,
pulse_duration_s,
fluence_j_cm2,
}
}
#[inline]
pub fn photon_energy_j(&self) -> f64 {
let omega = 2.0 * PI * C_LIGHT / self.wavelength_m;
HBAR * omega
}
#[inline]
pub fn photon_flux(&self) -> f64 {
self.peak_intensity / self.photon_energy_j()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OpticalMagneticMaterial {
pub name: &'static str,
pub ms: f64,
pub anisotropy_k: f64,
pub alpha_ife: f64,
pub coercive_field: f64,
pub demag_time_s: f64,
pub remag_time_s: f64,
}
impl OpticalMagneticMaterial {
pub fn gdfeco() -> Self {
Self {
name: "GdFeCo",
ms: 400.0e3, anisotropy_k: 300.0e3, alpha_ife: 1.0e-4, coercive_field: 50.0e3, demag_time_s: 300.0e-15, remag_time_s: 3.0e-12, }
}
pub fn cobalt() -> Self {
Self {
name: "Co",
ms: 1.4e6, anisotropy_k: 410.0e3, alpha_ife: 5.0e-5,
coercive_field: 100.0e3, demag_time_s: 150.0e-15, remag_time_s: 1.0e-12, }
}
pub fn permalloy_thin() -> Self {
Self {
name: "Permalloy (Ni80Fe20)",
ms: 800.0e3, anisotropy_k: 50.0e3, alpha_ife: 2.0e-5,
coercive_field: 10.0e3, demag_time_s: 200.0e-15, remag_time_s: 5.0e-12, }
}
#[inline]
pub fn coercive_field_tesla(&self) -> f64 {
MU_0 * self.coercive_field
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CircularHelicity {
RightCircular,
LeftCircular,
Linear,
}
impl CircularHelicity {
#[inline]
pub fn sigma_factor(self) -> f64 {
match self {
CircularHelicity::RightCircular => 1.0,
CircularHelicity::LeftCircular => -1.0,
CircularHelicity::Linear => 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct OpticalSwitchResult {
pub h_ife: f64,
pub demagnetization: f64,
pub switching_probability: f64,
pub switched: bool,
pub final_magnetization: Vector3<f64>,
}
#[derive(Debug, Clone)]
pub struct OpticalSwitching {
pub material: OpticalMagneticMaterial,
}
impl OpticalSwitching {
pub fn new(material: OpticalMagneticMaterial) -> Self {
Self { material }
}
pub fn inverse_faraday_field(
&self,
pulse: &LaserPulseParams,
helicity: CircularHelicity,
) -> f64 {
let n_ph = pulse.photon_flux();
let sigma = helicity.sigma_factor();
self.material.alpha_ife * n_ph * sigma / MU_0
}
pub fn demagnetization_fraction(&self, pulse: &LaserPulseParams) -> f64 {
const F_TH: f64 = 1.0e-3; const F_SAT: f64 = 10.0e-3;
let f = pulse.fluence_j_cm2;
if f < F_TH {
0.0
} else if f > F_SAT {
1.0
} else {
(f - F_TH) / (F_SAT - F_TH)
}
}
pub fn switching_probability(
&self,
pulse: &LaserPulseParams,
helicity: CircularHelicity,
initial_mz: f64,
) -> f64 {
let h_ife_signed = self.inverse_faraday_field(pulse, helicity);
let h_c = self.material.coercive_field;
let drive = h_ife_signed - initial_mz.signum() * h_c;
let h_width = h_c * 0.2; sigmoid(drive / h_width)
}
pub fn simulate_pulse_response(
&self,
pulse: &LaserPulseParams,
helicity: CircularHelicity,
m_initial: Vector3<f64>,
) -> OpticalSwitchResult {
let h_ife = self.inverse_faraday_field(pulse, helicity);
let demagnetization = self.demagnetization_fraction(pulse);
let p_switch = self.switching_probability(pulse, helicity, m_initial.z);
let switched = p_switch > 0.5;
let final_magnetization = if switched {
Vector3::new(0.0, 0.0, -m_initial.z.signum())
} else {
let recovered_z = m_initial.z * (1.0 - demagnetization);
Vector3::new(m_initial.x, m_initial.y, recovered_z)
};
OpticalSwitchResult {
h_ife,
demagnetization,
switching_probability: p_switch,
switched,
final_magnetization,
}
}
pub fn is_hds_material(&self) -> bool {
let ref_n_ph = 1.0e25_f64;
let ref_h_ife = self.material.alpha_ife * ref_n_ph / MU_0;
ref_h_ife > self.material.coercive_field
}
pub fn critical_fluence(&self, helicity: CircularHelicity) -> Option<f64> {
if helicity == CircularHelicity::Linear {
return None;
}
let f_crit = self.material.coercive_field * MU_0 * 1.0e-2;
Some(f_crit)
}
}
#[inline]
fn sigmoid(x: f64) -> f64 {
if x >= 0.0 {
let e = (-x).exp();
1.0 / (1.0 + e)
} else {
let e = x.exp();
e / (1.0 + e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ife_field_rcp_positive() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::ti_sapphire_standard();
let h_ife = switcher.inverse_faraday_field(&pulse, CircularHelicity::RightCircular);
assert!(
h_ife > 0.0,
"H_IFE must be positive for RCP; got {h_ife:.3e} A/m"
);
}
#[test]
fn test_ife_field_lcp_negative() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::ti_sapphire_standard();
let h_ife = switcher.inverse_faraday_field(&pulse, CircularHelicity::LeftCircular);
assert!(
h_ife < 0.0,
"H_IFE must be negative for LCP; got {h_ife:.3e} A/m"
);
}
#[test]
fn test_ife_field_linear_zero() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::ti_sapphire_standard();
let h_ife = switcher.inverse_faraday_field(&pulse, CircularHelicity::Linear);
assert_eq!(h_ife, 0.0, "H_IFE must be zero for linear polarisation");
}
#[test]
fn test_ife_field_scales_with_intensity() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse1 = LaserPulseParams::new(800e-9, 1.0e15, 100e-15).expect("valid params");
let pulse2 = LaserPulseParams::new(800e-9, 2.0e15, 100e-15).expect("valid params");
let h1 = switcher.inverse_faraday_field(&pulse1, CircularHelicity::RightCircular);
let h2 = switcher.inverse_faraday_field(&pulse2, CircularHelicity::RightCircular);
let ratio = h2 / h1;
assert!(
(ratio - 2.0).abs() < 1.0e-9,
"H_IFE must double when intensity doubles; ratio = {ratio}"
);
}
#[test]
fn test_demag_below_threshold() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::new(800e-9, 1.0e10, 100e-15).expect("valid params");
assert!(
pulse.fluence_j_cm2 < 1.0e-3,
"Sanity: fluence should be below 1 mJ/cm², got {:.3e} J/cm²",
pulse.fluence_j_cm2
);
let f_q = switcher.demagnetization_fraction(&pulse);
assert_eq!(f_q, 0.0, "f_quench must be 0 below threshold; got {f_q}");
}
#[test]
fn test_demag_above_saturation() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::new(800e-9, 1.0e18, 100e-15).expect("valid params");
assert!(
pulse.fluence_j_cm2 > 10.0e-3,
"Sanity: fluence should exceed 10 mJ/cm², got {:.3e} J/cm²",
pulse.fluence_j_cm2
);
let f_q = switcher.demagnetization_fraction(&pulse);
assert_eq!(f_q, 1.0, "f_quench must be 1 above saturation; got {f_q}");
}
#[test]
fn test_switching_prob_rcp_vs_lcp() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::ti_sapphire_standard();
let initial_mz = -1.0_f64;
let p_rcp =
switcher.switching_probability(&pulse, CircularHelicity::RightCircular, initial_mz);
let p_lcp =
switcher.switching_probability(&pulse, CircularHelicity::LeftCircular, initial_mz);
assert!(
p_rcp > p_lcp,
"P_switch(RCP, m=−z) should exceed P_switch(LCP, m=−z): {p_rcp:.4} vs {p_lcp:.4}"
);
}
#[test]
fn test_photon_energy_800nm() {
let pulse = LaserPulseParams::ti_sapphire_standard();
let e_photon = pulse.photon_energy_j();
let expected_j = 1.550 * 1.602_176_634e-19;
let relative_error = ((e_photon - expected_j) / expected_j).abs();
assert!(
relative_error < 0.01,
"Photon energy at 800 nm should be ~{expected_j:.3e} J (1.55 eV); \
got {e_photon:.3e} J (relative error {relative_error:.4})"
);
}
#[test]
fn test_gdfeco_preset_fields_valid() {
let m = OpticalMagneticMaterial::gdfeco();
assert!(m.alpha_ife > 0.0, "alpha_ife must be positive");
assert!(m.coercive_field > 0.0, "coercive_field must be positive");
assert!(m.demag_time_s > 0.0, "demag_time_s must be positive");
assert!(m.remag_time_s > 0.0, "remag_time_s must be positive");
assert!(m.ms > 0.0, "saturation magnetisation must be positive");
assert!(m.anisotropy_k > 0.0, "anisotropy constant must be positive");
}
#[test]
fn test_new_rejects_bad_wavelength() {
assert!(LaserPulseParams::new(50e-9, 1e15, 100e-15).is_err());
assert!(LaserPulseParams::new(20e-6, 1e15, 100e-15).is_err());
}
#[test]
fn test_new_rejects_non_positive_intensity() {
assert!(LaserPulseParams::new(800e-9, 0.0, 100e-15).is_err());
assert!(LaserPulseParams::new(800e-9, -1e15, 100e-15).is_err());
}
#[test]
fn test_new_rejects_non_positive_duration() {
assert!(LaserPulseParams::new(800e-9, 1e15, 0.0).is_err());
assert!(LaserPulseParams::new(800e-9, 1e15, -100e-15).is_err());
}
#[test]
fn test_helicity_sigma_factors() {
assert_eq!(CircularHelicity::RightCircular.sigma_factor(), 1.0);
assert_eq!(CircularHelicity::LeftCircular.sigma_factor(), -1.0);
assert_eq!(CircularHelicity::Linear.sigma_factor(), 0.0);
}
#[test]
fn test_ife_rcp_lcp_antisymmetric() {
let material = OpticalMagneticMaterial::cobalt();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::ti_sapphire_standard();
let h_rcp = switcher.inverse_faraday_field(&pulse, CircularHelicity::RightCircular);
let h_lcp = switcher.inverse_faraday_field(&pulse, CircularHelicity::LeftCircular);
assert!(
(h_rcp + h_lcp).abs() < 1.0e-6 * h_rcp.abs(),
"RCP and LCP IFE fields should be equal and opposite: {h_rcp:.3e} vs {h_lcp:.3e}"
);
}
#[test]
fn test_simulate_pulse_response_consistency() {
let material = OpticalMagneticMaterial::gdfeco();
let switcher = OpticalSwitching::new(material);
let pulse = LaserPulseParams::ti_sapphire_standard();
let m_initial = Vector3::new(0.0, 0.0, -1.0);
let result =
switcher.simulate_pulse_response(&pulse, CircularHelicity::RightCircular, m_initial);
assert_eq!(result.switched, result.switching_probability > 0.5);
assert!((0.0..=1.0).contains(&result.demagnetization));
assert!((0.0..=1.0).contains(&result.switching_probability));
}
#[test]
fn test_critical_fluence_linear_none() {
let switcher = OpticalSwitching::new(OpticalMagneticMaterial::gdfeco());
assert!(switcher
.critical_fluence(CircularHelicity::Linear)
.is_none());
}
#[test]
fn test_critical_fluence_circular_some_positive() {
let switcher = OpticalSwitching::new(OpticalMagneticMaterial::gdfeco());
let f_rcp = switcher.critical_fluence(CircularHelicity::RightCircular);
let f_lcp = switcher.critical_fluence(CircularHelicity::LeftCircular);
assert!(f_rcp.is_some() && f_rcp.unwrap() > 0.0);
assert!(f_lcp.is_some() && f_lcp.unwrap() > 0.0);
}
#[test]
fn test_sigmoid_known_values() {
assert!((sigmoid(0.0) - 0.5).abs() < 1.0e-15);
assert!(sigmoid(100.0) > 0.9999);
assert!(sigmoid(-100.0) < 1.0e-4);
}
#[test]
fn test_is_hds_material_gdfeco() {
let switcher = OpticalSwitching::new(OpticalMagneticMaterial::gdfeco());
assert!(switcher.is_hds_material());
}
#[test]
fn test_is_hds_material_permalloy_no_panic() {
let switcher = OpticalSwitching::new(OpticalMagneticMaterial::permalloy_thin());
let _ = switcher.is_hds_material(); }
#[test]
fn test_ti_sapphire_fluence_above_threshold() {
let pulse = LaserPulseParams::ti_sapphire_standard();
assert!(
pulse.fluence_j_cm2 > 1.0e-3,
"Ti:Sapphire standard pulse fluence should exceed 1 mJ/cm²; got {:.3e}",
pulse.fluence_j_cm2
);
}
#[test]
fn test_coercive_field_tesla_conversion() {
let m = OpticalMagneticMaterial::gdfeco();
let expected = m.coercive_field * MU_0;
let computed = m.coercive_field_tesla();
assert!(
(computed - expected).abs() < 1.0e-20,
"coercive_field_tesla() mismatch: {computed:.6e} vs {expected:.6e}"
);
}
}