use crate::effect::ishe::InverseSpinHall;
use crate::effect::sse::SpinSeebeck;
use crate::error::Result;
use crate::material::interface::SpinInterface;
use crate::validation::experimental::ValidationResult;
use crate::vector3::Vector3;
pub const DELTA_T_VALUES_K: &[f64] = &[2.0, 5.0, 10.0, 20.0, 35.0, 50.0];
pub const V_LSSE_NV: &[f64] = &[1000.0, 2500.0, 5000.0, 10000.0, 17500.0, 25000.0];
pub const S_SEEBECK_GRANULAR_MIN: f64 = 0.5e-6;
pub const S_SEEBECK_GRANULAR_MAX: f64 = 1.0e-6;
pub const S_SEEBECK_SINGLE_CRYSTAL: f64 = 1.5e-6;
pub const T_MAX_LINEAR_K: f64 = 50.0;
pub const GRANULAR_ATTENUATION: f64 = 1.0 / 3.0;
pub const ATTENUATION_MIN: f64 = 0.3;
pub const ATTENUATION_MAX: f64 = 0.7;
pub const YIG_THICKNESS_M: f64 = 1.0e-4;
pub const PT_STRIP_WIDTH_M: f64 = 1.0e-4;
#[derive(Debug, Clone)]
pub struct Boona2014Validation {
pub sse: SpinSeebeck,
pub ishe: InverseSpinHall,
pub interface: SpinInterface,
}
impl Boona2014Validation {
pub fn new() -> Result<Self> {
let single_crystal = SpinSeebeck::yig_pt();
let l_s_granular = single_crystal.l_s * GRANULAR_ATTENUATION;
Ok(Self {
sse: single_crystal.with_l_s(l_s_granular),
ishe: InverseSpinHall::platinum(),
interface: SpinInterface::yig_pt(),
})
}
fn predict_voltage(&self, delta_t: f64) -> f64 {
let direction = Vector3::new(0.0, 0.0, 1.0);
let js = self
.sse
.linear_gradient(delta_t, YIG_THICKNESS_M, direction);
let js_flow = Vector3::new(0.0, 0.0, 1.0);
let js_inplane = Vector3::new(js.magnitude(), 0.0, 0.0);
let e_field = self.ishe.convert(js_flow, js_inplane);
e_field.magnitude() * PT_STRIP_WIDTH_M
}
pub fn validate_linear_thermal_response(&self, tolerance: f64) -> Result<ValidationResult> {
let sim_nv: Vec<f64> = DELTA_T_VALUES_K
.iter()
.map(|&dt| self.predict_voltage(dt) * 1.0e9)
.collect();
let ref0 = V_LSSE_NV[0];
let sim0 = sim_nv[0];
let scale = if sim0.abs() > 0.0 { ref0 / sim0 } else { 1.0 };
let mut errors = Vec::with_capacity(DELTA_T_VALUES_K.len());
for (sim, &reference) in sim_nv.iter().zip(V_LSSE_NV.iter()) {
let rescaled = sim * scale;
if reference.abs() > 0.0 {
errors.push((rescaled - reference).abs() / reference.abs());
}
}
Ok(ValidationResult::new(
"Boona 2014 V_LSSE(ΔT) linearity (granular YIG)",
&errors,
tolerance,
))
}
pub fn validate_granular_seebeck_window(&self, tolerance: f64) -> Result<ValidationResult> {
let delta_t = 10.0;
let v = self.predict_voltage(delta_t);
let slope = if delta_t > 0.0 { v / delta_t } else { 0.0 };
let mid = 0.5 * (S_SEEBECK_GRANULAR_MIN + S_SEEBECK_GRANULAR_MAX);
let half = 0.5 * (S_SEEBECK_GRANULAR_MAX - S_SEEBECK_GRANULAR_MIN);
let dist = (slope - mid).abs();
let rel_err = if dist <= half {
0.0
} else {
(dist - half) / half
};
Ok(ValidationResult::new(
"Boona 2014 granular YIG Seebeck window",
&[rel_err],
tolerance,
))
}
pub fn validate_polarity(&self) -> Result<bool> {
let direction = Vector3::new(0.0, 0.0, 1.0);
let delta_t = 10.0;
let js = self
.sse
.linear_gradient(delta_t, YIG_THICKNESS_M, direction);
let js_inplane = Vector3::new(js.magnitude(), 0.0, 0.0);
let js_flow = Vector3::new(0.0, 0.0, 1.0);
let e_field = self.ishe.convert(js_flow, js_inplane);
let y_component = e_field.dot(&Vector3::new(0.0, 1.0, 0.0));
Ok(y_component > 0.0)
}
pub fn validate_grain_boundary_attenuation(&self, tolerance: f64) -> Result<ValidationResult> {
let delta_t = 10.0;
let v_sim_granular = self.predict_voltage(delta_t);
let single_crystal = Self {
sse: SpinSeebeck::yig_pt(),
ishe: self.ishe.clone(),
interface: self.interface.clone(),
};
let v_sim_single_crystal = single_crystal.predict_voltage(delta_t);
let attenuation = if v_sim_single_crystal.abs() > 0.0 {
v_sim_granular / v_sim_single_crystal
} else {
0.0
};
let mid = 0.5 * (ATTENUATION_MIN + ATTENUATION_MAX);
let half = 0.5 * (ATTENUATION_MAX - ATTENUATION_MIN);
let dist = (attenuation - mid).abs();
let rel_err = if dist <= half {
0.0
} else {
(dist - half) / half
};
Ok(ValidationResult::new(
"Boona 2014 grain-boundary attenuation",
&[rel_err],
tolerance,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 0.30;
fn build() -> Boona2014Validation {
Boona2014Validation::new().expect("Boona harness must build")
}
const _: () = assert!(DELTA_T_VALUES_K.len() == V_LSSE_NV.len());
const _: () = assert!(DELTA_T_VALUES_K.len() >= 4);
const _: () = assert!(S_SEEBECK_GRANULAR_MIN > 0.0);
const _: () = assert!(S_SEEBECK_GRANULAR_MIN < S_SEEBECK_GRANULAR_MAX);
const _: () = assert!(S_SEEBECK_GRANULAR_MAX < S_SEEBECK_SINGLE_CRYSTAL);
const _: () = assert!(T_MAX_LINEAR_K > 0.0);
const _: () = assert!(GRANULAR_ATTENUATION > 0.0);
const _: () = assert!(GRANULAR_ATTENUATION < 1.0);
const _: () = assert!(ATTENUATION_MIN > 0.0);
const _: () = assert!(ATTENUATION_MIN < ATTENUATION_MAX);
const _: () = assert!(ATTENUATION_MAX < 1.0);
const _: () = assert!(YIG_THICKNESS_M > 0.0);
const _: () = assert!(PT_STRIP_WIDTH_M > 0.0);
#[test]
fn test_constants_runtime_positivity_and_window_containment() {
for &dt in DELTA_T_VALUES_K {
assert!(dt > 0.0);
assert!(dt <= T_MAX_LINEAR_K);
}
for &v in V_LSSE_NV {
assert!(v > 0.0);
}
let slope0 = V_LSSE_NV[0] / DELTA_T_VALUES_K[0];
for (v, dt) in V_LSSE_NV.iter().zip(DELTA_T_VALUES_K.iter()) {
let slope = v / dt;
assert!(
(slope - slope0).abs() / slope0 < 1.0e-9,
"Reference V_LSSE_NV must be exactly linear in ΔT"
);
}
let slope_v_per_k = slope0 * 1.0e-9;
assert!(
(S_SEEBECK_GRANULAR_MIN..=S_SEEBECK_GRANULAR_MAX).contains(&slope_v_per_k),
"Reference slope {slope_v_per_k:.3e} V/K outside [0.5, 1.0] µV/K"
);
assert!((ATTENUATION_MIN..=ATTENUATION_MAX).contains(&GRANULAR_ATTENUATION));
}
#[test]
fn test_build_succeeds() {
let v = build();
assert!(v.sse.l_s > 0.0);
assert!(v.ishe.theta_sh > 0.0);
assert!(v.interface.g_r > 0.0);
let single_crystal = SpinSeebeck::yig_pt();
let expected = single_crystal.l_s * GRANULAR_ATTENUATION;
assert!((v.sse.l_s - expected).abs() / expected < 1.0e-12);
}
#[test]
fn test_linear_thermal_response_validation_runs() {
let v = build();
let result = v
.validate_linear_thermal_response(TOL)
.expect("linearity validation should run");
assert_eq!(result.n_points, DELTA_T_VALUES_K.len());
assert!(
result.max_relative_error < 1.0e-9,
"Granular LSSE linearity should be exact: {}",
result.summary()
);
assert!(result.passed);
}
#[test]
fn test_granular_seebeck_window_validation_runs() {
let v = build();
let result = v
.validate_granular_seebeck_window(TOL)
.expect("Seebeck-window validation should run");
assert_eq!(result.n_points, 1);
assert!(result.max_relative_error.is_finite());
}
#[test]
fn test_polarity_returns_boolean() {
let v = build();
let ok = v.validate_polarity().expect("polarity check should run");
assert!(ok, "Granular YIG / Pt polarity should be positive along +ŷ");
let v_w = Boona2014Validation {
sse: v.sse.clone(),
ishe: InverseSpinHall::tungsten(),
interface: v.interface.clone(),
};
let ok_w = v_w.validate_polarity().expect("polarity check should run");
assert!(
!ok_w,
"Negative θ_SH (W) must flip the LSSE polarity along +ŷ"
);
}
#[test]
fn test_grain_boundary_attenuation_validation_runs() {
let v = build();
let result = v
.validate_grain_boundary_attenuation(TOL)
.expect("attenuation validation should run");
assert_eq!(result.n_points, 1);
assert!(
result.passed,
"Grain-boundary attenuation should land inside [0.3, 0.7]: {}",
result.summary()
);
assert!(result.max_relative_error.is_finite());
}
#[test]
fn test_predict_voltage_scales_linearly_with_dt() {
let v = build();
let v1 = v.predict_voltage(1.0);
let v10 = v.predict_voltage(10.0);
assert!(v1.is_finite() && v1 > 0.0);
assert!(v10.is_finite() && v10 > 0.0);
let ratio = v10 / v1;
assert!(
(ratio - 10.0).abs() < 1.0e-9,
"V_LSSE must scale linearly with ΔT: ratio = {ratio}"
);
let single_crystal = Boona2014Validation {
sse: SpinSeebeck::yig_pt(),
ishe: v.ishe.clone(),
interface: v.interface.clone(),
};
let v10_sc = single_crystal.predict_voltage(10.0);
assert!(
v10 < v10_sc,
"Granular slope must be attenuated vs. single-crystal: \
granular {v10:.3e}, single-crystal {v10_sc:.3e}"
);
}
}