#![allow(dead_code)]
#![allow(clippy::too_many_arguments)]
const K_B: f64 = 1.380_649e-23;
const N_A: f64 = 6.022_140_76e23;
const MU_B: f64 = 9.274_010_08e-24;
const MU_0: f64 = 1.256_637_062e-6;
const R_GAS: f64 = 8.314_462_618;
pub struct MagnetocaloricMaterial {
pub curie_temperature: f64,
pub specific_heat: f64,
pub delta_s_per_tesla: f64,
pub delta_t_per_tesla: f64,
pub density: f64,
}
impl MagnetocaloricMaterial {
pub fn new(
curie_temperature: f64,
specific_heat: f64,
delta_s_per_tesla: f64,
delta_t_per_tesla: f64,
density: f64,
) -> Self {
Self {
curie_temperature,
specific_heat,
delta_s_per_tesla,
delta_t_per_tesla,
density,
}
}
pub fn gadolinium() -> Self {
Self::new(294.0, 300.0, 1.5, 1.5, 7_900.0)
}
pub fn isothermal_entropy_change(&self, delta_field: f64) -> f64 {
self.delta_s_per_tesla * delta_field.abs()
}
pub fn adiabatic_temp_change(&self, delta_field: f64) -> f64 {
self.delta_t_per_tesla * delta_field.abs()
}
pub fn reduced_temperature(&self, temperature: f64) -> f64 {
temperature / self.curie_temperature
}
pub fn adiabatic_temp_from_entropy(&self, temperature: f64, delta_s: f64) -> f64 {
-(temperature / self.specific_heat) * delta_s
}
pub fn refrigerant_capacity(&self, delta_field: f64, fwhm_kelvin: f64) -> f64 {
self.isothermal_entropy_change(delta_field) * fwhm_kelvin
}
}
pub struct MagneticEntropy {
pub molar_mass: f64,
pub effective_moment: f64,
}
impl MagneticEntropy {
pub fn new(molar_mass: f64, effective_moment: f64) -> Self {
Self {
molar_mass,
effective_moment,
}
}
pub fn maxwell_relation_entropy_change(&self, dm_dt: f64, delta_h: f64) -> f64 {
dm_dt * delta_h
}
pub fn clausius_clapeyron_slope(&self, delta_m: f64, delta_s: f64) -> f64 {
if delta_s.abs() < 1e-30 {
return 0.0;
}
-MU_0 * delta_m / delta_s
}
pub fn disordered_entropy(&self, total_angular_momentum: f64) -> f64 {
let g = 2.0 * total_angular_momentum + 1.0;
R_GAS * g.ln() / self.molar_mass
}
pub fn numerical_maxwell_entropy(
&self,
m_low: &[(f64, f64)],
m_high: &[(f64, f64)],
delta_h: f64,
) -> Vec<(f64, f64)> {
let n = m_low.len().min(m_high.len());
let mut result = Vec::with_capacity(n);
for i in 0..n {
let (t_low, m_l) = m_low[i];
let (_t_high, m_h) = m_high[i];
let dm_dh = (m_h - m_l) / delta_h;
result.push((t_low, dm_dh * delta_h));
}
result
}
pub fn integrated_entropy(&self, ds_data: &[(f64, f64)]) -> f64 {
if ds_data.len() < 2 {
return 0.0;
}
let mut integral = 0.0;
for w in ds_data.windows(2) {
let (t1, s1) = w[0];
let (t2, s2) = w[1];
integral += 0.5 * (s1 + s2) * (t2 - t1);
}
integral
}
}
pub struct GiantMce {
pub transition_temp: f64,
pub field_sensitivity: f64,
pub peak_entropy_change: f64,
pub transition_width: f64,
pub hysteresis_loss: f64,
}
impl GiantMce {
pub fn new(
transition_temp: f64,
field_sensitivity: f64,
peak_entropy_change: f64,
transition_width: f64,
hysteresis_loss: f64,
) -> Self {
Self {
transition_temp,
field_sensitivity,
peak_entropy_change,
transition_width,
hysteresis_loss,
}
}
pub fn la_fe_si() -> Self {
Self::new(195.0, 2.5, 20.0, 8.0, 200.0)
}
pub fn mn_as() -> Self {
Self::new(318.0, -0.5, 30.0, 5.0, 800.0)
}
pub fn shifted_transition_temp(&self, field_tesla: f64) -> f64 {
self.transition_temp + self.field_sensitivity * field_tesla
}
pub fn entropy_change(&self, temperature: f64, field_tesla: f64) -> f64 {
let t0 = self.shifted_transition_temp(field_tesla);
let dt = temperature - t0;
let half_w = self.transition_width / 2.0;
-self.peak_entropy_change * (half_w * half_w) / (dt * dt + half_w * half_w)
}
pub fn net_cooling_capacity(&self, temperature: f64, field_tesla: f64) -> f64 {
let ds = self.entropy_change(temperature, field_tesla).abs();
let gross = ds * self.transition_width;
(gross - self.hysteresis_loss).max(0.0)
}
pub fn is_first_order(&self) -> bool {
self.peak_entropy_change > 10.0
}
}
pub struct MagneticRefrigeration {
pub t_hot: f64,
pub t_cold: f64,
pub field_swing: f64,
pub material: MagnetocaloricMaterial,
}
impl MagneticRefrigeration {
pub fn new(
t_hot: f64,
t_cold: f64,
field_swing: f64,
material: MagnetocaloricMaterial,
) -> Self {
Self {
t_hot,
t_cold,
field_swing,
material,
}
}
pub fn temp_span(&self) -> f64 {
self.t_hot - self.t_cold
}
pub fn carnot_cop(&self) -> f64 {
let span = self.temp_span();
if span < 1e-10 {
return f64::INFINITY;
}
self.t_cold / span
}
pub fn brayton_cooling_power(&self) -> f64 {
let ds = self.material.isothermal_entropy_change(self.field_swing);
ds * self.t_cold
}
pub fn ericsson_cooling_power(&self) -> f64 {
self.brayton_cooling_power()
}
pub fn amr_cop(&self) -> f64 {
let ds = self.material.isothermal_entropy_change(self.field_swing);
let q_cold = ds * self.t_cold;
let q_hot = ds * self.t_hot;
if (q_hot - q_cold).abs() < 1e-30 {
return 0.0;
}
q_cold / (q_hot - q_cold)
}
pub fn second_law_efficiency(&self) -> f64 {
let cop_actual = self.amr_cop();
let cop_carnot = self.carnot_cop();
if cop_carnot == f64::INFINITY || cop_carnot < 1e-30 {
return 0.0;
}
cop_actual / cop_carnot
}
pub fn volumetric_cooling_power(&self, frequency_hz: f64) -> f64 {
let ds = self.material.isothermal_entropy_change(self.field_swing);
ds * self.t_cold * self.material.density * frequency_hz
}
}
pub struct LandauTheoryMagneto {
pub a0: f64,
pub b: f64,
pub c: f64,
pub curie_temperature: f64,
}
impl LandauTheoryMagneto {
pub fn new(a0: f64, b: f64, c: f64, curie_temperature: f64) -> Self {
Self {
a0,
b,
c,
curie_temperature,
}
}
pub fn a_coeff(&self, temperature: f64) -> f64 {
self.a0 * (temperature - self.curie_temperature)
}
pub fn free_energy(&self, magnetization: f64, temperature: f64, field: f64) -> f64 {
let a = self.a_coeff(temperature);
let m2 = magnetization * magnetization;
let m4 = m2 * m2;
let m6 = m4 * m2;
a * m2 + self.b * m4 + self.c * m6 - MU_0 * field * magnetization
}
pub fn field_from_magnetization(&self, magnetization: f64, temperature: f64) -> f64 {
let a = self.a_coeff(temperature);
let m3 = magnetization * magnetization * magnetization;
let m5 = m3 * magnetization * magnetization;
(2.0 * a * magnetization + 4.0 * self.b * m3 + 6.0 * self.c * m5) / MU_0
}
pub fn susceptibility(&self, temperature: f64) -> f64 {
let a = self.a_coeff(temperature).abs();
if a < 1e-30 {
return f64::INFINITY;
}
MU_0 / (2.0 * a)
}
pub fn spontaneous_magnetization(&self, temperature: f64) -> f64 {
if temperature >= self.curie_temperature {
return 0.0;
}
let a = self.a_coeff(temperature);
if self.b > 1e-30 {
let m2 = -a / (2.0 * self.b);
if m2 > 0.0 { m2.sqrt() } else { 0.0 }
} else {
0.0
}
}
pub fn transition_type(&self) -> &'static str {
if self.b < 0.0 {
"first-order"
} else {
"second-order"
}
}
}
pub struct MeanFieldMagnetics {
pub total_angular_momentum: f64,
pub g_factor: f64,
pub weiss_parameter: f64,
pub number_density: f64,
}
impl MeanFieldMagnetics {
pub fn new(
total_angular_momentum: f64,
g_factor: f64,
weiss_parameter: f64,
number_density: f64,
) -> Self {
Self {
total_angular_momentum,
g_factor,
weiss_parameter,
number_density,
}
}
pub fn gadolinium(number_density: f64) -> Self {
Self::new(3.5, 2.0, 1.0e3, number_density)
}
pub fn brillouin(&self, x: f64) -> f64 {
let j = self.total_angular_momentum;
let two_j = 2.0 * j;
let a = (two_j + 1.0) / two_j;
let b = 1.0 / two_j;
if x.abs() < 1e-10 {
return (j + 1.0) / (3.0 * j) * x;
}
a * coth(a * x) - b * coth(b * x)
}
pub fn saturation_magnetization(&self) -> f64 {
self.number_density * self.g_factor * self.total_angular_momentum * MU_B
}
pub fn curie_temperature(&self) -> f64 {
let j = self.total_angular_momentum;
let g = self.g_factor;
let n = self.number_density;
let lambda = self.weiss_parameter;
n * g * g * MU_B * MU_B * j * (j + 1.0) * lambda / (3.0 * K_B)
}
pub fn reduced_magnetization(
&self,
temperature: f64,
applied_field: f64,
n_iter: usize,
) -> f64 {
if temperature <= 0.0 {
return 1.0;
}
let j = self.total_angular_momentum;
let g = self.g_factor;
let ms = self.saturation_magnetization();
let mut m = 0.5_f64;
for _ in 0..n_iter {
let h_eff = applied_field + self.weiss_parameter * ms * m;
let x = g * j * MU_B * h_eff / (K_B * temperature);
m = self.brillouin(x);
}
m
}
pub fn curie_weiss_susceptibility(&self, temperature: f64) -> f64 {
let j = self.total_angular_momentum;
let g = self.g_factor;
let n = self.number_density;
let c = n * g * g * j * (j + 1.0) * MU_B * MU_B / (3.0 * K_B);
let tc = self.curie_temperature();
if (temperature - tc).abs() < 1e-6 {
return f64::INFINITY;
}
c / (temperature - tc)
}
}
#[inline]
fn coth(x: f64) -> f64 {
if x.abs() > 100.0 {
return x.signum();
}
let ex = x.exp();
let em = (-x).exp();
(ex + em) / (ex - em)
}
pub struct MagnetoCaloric {
pub martensite_start: f64,
pub martensite_finish: f64,
pub austenite_start: f64,
pub austenite_finish: f64,
pub structural_entropy_change: f64,
pub critical_field: f64,
}
impl MagnetoCaloric {
pub fn new(
martensite_start: f64,
martensite_finish: f64,
austenite_start: f64,
austenite_finish: f64,
structural_entropy_change: f64,
critical_field: f64,
) -> Self {
Self {
martensite_start,
martensite_finish,
austenite_start,
austenite_finish,
structural_entropy_change,
critical_field,
}
}
pub fn ni_mn_ga() -> Self {
Self::new(300.0, 290.0, 310.0, 320.0, 18.0, 1.0)
}
pub fn martensite_fraction(&self, temperature: f64) -> f64 {
let ms = self.martensite_start;
let mf = self.martensite_finish;
if temperature >= ms {
return 0.0;
}
if temperature <= mf {
return 1.0;
}
(ms - temperature) / (ms - mf)
}
pub fn metamagnetic_entropy_change(&self, temperature: f64, field: f64) -> f64 {
let xi = self.martensite_fraction(temperature);
let field_factor = (field / self.critical_field).min(1.0);
-self.structural_entropy_change * xi * field_factor
}
pub fn is_martensitic(&self, temperature: f64) -> bool {
temperature <= self.martensite_start
}
pub fn virgin_curve_magnetization(&self, field: f64, saturation_mag: f64) -> f64 {
if field >= self.critical_field {
saturation_mag
} else {
saturation_mag * field / self.critical_field
}
}
}
pub struct BarocaloriEffect {
pub dt_dp: f64,
pub ds_dp: f64,
pub specific_heat: f64,
pub temperature: f64,
}
impl BarocaloriEffect {
pub fn new(dt_dp: f64, ds_dp: f64, specific_heat: f64, temperature: f64) -> Self {
Self {
dt_dp,
ds_dp,
specific_heat,
temperature,
}
}
pub fn isothermal_entropy_change(&self, delta_pressure_gpa: f64) -> f64 {
self.ds_dp * delta_pressure_gpa
}
pub fn adiabatic_temp_change(&self, delta_pressure_gpa: f64) -> f64 {
let ds = self.isothermal_entropy_change(delta_pressure_gpa);
-(self.temperature / self.specific_heat) * ds
}
pub fn barocaloric_coefficient(&self) -> f64 {
self.adiabatic_temp_change(1.0)
}
pub fn clausius_clapeyron_pressure(&self) -> f64 {
self.dt_dp
}
}
pub struct ElastocaloricMaterial {
pub transformation_entropy: f64,
pub critical_stress: f64,
pub clausius_clapeyron_slope: f64,
pub specific_heat: f64,
pub temperature: f64,
pub hysteresis_stress: f64,
}
impl ElastocaloricMaterial {
pub fn new(
transformation_entropy: f64,
critical_stress: f64,
clausius_clapeyron_slope: f64,
specific_heat: f64,
temperature: f64,
hysteresis_stress: f64,
) -> Self {
Self {
transformation_entropy,
critical_stress,
clausius_clapeyron_slope,
specific_heat,
temperature,
hysteresis_stress,
}
}
pub fn nitinol() -> Self {
Self::new(20.0, 200.0, 6.0, 500.0, 310.0, 60.0)
}
pub fn adiabatic_temp_change(&self) -> f64 {
-self.temperature * self.transformation_entropy / self.specific_heat
}
pub fn cooling_power_per_cycle(&self) -> f64 {
let gross = self.transformation_entropy.abs() * self.temperature;
let loss = self.hysteresis_stress * 0.01; (gross - loss).max(0.0)
}
pub fn critical_stress_at(&self, temperature: f64) -> f64 {
self.critical_stress + self.clausius_clapeyron_slope * (temperature - self.temperature)
}
pub fn superelastic_work_input(&self, strain_amplitude: f64) -> f64 {
self.critical_stress * 1e6 * strain_amplitude / 1000.0 }
}
pub struct SpinCaloritronics {
pub spin_seebeck_coeff: f64,
pub spin_conductivity: f64,
pub spin_nernst_coeff: f64,
pub tmr_ratio: f64,
pub temperature: f64,
}
impl SpinCaloritronics {
pub fn new(
spin_seebeck_coeff: f64,
spin_conductivity: f64,
spin_nernst_coeff: f64,
tmr_ratio: f64,
temperature: f64,
) -> Self {
Self {
spin_seebeck_coeff,
spin_conductivity,
spin_nernst_coeff,
tmr_ratio,
temperature,
}
}
pub fn spin_seebeck_voltage(&self, delta_t: f64) -> f64 {
self.spin_seebeck_coeff * delta_t
}
pub fn spin_peltier_heat_flux(&self, spin_current_density: f64) -> f64 {
let pi_s = self.spin_seebeck_coeff * self.temperature;
pi_s * spin_current_density
}
pub fn spin_nernst_voltage(&self, delta_t: f64) -> f64 {
self.spin_nernst_coeff * delta_t
}
pub fn spin_figure_of_merit(&self) -> f64 {
let l0 = 2.44e-8_f64;
self.spin_seebeck_coeff * self.spin_seebeck_coeff / l0
}
pub fn seebeck_spin_tunneling_voltage(&self, delta_t: f64, spin_polarization: f64) -> f64 {
spin_polarization * self.spin_seebeck_coeff * delta_t
}
pub fn tunnel_spin_current(&self, delta_t: f64) -> f64 {
self.spin_conductivity * self.spin_seebeck_coeff * delta_t
}
}
pub fn lorentzian_peak(x: f64, x0: f64, gamma: f64) -> f64 {
let half_g = gamma / 2.0;
half_g * half_g / ((x - x0) * (x - x0) + half_g * half_g)
}
pub fn gaussian_peak(x: f64, x0: f64, sigma: f64) -> f64 {
let z = (x - x0) / sigma;
(-0.5 * z * z).exp()
}
pub fn bose_einstein(hbar_omega: f64, temperature: f64) -> f64 {
if temperature <= 0.0 {
return 0.0;
}
let x = hbar_omega / (K_B * temperature);
if x > 700.0 {
return 0.0;
}
1.0 / (x.exp() - 1.0)
}
pub fn magnon_specific_heat(temperature: f64, bloch_coefficient: f64) -> f64 {
if temperature <= 0.0 {
return 0.0;
}
1.5 * bloch_coefficient * temperature.powf(0.5)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
#[test]
fn test_gd_mce_linear_field_scaling() {
let gd = MagnetocaloricMaterial::gadolinium();
let dt2 = gd.adiabatic_temp_change(2.0);
let dt4 = gd.adiabatic_temp_change(4.0);
assert!(
(dt4 - 2.0 * dt2).abs() < EPS,
"ΔT_ad should scale linearly: dt2={dt2}, dt4={dt4}"
);
}
#[test]
fn test_isothermal_entropy_positive() {
let mat = MagnetocaloricMaterial::gadolinium();
let ds = mat.isothermal_entropy_change(2.0);
assert!(
ds > 0.0,
"ΔS_M should be positive for a positive field swing"
);
}
#[test]
fn test_refrigerant_capacity_fwhm() {
let mat = MagnetocaloricMaterial::gadolinium();
let rc5 = mat.refrigerant_capacity(2.0, 5.0);
let rc10 = mat.refrigerant_capacity(2.0, 10.0);
assert!(
rc10 > rc5,
"Larger FWHM should give greater refrigerant capacity"
);
}
#[test]
fn test_maxwell_relation_linearity() {
let me = MagneticEntropy::new(0.1574, 7.0);
let ds1 = me.maxwell_relation_entropy_change(1.0, 1e5);
let ds2 = me.maxwell_relation_entropy_change(2.0, 1e5);
assert!(
(ds2 - 2.0 * ds1).abs() < EPS,
"Maxwell ΔS should be proportional to dM/dT"
);
}
#[test]
fn test_clausius_clapeyron_sign() {
let me = MagneticEntropy::new(0.1265, 4.5);
let slope = me.clausius_clapeyron_slope(50.0, 30.0);
assert!(
slope < 0.0,
"dT_C/dH should be negative for ΔM>0, ΔS>0: {slope}"
);
}
#[test]
fn test_disordered_entropy_j_half() {
let me = MagneticEntropy::new(R_GAS, 0.5);
let s = me.disordered_entropy(0.5);
let expected = 2_f64.ln();
assert!(
(s - expected).abs() < 1e-8,
"Disordered entropy for J=1/2 should be ln(2)={expected:.6}, got {s:.6}"
);
}
#[test]
fn test_gmce_lafesi_peak_greater_than_gd() {
let lafesi = GiantMce::la_fe_si();
let gd = MagnetocaloricMaterial::gadolinium();
assert!(
lafesi.peak_entropy_change > gd.delta_s_per_tesla * 2.0,
"La(Fe,Si)13 peak ΔS={} should exceed Gd at 2 T ({})",
lafesi.peak_entropy_change,
gd.delta_s_per_tesla * 2.0
);
}
#[test]
fn test_gmce_entropy_change_negative() {
let lafesi = GiantMce::la_fe_si();
let ds = lafesi.entropy_change(lafesi.transition_temp, 2.0);
assert!(
ds < 0.0,
"Entropy change should be negative (cooling): {ds}"
);
}
#[test]
fn test_gmce_first_order_flag() {
let lafesi = GiantMce::la_fe_si();
assert!(
lafesi.is_first_order(),
"La(Fe,Si) should be identified as first-order"
);
}
#[test]
fn test_amr_carnot_cop() {
let mat = MagnetocaloricMaterial::gadolinium();
let amr = MagneticRefrigeration::new(300.0, 270.0, 2.0, mat);
let cop = amr.carnot_cop();
let expected = 270.0 / 30.0;
assert!(
(cop - expected).abs() < EPS,
"Carnot COP should be {expected:.4}, got {cop:.4}"
);
}
#[test]
fn test_amr_cop_positive() {
let mat = MagnetocaloricMaterial::gadolinium();
let amr = MagneticRefrigeration::new(300.0, 270.0, 2.0, mat);
let cop = amr.amr_cop();
assert!(
cop > 0.0 && cop.is_finite(),
"AMR COP should be positive and finite: {cop}"
);
}
#[test]
fn test_landau_free_energy_zero() {
let lm = LandauTheoryMagneto::new(1.0, 1.0, 0.1, 300.0);
let f = lm.free_energy(0.0, 300.0, 0.0);
assert!(f.abs() < EPS, "Free energy at M=0, H=0 should be zero: {f}");
}
#[test]
fn test_landau_no_magnetization_above_tc() {
let lm = LandauTheoryMagneto::new(1.0, 1.0, 0.1, 300.0);
let ms = lm.spontaneous_magnetization(350.0);
assert!(ms.abs() < EPS, "M_s should be zero above T_C: {ms}");
}
#[test]
fn test_landau_second_order_type() {
let lm = LandauTheoryMagneto::new(1.0, 1.0, 0.1, 300.0);
assert_eq!(lm.transition_type(), "second-order");
}
#[test]
fn test_mean_field_curie_temp_scaling() {
let mf1 = MeanFieldMagnetics::gadolinium(1e28);
let mf2 = MeanFieldMagnetics::new(3.5, 2.0, 2.0e3, 1e28);
let tc1 = mf1.curie_temperature();
let tc2 = mf2.curie_temperature();
assert!(
(tc2 - 2.0 * tc1).abs() < 1e-4,
"Doubling λ should double T_C: tc1={tc1:.4}, tc2={tc2:.4}"
);
}
#[test]
fn test_mean_field_saturation_at_zero_temp() {
let mf = MeanFieldMagnetics::gadolinium(1e28);
let m = mf.reduced_magnetization(1.0, 0.0, 200);
assert!(
(m - 1.0).abs() < 0.01,
"Reduced magnetisation at T→0 should approach 1: m={m:.6}"
);
}
#[test]
fn test_brillouin_at_zero() {
let mf = MeanFieldMagnetics::gadolinium(1e28);
let b = mf.brillouin(0.0);
assert!(b.abs() < 1e-8, "B_J(0) should be 0: {b}");
}
#[test]
fn test_heusler_martensite_fraction_below_mf() {
let h = MagnetoCaloric::ni_mn_ga();
let xi = h.martensite_fraction(280.0);
assert!(
(xi - 1.0).abs() < EPS,
"Martensite fraction should be 1 below M_f: {xi}"
);
}
#[test]
fn test_heusler_martensite_fraction_above_ms() {
let h = MagnetoCaloric::ni_mn_ga();
let xi = h.martensite_fraction(310.0);
assert!(
xi.abs() < EPS,
"Martensite fraction should be 0 above M_s: {xi}"
);
}
#[test]
fn test_barocaloric_sign() {
let bce = BarocaloriEffect::new(5.0, 10.0, 1000.0, 300.0);
let dt = bce.adiabatic_temp_change(0.5);
assert!(dt < 0.0, "Barocaloric ΔT should be negative for ΔS>0: {dt}");
}
#[test]
fn test_barocaloric_entropy_linear() {
let bce = BarocaloriEffect::new(5.0, 10.0, 1000.0, 300.0);
let ds1 = bce.isothermal_entropy_change(1.0);
let ds2 = bce.isothermal_entropy_change(2.0);
assert!(
(ds2 - 2.0 * ds1).abs() < EPS,
"Barocaloric ΔS should scale linearly: ds1={ds1}, ds2={ds2}"
);
}
#[test]
fn test_elastocaloric_nitinol_cooling() {
let niti = ElastocaloricMaterial::nitinol();
let dt = niti.adiabatic_temp_change();
assert!(dt < 0.0, "NiTi should cool on stress loading, ΔT_ad={dt}");
}
#[test]
fn test_elastocaloric_stress_temperature_slope() {
let niti = ElastocaloricMaterial::nitinol();
let sigma_low = niti.critical_stress_at(300.0);
let sigma_high = niti.critical_stress_at(320.0);
assert!(
sigma_high > sigma_low,
"Critical stress should increase with T (Clausius-Clapeyron): σ_low={sigma_low}, σ_high={sigma_high}"
);
}
#[test]
fn test_spin_seebeck_proportional_to_dt() {
let sc = SpinCaloritronics::new(100e-6, 1e4, 50e-9, 0.3, 300.0);
let v1 = sc.spin_seebeck_voltage(10.0);
let v2 = sc.spin_seebeck_voltage(20.0);
assert!(
(v2 - 2.0 * v1).abs() < EPS,
"SSE voltage should be proportional to ΔT: v1={v1}, v2={v2}"
);
}
#[test]
fn test_spin_peltier_proportional_to_current() {
let sc = SpinCaloritronics::new(100e-6, 1e4, 50e-9, 0.3, 300.0);
let q1 = sc.spin_peltier_heat_flux(1e6);
let q2 = sc.spin_peltier_heat_flux(2e6);
assert!(
(q2 - 2.0 * q1).abs() < 1e-15,
"Spin Peltier heat flux should double with double current: q1={q1}, q2={q2}"
);
}
#[test]
fn test_lorentzian_peak_at_centre() {
let l = lorentzian_peak(5.0, 5.0, 2.0);
assert!(
(l - 1.0).abs() < EPS,
"Lorentzian at centre should be 1: {l}"
);
}
#[test]
fn test_gaussian_peak_at_centre() {
let g = gaussian_peak(3.0, 3.0, 1.0);
assert!((g - 1.0).abs() < EPS, "Gaussian at centre should be 1: {g}");
}
#[test]
fn test_numerical_maxwell_integration() {
let me = MagneticEntropy::new(0.1574, 7.0);
let ds_data: Vec<(f64, f64)> = (0..=10).map(|i| (i as f64, 5.0)).collect();
let integral = me.integrated_entropy(&ds_data);
assert!(
(integral - 50.0).abs() < 1e-6,
"Integrated entropy should be 50 J/kg: {integral:.4}"
);
}
#[test]
fn test_volumetric_cooling_power_frequency() {
let mat = MagnetocaloricMaterial::gadolinium();
let amr = MagneticRefrigeration::new(300.0, 270.0, 2.0, mat);
let p1 = amr.volumetric_cooling_power(1.0);
let p2 = amr.volumetric_cooling_power(2.0);
assert!(
(p2 - 2.0 * p1).abs() < EPS,
"Volumetric cooling power should scale linearly with frequency"
);
}
#[test]
fn test_magnon_specific_heat_increases() {
let c100 = magnon_specific_heat(100.0, 1e-3);
let c200 = magnon_specific_heat(200.0, 1e-3);
assert!(
c200 > c100,
"Magnon specific heat should increase with T: c100={c100:.6}, c200={c200:.6}"
);
}
#[test]
fn test_pi_constant() {
use std::f64::consts::{PI, TAU};
let two_pi = 2.0 * PI;
assert!((two_pi - TAU).abs() < 1e-6);
}
}