use crate::error::Result;
use crate::validation::experimental::ValidationResult;
pub const MAGNON_DIFFUSION_LENGTH_M: f64 = 9.4e-6;
pub const DISTANCES_M: &[f64] = &[1.5e-6, 3.0e-6, 7.0e-6, 15e-6, 25e-6, 40e-6];
pub const R_NL_RELATIVE: &[f64] = &[1.0, 0.8525, 0.5571, 0.2378, 0.0821, 0.0166];
pub const TEMPERATURE_K: f64 = 295.0;
const _: () = assert!(R_NL_RELATIVE.len() == DISTANCES_M.len());
const _: () = assert!(DISTANCES_M.len() >= 6);
const _: () = assert!(MAGNON_DIFFUSION_LENGTH_M > 0.0);
const _: () = assert!(TEMPERATURE_K > 0.0);
const _: () = assert!(R_NL_RELATIVE[0] == 1.0);
const _: () = assert!(R_NL_RELATIVE[5] > 0.0);
const _: () = assert!(R_NL_RELATIVE[5] < R_NL_RELATIVE[4]);
const _: () = assert!(R_NL_RELATIVE[4] < R_NL_RELATIVE[3]);
const _: () = assert!(R_NL_RELATIVE[3] < R_NL_RELATIVE[2]);
const _: () = assert!(R_NL_RELATIVE[2] < R_NL_RELATIVE[1]);
const _: () = assert!(R_NL_RELATIVE[1] < R_NL_RELATIVE[0]);
#[derive(Debug, Clone)]
pub struct Cornelissen2015Validation {
pub magnon_diffusion_length: f64,
pub temperature: f64,
}
impl Cornelissen2015Validation {
pub fn new() -> Result<Self> {
Ok(Self {
magnon_diffusion_length: MAGNON_DIFFUSION_LENGTH_M,
temperature: TEMPERATURE_K,
})
}
pub fn r_nl_relative(&self, d: f64) -> f64 {
let d_ref = DISTANCES_M[0];
(-(d - d_ref) / self.magnon_diffusion_length).exp()
}
pub fn validate_exponential_decay(&self, tolerance: f64) -> Result<ValidationResult> {
let mut errors = Vec::with_capacity(DISTANCES_M.len());
for (&d, &r_ref) in DISTANCES_M.iter().zip(R_NL_RELATIVE.iter()) {
let r_sim = self.r_nl_relative(d);
let denominator = r_ref.max(1.0e-10);
let rel_error = (r_sim - r_ref).abs() / denominator;
let capped = if r_ref < 1.0e-4 {
rel_error.min(1.0)
} else {
rel_error
};
errors.push(capped);
}
Ok(ValidationResult::new(
"Cornelissen 2015 R_NL exponential decay",
&errors,
tolerance,
))
}
pub fn validate_diffusion_length(&self, tolerance: f64) -> Result<ValidationResult> {
let rel_error = (self.magnon_diffusion_length - MAGNON_DIFFUSION_LENGTH_M).abs()
/ MAGNON_DIFFUSION_LENGTH_M;
let errors = vec![rel_error];
Ok(ValidationResult::new(
"Cornelissen 2015 magnon diffusion length",
&errors,
tolerance,
))
}
pub fn transport_length_scale(&self, temperature: f64) -> f64 {
MAGNON_DIFFUSION_LENGTH_M * (TEMPERATURE_K / temperature).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 0.30;
fn build() -> Cornelissen2015Validation {
Cornelissen2015Validation::new().expect("Cornelissen harness must build")
}
const _: () = assert!(R_NL_RELATIVE.len() == DISTANCES_M.len());
#[test]
fn test_exponential_decay_passes_30pct() {
let v = build();
let result = v
.validate_exponential_decay(TOL)
.expect("exponential decay validation must run");
assert_eq!(result.n_points, DISTANCES_M.len());
assert!(
result.passed,
"Exponential decay validation failed: {}",
result.summary()
);
}
#[test]
fn test_r_nl_monotonically_decreasing() {
let v = build();
let ratios: Vec<f64> = DISTANCES_M.iter().map(|&d| v.r_nl_relative(d)).collect();
for window in ratios.windows(2) {
assert!(
window[1] < window[0],
"R_NL must decrease monotonically: {:.4e} ≥ {:.4e}",
window[1],
window[0]
);
}
}
#[test]
fn test_diffusion_length_validates() {
let v = build();
let result = v
.validate_diffusion_length(0.01)
.expect("diffusion length validation must run");
assert_eq!(result.n_points, 1);
assert!(
result.passed,
"Diffusion length round-trip must be exact: {}",
result.summary()
);
assert!(
result.max_relative_error < 1.0e-12,
"Relative error must be at floating-point precision: {}",
result.max_relative_error
);
}
#[test]
fn test_temperature_scaling_physical() {
let v = build();
let lambda_low_t = v.transport_length_scale(150.0); let lambda_room_t = v.transport_length_scale(295.0); assert!(
lambda_low_t > lambda_room_t,
"λ_m must be longer at lower temperature: \
λ_m(150 K) = {:.2} μm, λ_m(295 K) = {:.2} μm",
lambda_low_t * 1.0e6,
lambda_room_t * 1.0e6
);
}
#[test]
fn test_long_distance_small_signal() {
let v = build();
let r_40um = v.r_nl_relative(40e-6);
assert!(
r_40um < 0.05,
"At 40 μm the nonlocal signal must be below 5% of the anchor: {:.3e}",
r_40um
);
let expected = *R_NL_RELATIVE.last().expect("R_NL_RELATIVE is non-empty");
assert!(
(r_40um - expected).abs() / expected < 0.01,
"Simulated R_NL at 40 μm ({:.4e}) must match tabulated value ({:.4e})",
r_40um,
expected
);
}
#[test]
fn test_build_fields_consistent() {
let v = build();
assert!(
(v.magnon_diffusion_length - MAGNON_DIFFUSION_LENGTH_M).abs() < 1.0e-15,
"magnon_diffusion_length must equal MAGNON_DIFFUSION_LENGTH_M exactly"
);
assert!(
(v.temperature - TEMPERATURE_K).abs() < 1.0e-9,
"temperature must equal TEMPERATURE_K exactly"
);
}
#[test]
fn test_anchor_point_unity() {
let v = build();
let r_anchor = v.r_nl_relative(DISTANCES_M[0]);
assert!(
(r_anchor - 1.0).abs() < 1.0e-12,
"Anchor point must produce R_NL = 1.0: got {:.6}",
r_anchor
);
}
}