#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use super::heat_current::HeatCurrentCalculator;
use super::onsager::OnsagerMatrix;
use crate::error::Error;
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SpinCaloritronicsMaterial {
pub onsager: OnsagerMatrix,
pub heat_calc: HeatCurrentCalculator,
pub name: String,
}
impl SpinCaloritronicsMaterial {
pub fn new(onsager: OnsagerMatrix) -> Self {
let peltier_coeff = onsager.temperature * onsager.seebeck;
let spin_peltier_coeff = onsager.temperature * onsager.spin_seebeck;
let heat_calc = HeatCurrentCalculator::new(
onsager.thermal_conductivity,
peltier_coeff,
spin_peltier_coeff,
);
Self {
onsager,
heat_calc,
name: String::from("custom"),
}
}
pub fn yig_pt(temperature: f64) -> Self {
let onsager = OnsagerMatrix::yig_pt(temperature);
let mut mat = Self::new(onsager);
mat.name = String::from("YIG/Pt");
mat
}
pub fn fe_pt(temperature: f64) -> Self {
let onsager = OnsagerMatrix::fe_pt(temperature);
let mut mat = Self::new(onsager);
mat.name = String::from("Fe/Pt");
mat
}
pub fn cofeb_pt(temperature: f64) -> Self {
let onsager = OnsagerMatrix::cofeb_pt(temperature);
let mut mat = Self::new(onsager);
mat.name = String::from("CoFeB/Pt");
mat
}
pub fn compute_all(
&self,
grad_t: &Vector3<f64>,
j_spin: &Vector3<f64>,
) -> Result<CaloritronicsResult, Error> {
if self.onsager.temperature <= 0.0 {
return Err(Error::InvalidParameter {
param: String::from("temperature"),
reason: String::from("must be positive"),
});
}
if self.onsager.conductivity <= 0.0 {
return Err(Error::InvalidParameter {
param: String::from("conductivity"),
reason: String::from("must be positive"),
});
}
let spin_seebeck_current = self.onsager.spin_current_from_grad_t(grad_t);
let spin_peltier_heat_vec = self.heat_calc.spin_peltier_current(j_spin);
let peltier_heat = spin_peltier_heat_vec.magnitude();
let grad_t_magnitude = grad_t.magnitude();
let nernst_voltage = self.onsager.nernst_voltage(grad_t_magnitude);
let spin_nernst_current = Vector3::new(
self.onsager.hall_angle * spin_seebeck_current.x,
self.onsager.hall_angle * spin_seebeck_current.y,
self.onsager.hall_angle * spin_seebeck_current.z,
);
let reciprocity_satisfied = self.onsager.reciprocity_error() < 1.0e-10;
let zero_charge = Vector3::zero();
let total_heat_current = self
.heat_calc
.total_heat_current(grad_t, &zero_charge, j_spin);
Ok(CaloritronicsResult {
spin_seebeck_current,
peltier_heat,
nernst_voltage,
spin_nernst_current,
reciprocity_satisfied,
total_heat_current,
})
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CaloritronicsResult {
pub spin_seebeck_current: Vector3<f64>,
pub peltier_heat: f64,
pub nernst_voltage: f64,
pub spin_nernst_current: Vector3<f64>,
pub reciprocity_satisfied: bool,
pub total_heat_current: Vector3<f64>,
}
#[cfg(test)]
mod tests {
use super::*;
fn grad_t_x(magnitude: f64) -> Vector3<f64> {
Vector3::new(magnitude, 0.0, 0.0)
}
fn zero_vec() -> Vector3<f64> {
Vector3::zero()
}
fn small_spin() -> Vector3<f64> {
Vector3::new(1.0e3, 0.0, 0.0)
}
#[test]
fn test_yig_pt_material_builds() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
assert_eq!(mat.name, "YIG/Pt");
assert!(mat.onsager.conductivity > 0.0);
assert!(mat.onsager.temperature > 0.0);
}
#[test]
fn test_fe_pt_material_builds() {
let mat = SpinCaloritronicsMaterial::fe_pt(300.0);
assert_eq!(mat.name, "Fe/Pt");
assert!(mat.onsager.conductivity > 0.0);
}
#[test]
fn test_cofeb_pt_material_builds() {
let mat = SpinCaloritronicsMaterial::cofeb_pt(300.0);
assert_eq!(mat.name, "CoFeB/Pt");
assert!(mat.onsager.conductivity > 0.0);
}
#[test]
fn test_onsager_reciprocity_yig_pt() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let err = mat.onsager.reciprocity_error();
assert!(err < 1.0e-6, "YIG/Pt reciprocity error {err} exceeds 1e-6");
}
#[test]
fn test_onsager_reciprocity_fe_pt() {
let mat = SpinCaloritronicsMaterial::fe_pt(300.0);
let err = mat.onsager.reciprocity_error();
assert!(err < 1.0e-6, "Fe/Pt reciprocity error {err} exceeds 1e-6");
}
#[test]
fn test_spin_current_direction() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let grad_t = grad_t_x(1000.0);
let j_spin = mat.onsager.spin_current_from_grad_t(&grad_t);
assert!(
j_spin.x > 0.0,
"spin current should flow along +x (∇T direction)"
);
assert!(j_spin.y.abs() < f64::EPSILON);
assert!(j_spin.z.abs() < f64::EPSILON);
}
#[test]
fn test_peltier_heat_from_onsager() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let j1 = Vector3::new(1.0e3, 0.0, 0.0);
let j2 = Vector3::new(2.0e3, 0.0, 0.0);
let heat1 = mat.onsager.heat_current_from_spin_current(&j1);
let heat2 = mat.onsager.heat_current_from_spin_current(&j2);
let ratio = heat2.magnitude() / heat1.magnitude();
assert!(
(ratio - 2.0).abs() < 1.0e-10,
"Peltier heat not proportional: ratio = {ratio}"
);
}
#[test]
fn test_nernst_sign() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let nernst_v = mat.onsager.nernst_voltage(1000.0);
assert!(
nernst_v > 0.0,
"Nernst voltage should be positive for negative S_e"
);
}
#[test]
fn test_zero_field_zero_charge_current() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let grad_t = grad_t_x(1000.0);
let e_field = zero_vec();
let currents = mat.onsager.all_currents(&grad_t, &e_field);
let expected_jc_x = mat.onsager.conductivity * mat.onsager.seebeck * grad_t.x;
assert!(
(currents.charge_current.x - expected_jc_x).abs() < 1.0e-6,
"charge current mismatch"
);
}
#[test]
fn test_zero_grad_t_zero_spin_current() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let grad_t = zero_vec();
let j_spin = mat.onsager.spin_current_from_grad_t(&grad_t);
assert!(
j_spin.magnitude() < f64::EPSILON,
"Spin current should be zero for zero ∇T"
);
}
#[test]
fn test_peltier_seebeck_ratio() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let expected_pi_s = mat.onsager.temperature * mat.onsager.spin_seebeck;
assert!(
(mat.heat_calc.spin_peltier_coeff - expected_pi_s).abs() < 1.0e-12,
"Kelvin relation violated: Π_s = {}, T·S_s = {}",
mat.heat_calc.spin_peltier_coeff,
expected_pi_s
);
}
#[test]
fn test_compute_all_nonzero_for_grad_t() {
let mat = SpinCaloritronicsMaterial::yig_pt(300.0);
let grad_t = grad_t_x(500.0);
let j_spin = zero_vec();
let result = mat
.compute_all(&grad_t, &j_spin)
.expect("compute_all failed");
assert!(
result.spin_seebeck_current.magnitude() > 0.0,
"spin Seebeck current should be nonzero"
);
}
#[test]
fn test_compute_all_reciprocity_flag() {
for (name, mat) in [
("YIG/Pt", SpinCaloritronicsMaterial::yig_pt(300.0)),
("Fe/Pt", SpinCaloritronicsMaterial::fe_pt(300.0)),
("CoFeB/Pt", SpinCaloritronicsMaterial::cofeb_pt(300.0)),
] {
let grad_t = grad_t_x(100.0);
let j_spin = small_spin();
let result = mat
.compute_all(&grad_t, &j_spin)
.expect("compute_all failed");
assert!(
result.reciprocity_satisfied,
"{name}: reciprocity_satisfied should be true"
);
}
}
#[test]
fn test_heat_calculator_fourier() {
let kappa = 46.0_f64;
let calc = HeatCurrentCalculator::new(kappa, 0.0, 0.0);
let grad_t = Vector3::new(1000.0, 0.0, 0.0);
let j_q = calc.fourier_current(&grad_t);
assert!(
(j_q.x - (-kappa * 1000.0)).abs() < 1.0e-8,
"Fourier current x mismatch: got {}, expected {}",
j_q.x,
-kappa * 1000.0
);
assert!(j_q.y.abs() < f64::EPSILON);
assert!(j_q.z.abs() < f64::EPSILON);
}
#[test]
fn test_total_heat_current_combines_all_mechanisms() {
let kappa = 46.0_f64;
let pi_charge = -1.5e-3_f64;
let pi_spin = 0.3_f64;
let calc = HeatCurrentCalculator::new(kappa, pi_charge, pi_spin);
let grad_t = Vector3::new(1000.0, 0.0, 0.0);
let j_charge = Vector3::new(1.0e6, 0.0, 0.0);
let j_spin = Vector3::new(1.0e3, 0.0, 0.0);
let total = calc.total_heat_current(&grad_t, &j_charge, &j_spin);
let expected_x = (-kappa * 1000.0) + (pi_charge * 1.0e6) + (pi_spin * 1.0e3);
assert!(
(total.x - expected_x).abs() < 1.0e-6,
"total heat current x mismatch: got {}, expected {}",
total.x,
expected_x
);
let fourier = calc.fourier_current(&grad_t);
let peltier = calc.peltier_current(&j_charge);
let spin_peltier = calc.spin_peltier_current(&j_spin);
let sum_x = fourier.x + peltier.x + spin_peltier.x;
assert!(
(total.x - sum_x).abs() < 1.0e-8,
"total should equal sum of components"
);
}
}