use crate::effect::exchange_bias::ExchangeBias;
use crate::error::Result;
use crate::validation::experimental::ValidationResult;
pub const TOL: f64 = 0.30;
pub const IRMN_THICKNESS_NM: [f64; 6] = [2.0, 4.0, 6.0, 8.0, 10.0, 12.0];
pub const H_EB_MILLI_TESLA: [f64; 6] = [0.0, 8.0, 25.0, 32.0, 30.0, 28.0];
pub const TRAINING_CYCLES: [u32; 5] = [1, 2, 3, 5, 10];
pub const TRAINING_RATIO: [f64; 5] = [1.0, 0.92, 0.88, 0.84, 0.80];
pub const TEMP_RATIO: [f64; 5] = [0.0, 0.25, 0.50, 0.75, 1.0];
pub const H_EB_TEMP_RATIO: [f64; 5] = [1.0, 0.6495, 0.3536, 0.1250, 0.0];
#[derive(Debug, Clone)]
pub struct Nogues1999Validation {
pub eb: ExchangeBias,
}
impl Nogues1999Validation {
pub fn new() -> Result<Self> {
Ok(Self {
eb: ExchangeBias::irmn_co(5.0),
})
}
fn h_eb_at_thickness(h_eb_max: f64, t_afm_nm: f64) -> f64 {
let t_crit_nm = 3.5;
let t_sat_nm = 2.0;
if t_afm_nm <= t_crit_nm {
return 0.0;
}
h_eb_max * (1.0 - (-(t_afm_nm - t_crit_nm) / t_sat_nm).exp())
}
pub fn validate_thickness_scaling(&self, tol: f64) -> Result<ValidationResult> {
let h_eb_max_model = self.eb.loop_shift_field().abs();
let ref_peak = H_EB_MILLI_TESLA.iter().copied().fold(0.0_f64, f64::max);
let mut errors: Vec<f64> = Vec::with_capacity(IRMN_THICKNESS_NM.len());
for (&t_nm, &h_ref_mt) in IRMN_THICKNESS_NM.iter().zip(H_EB_MILLI_TESLA.iter()) {
let h_ref_norm = h_ref_mt / ref_peak;
let h_model_abs = Self::h_eb_at_thickness(h_eb_max_model, t_nm);
let h_model_norm = h_model_abs / h_eb_max_model;
let rel_err = if h_ref_norm < 1e-3 {
(h_model_norm - h_ref_norm).abs()
} else {
(h_model_norm - h_ref_norm).abs() / h_ref_norm
};
errors.push(rel_err);
}
Ok(ValidationResult::new(
"Nogués 1999 IrMn thickness scaling",
&errors,
tol,
))
}
pub fn validate_training_effect(&self, tol: f64) -> Result<ValidationResult> {
let h_eb_1 = self.eb.training_field(1);
let mut errors: Vec<f64> = Vec::with_capacity(TRAINING_CYCLES.len());
for (&n, &ratio_ref) in TRAINING_CYCLES.iter().zip(TRAINING_RATIO.iter()) {
let h_n = self.eb.training_field(n);
let ratio_model = if h_eb_1.abs() > 1e-30 {
h_n / h_eb_1
} else {
0.0
};
let rel_err = if ratio_ref.abs() > 1e-10 {
(ratio_model - ratio_ref).abs() / ratio_ref.abs()
} else {
ratio_model.abs()
};
errors.push(rel_err);
}
Ok(ValidationResult::new(
"Nogués 1999 training effect",
&errors,
tol,
))
}
pub fn validate_temperature_dependence(&self, tol: f64) -> Result<ValidationResult> {
let h_eb_0 = self.eb.loop_shift_field();
let mut errors: Vec<f64> = Vec::with_capacity(TEMP_RATIO.len());
for (&t_over_tb, &ratio_ref) in TEMP_RATIO.iter().zip(H_EB_TEMP_RATIO.iter()) {
let temperature = t_over_tb * self.eb.t_b;
let h_at_t = self.eb.loop_shift_at_temperature(temperature);
let ratio_model = if h_eb_0.abs() > 1e-30 {
h_at_t / h_eb_0
} else {
0.0
};
let rel_err = if ratio_ref.abs() > 1e-10 {
(ratio_model - ratio_ref).abs() / ratio_ref.abs()
} else {
ratio_model.abs()
};
errors.push(rel_err);
}
Ok(ValidationResult::new(
"Nogués 1999 temperature dependence",
&errors,
tol,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_succeeds() {
let v = Nogues1999Validation::new();
assert!(v.is_ok(), "Nogues1999Validation::new() should not fail");
}
#[test]
fn test_reference_arrays_have_consistent_lengths() {
assert_eq!(IRMN_THICKNESS_NM.len(), H_EB_MILLI_TESLA.len());
assert_eq!(TRAINING_CYCLES.len(), TRAINING_RATIO.len());
assert_eq!(TEMP_RATIO.len(), H_EB_TEMP_RATIO.len());
}
#[test]
fn test_reference_thickness_data_nonzero_peak() {
let peak = H_EB_MILLI_TESLA.iter().copied().fold(0.0_f64, f64::max);
assert!(peak > 0.0, "Reference H_EB peak must be positive");
}
#[test]
fn test_reference_training_starts_at_unity() {
assert!((TRAINING_RATIO[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_reference_training_monotone_decay() {
for w in TRAINING_RATIO.windows(2) {
assert!(
w[1] <= w[0],
"TRAINING_RATIO must be monotonically non-increasing"
);
}
}
#[test]
fn test_reference_temp_starts_at_unity_ends_at_zero() {
assert!((H_EB_TEMP_RATIO[0] - 1.0).abs() < 1e-10);
assert!(H_EB_TEMP_RATIO[H_EB_TEMP_RATIO.len() - 1].abs() < 1e-10);
}
#[test]
fn test_reference_temp_monotone_decay() {
for w in H_EB_TEMP_RATIO.windows(2) {
assert!(
w[1] <= w[0],
"H_EB_TEMP_RATIO must be monotonically non-increasing"
);
}
}
#[test]
fn test_thickness_scaling_passes_default_tolerance() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_thickness_scaling(TOL).unwrap();
assert!(
result.passed,
"Thickness scaling validation failed: {}",
result.summary()
);
}
#[test]
fn test_thickness_scaling_returns_correct_number_of_points() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_thickness_scaling(TOL).unwrap();
assert_eq!(result.n_points, IRMN_THICKNESS_NM.len());
}
#[test]
fn test_thickness_model_zero_at_zero_thickness() {
let h_max = 1.0e6_f64; let h_at_zero = Nogues1999Validation::h_eb_at_thickness(h_max, 0.0);
assert!(
h_at_zero.abs() < 1e-10,
"h_eb_at_thickness should be ~0 at t=0 nm; got {h_at_zero}"
);
}
#[test]
fn test_thickness_model_saturates() {
let h_max = 1.0e6_f64;
let h_large = Nogues1999Validation::h_eb_at_thickness(h_max, 100.0);
assert!(
(h_large / h_max - 1.0).abs() < 0.01,
"h_eb_at_thickness should saturate near H_max; got {h_large} vs {h_max}"
);
}
#[test]
fn test_thickness_model_is_monotone() {
let h_max = 1.0e6_f64;
let thicknesses: Vec<f64> = (0..20).map(|i| i as f64 * 1.0).collect();
let values: Vec<f64> = thicknesses
.iter()
.map(|&t| Nogues1999Validation::h_eb_at_thickness(h_max, t))
.collect();
for w in values.windows(2) {
assert!(
w[1] >= w[0] - 1e-12,
"h_eb_at_thickness should be monotone; values: {values:?}"
);
}
}
#[test]
fn test_training_effect_passes_default_tolerance() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_training_effect(TOL).unwrap();
assert!(
result.passed,
"Training effect validation failed: {}",
result.summary()
);
}
#[test]
fn test_training_effect_returns_correct_number_of_points() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_training_effect(TOL).unwrap();
assert_eq!(result.n_points, TRAINING_CYCLES.len());
}
#[test]
fn test_training_first_point_zero_error() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_training_effect(TOL).unwrap();
assert!(
result.max_relative_error < TOL,
"Training validation max error = {} exceeds tolerance {}",
result.max_relative_error,
TOL
);
}
#[test]
fn test_temperature_dependence_passes_default_tolerance() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_temperature_dependence(TOL).unwrap();
assert!(
result.passed,
"Temperature dependence validation failed: {}",
result.summary()
);
}
#[test]
fn test_temperature_dependence_returns_correct_number_of_points() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_temperature_dependence(TOL).unwrap();
assert_eq!(result.n_points, TEMP_RATIO.len());
}
#[test]
fn test_temperature_at_blocking_temp_zero_error() {
let v = Nogues1999Validation::new().unwrap();
let result = v.validate_temperature_dependence(TOL).unwrap();
assert!(result.n_points == TEMP_RATIO.len());
assert!(result.passed, "{}", result.summary());
}
#[test]
fn test_all_summaries_contain_pass() {
let v = Nogues1999Validation::new().unwrap();
for result in [
v.validate_thickness_scaling(TOL).unwrap(),
v.validate_training_effect(TOL).unwrap(),
v.validate_temperature_dependence(TOL).unwrap(),
] {
let s = result.summary();
assert!(s.contains("PASS"), "Expected PASS in summary: {s}");
}
}
}