use std::f64::consts::PI;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::constants::{E_CHARGE, GAMMA, HBAR, KB, MU_0};
use crate::error::{invalid_param, Result};
use crate::llg::LLGSolver;
use crate::material::Ferromagnet;
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SpinTorqueOscillatorConfig {
pub spin_polarization: f64,
pub t_fm: f64,
pub area: f64,
pub beta_stt: f64,
pub p_hat: Vector3<f64>,
pub volume: f64,
}
impl SpinTorqueOscillatorConfig {
pub fn new(
spin_polarization: f64,
t_fm: f64,
area: f64,
beta_stt: f64,
p_hat: Vector3<f64>,
) -> Result<Self> {
if spin_polarization <= 0.0 || spin_polarization >= 1.0 {
return Err(invalid_param(
"spin_polarization",
"must be strictly between 0 and 1",
));
}
if t_fm <= 0.0 {
return Err(invalid_param(
"t_fm",
"free-layer thickness must be positive",
));
}
if area <= 0.0 {
return Err(invalid_param("area", "device area must be positive"));
}
let volume = area * t_fm;
let p_hat_norm = p_hat.normalize();
Ok(Self {
spin_polarization,
t_fm,
area,
beta_stt,
p_hat: p_hat_norm,
volume,
})
}
}
pub struct SpinTorqueOscillator {
pub solver: LLGSolver,
pub config: SpinTorqueOscillatorConfig,
}
impl std::fmt::Debug for SpinTorqueOscillator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SpinTorqueOscillator")
.field("material_ms", &self.solver.material.ms)
.field("material_alpha", &self.solver.material.alpha)
.field("h_ext", &self.solver.h_ext)
.field("config", &self.config)
.finish()
}
}
impl Clone for SpinTorqueOscillator {
fn clone(&self) -> Self {
let mut solver = LLGSolver::new(self.solver.material.clone());
solver.h_ext = self.solver.h_ext;
Self {
solver,
config: self.config.clone(),
}
}
}
impl SpinTorqueOscillator {
pub fn new(
material: Ferromagnet,
h_ext: Vector3<f64>,
config: SpinTorqueOscillatorConfig,
) -> Result<Self> {
let mut solver = LLGSolver::new(material);
solver.h_ext = h_ext;
Ok(Self { solver, config })
}
pub fn permalloy_nanopillar() -> Result<Self> {
let mut material = Ferromagnet::permalloy();
material.alpha = 0.01;
material.ms = 8.0e5;
let h_ext = Vector3::new(0.0, 0.0, 1.0e5); let t_fm = 5.0e-9; let radius = 50.0e-9; let area = PI * radius * radius;
let config = SpinTorqueOscillatorConfig::new(
0.35, t_fm, area, 0.01, Vector3::new(0.0, 0.0, -1.0), )?;
Self::new(material, h_ext, config)
}
#[inline]
pub fn a_j(&self, j_density: f64) -> f64 {
let ms = self.solver.material.ms;
let eta = self.config.spin_polarization;
let t_fm = self.config.t_fm;
(HBAR / (2.0 * E_CHARGE)) * j_density * eta / (MU_0 * ms * t_fm)
}
pub fn slonczewski_torque(&self, m: Vector3<f64>, j_density: f64) -> Vector3<f64> {
let a_j = self.a_j(j_density);
let m_hat = m.normalize();
let p = self.config.p_hat;
let m_cross_p = m_hat.cross(&p); let m_cross_m_cross_p = m_hat.cross(&m_cross_p);
m_cross_m_cross_p * a_j + m_cross_p * (self.config.beta_stt * a_j)
}
pub fn dmdt_with_stt(&self, m: Vector3<f64>, j_density: f64) -> Vector3<f64> {
let ms = self.solver.material.ms;
let alpha = self.solver.material.alpha;
let gamma = GAMMA;
let h_eff = self.solver.effective_field(m);
let m_hat = m.normalize();
let factor = gamma / (1.0 + alpha * alpha);
let m_cross_h = m_hat.cross(&h_eff); let m_cross_m_cross_h = m_hat.cross(&m_cross_h);
let llg_term = (m_cross_h + m_cross_m_cross_h * alpha) * (-factor * ms);
let a_j = self.a_j(j_density);
let p = self.config.p_hat;
let m_cross_p = m_hat.cross(&p);
let m_cross_m_cross_p = m_hat.cross(&m_cross_p);
let stt_term = (m_cross_m_cross_p + m_cross_p * alpha) * (factor * a_j * ms);
llg_term + stt_term
}
pub fn threshold_current_density(&self) -> f64 {
let ms = self.solver.material.ms;
let alpha = self.solver.material.alpha;
let t_fm = self.config.t_fm;
let eta = self.config.spin_polarization;
(2.0 * E_CHARGE * MU_0 * ms * t_fm * alpha) / (HBAR * eta)
}
pub fn simulate(
&self,
m0: Vector3<f64>,
dt: f64,
n_steps: usize,
j_density: f64,
) -> Vec<Vector3<f64>> {
let ms = self.solver.material.ms;
let mut trajectory = Vec::with_capacity(n_steps + 1);
trajectory.push(m0);
let mut m = m0;
for _ in 0..n_steps {
m = self.rk4_step(m, dt, j_density, ms);
trajectory.push(m);
}
trajectory
}
fn rk4_step(&self, m: Vector3<f64>, dt: f64, j_density: f64, ms: f64) -> Vector3<f64> {
let half_dt = dt * 0.5;
let k1 = self.dmdt_with_stt(m, j_density);
let k2 = self.dmdt_with_stt(m + k1 * half_dt, j_density);
let k3 = self.dmdt_with_stt(m + k2 * half_dt, j_density);
let k4 = self.dmdt_with_stt(m + k3 * dt, j_density);
let m_new = m + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
let mag = m_new.magnitude();
if mag > 1.0e-20 {
m_new * (ms / mag)
} else {
let h_mag = self.solver.h_ext.magnitude();
if h_mag > 0.0 {
self.solver.h_ext * (ms / h_mag)
} else {
Vector3::new(0.0, 0.0, ms)
}
}
}
pub fn oscillation_frequency(&self, trajectory: &[Vector3<f64>], dt: f64) -> Option<f64> {
if trajectory.len() < 2 {
return None;
}
let ms = self.solver.material.ms;
let ms_inv = if ms > 0.0 { 1.0 / ms } else { return None };
let mut n_crossings: usize = 0;
let skip = trajectory.len() / 5;
let working = &trajectory[skip..];
if working.len() < 2 {
return None;
}
let mut prev_sign = working[0].z * ms_inv > 0.0;
for state in working.iter().skip(1) {
let current_sign = state.z * ms_inv > 0.0;
if current_sign != prev_sign {
n_crossings += 1;
}
prev_sign = current_sign;
}
if n_crossings < 2 {
return None;
}
let t_total = (working.len() - 1) as f64 * dt;
Some(n_crossings as f64 / (2.0 * t_total))
}
pub fn oscillation_power(&self, trajectory: &[Vector3<f64>]) -> f64 {
if trajectory.is_empty() {
return 0.0;
}
let ms = self.solver.material.ms;
let ms_safe = if ms > 0.0 { ms } else { return 0.0 };
let n = trajectory.len() as f64;
let mean = trajectory.iter().map(|m| m.z / ms_safe).sum::<f64>() / n;
let variance = trajectory
.iter()
.map(|m| {
let diff = m.z / ms_safe - mean;
diff * diff
})
.sum::<f64>()
/ n;
variance
}
pub fn thermal_linewidth(&self, _j_density: f64, temperature: f64) -> f64 {
let ms = self.solver.material.ms;
let alpha = self.solver.material.alpha;
let m_eq = {
let h_mag = self.solver.h_ext.magnitude();
if h_mag > 0.0 {
self.solver.h_ext * (ms / h_mag)
} else {
Vector3::new(0.0, 0.0, ms)
}
};
let h_eff = self.solver.effective_field(m_eq);
let h_eff_magnitude = h_eff.magnitude();
let e_spin = 0.5 * MU_0 * ms * self.config.volume * h_eff_magnitude;
if e_spin < 1.0e-50 {
return 1.0e12;
}
(alpha * KB * temperature) / (PI * e_spin)
}
pub fn injection_locking_range(&self, q_factor: f64, i_injection_normalized: f64) -> f64 {
if q_factor <= 0.0 {
return 0.0;
}
let i = i_injection_normalized.clamp(0.0, 1.0 - f64::EPSILON);
let denom_sq = 1.0 - i * i;
if denom_sq <= 0.0 {
return f64::INFINITY;
}
let ms = self.solver.material.ms;
let m_eq = {
let h_mag = self.solver.h_ext.magnitude();
if h_mag > 0.0 {
self.solver.h_ext * (ms / h_mag)
} else {
Vector3::new(0.0, 0.0, ms)
}
};
let h_eff = self.solver.effective_field(m_eq);
let h_eff_magnitude = h_eff.magnitude();
let f_osc = GAMMA * h_eff_magnitude / (2.0 * PI);
f_osc / (2.0 * q_factor) * i / denom_sq.sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn py_stno() -> SpinTorqueOscillator {
SpinTorqueOscillator::permalloy_nanopillar()
.expect("permalloy_nanopillar construction should succeed")
}
fn tilted_m(stno: &SpinTorqueOscillator) -> Vector3<f64> {
let ms = stno.solver.material.ms;
Vector3::new(0.1, 0.0, 1.0).normalize() * ms
}
#[test]
fn test_config_invalid_spin_polarization_zero() {
let res = SpinTorqueOscillatorConfig::new(
0.0,
5.0e-9,
PI * (50.0e-9f64).powi(2),
0.01,
Vector3::new(0.0, 0.0, -1.0),
);
assert!(res.is_err());
}
#[test]
fn test_config_invalid_spin_polarization_one() {
let res = SpinTorqueOscillatorConfig::new(
1.0,
5.0e-9,
PI * (50.0e-9f64).powi(2),
0.01,
Vector3::new(0.0, 0.0, -1.0),
);
assert!(res.is_err());
}
#[test]
fn test_config_invalid_t_fm() {
let res = SpinTorqueOscillatorConfig::new(
0.35,
-1.0e-9,
PI * (50.0e-9f64).powi(2),
0.01,
Vector3::new(0.0, 0.0, -1.0),
);
assert!(res.is_err());
}
#[test]
fn test_config_invalid_area() {
let res =
SpinTorqueOscillatorConfig::new(0.35, 5.0e-9, 0.0, 0.01, Vector3::new(0.0, 0.0, -1.0));
assert!(res.is_err());
}
#[test]
fn test_config_volume_derived_correctly() {
let t_fm = 5.0e-9;
let area = PI * (50.0e-9f64).powi(2);
let config =
SpinTorqueOscillatorConfig::new(0.35, t_fm, area, 0.01, Vector3::new(0.0, 0.0, -1.0))
.expect("valid config");
let expected_volume = area * t_fm;
assert!((config.volume - expected_volume).abs() < 1.0e-40);
}
#[test]
fn test_threshold_positive() {
let stno = py_stno();
let j_th = stno.threshold_current_density();
assert!(
j_th > 0.0,
"threshold current density must be positive, got {j_th}"
);
assert!(j_th.is_finite(), "j_th must be finite");
assert!(j_th < 1.0e15, "j_th = {j_th} seems unrealistically large");
}
#[test]
fn test_magnetization_norm_conserved() {
let stno = py_stno();
let ms = stno.solver.material.ms;
let m0 = tilted_m(&stno);
let dt = 5.0e-13;
let trajectory = stno.simulate(m0, dt, 100, 0.0);
for (i, m) in trajectory.iter().enumerate() {
let norm_error = (m.magnitude() - ms).abs();
assert!(
norm_error < 1.0e-3, "step {i}: |m| = {:.6e} ≠ M_s = {ms:.6e} (error = {norm_error:.2e})",
m.magnitude()
);
}
}
#[test]
fn test_stt_perpendicular_to_m() {
let stno = py_stno();
let ms = stno.solver.material.ms;
let test_cases = [
Vector3::new(1.0, 0.0, 0.0) * ms,
Vector3::new(0.0, 1.0, 0.0) * ms,
Vector3::new(0.0, 0.0, 1.0) * ms,
Vector3::new(1.0, 1.0, 0.0).normalize() * ms,
Vector3::new(0.1, 0.0, 1.0).normalize() * ms,
];
let j_density = 1.0e12;
for m in &test_cases {
let tau = stno.slonczewski_torque(*m, j_density);
let m_hat = m.normalize();
let dot = tau.dot(&m_hat).abs();
assert!(
dot < 1.0e-6,
"τ_STT · m̂ = {dot:.3e} ≠ 0 for m = ({:.3}, {:.3}, {:.3})",
m_hat.x,
m_hat.y,
m_hat.z
);
}
}
#[test]
fn test_auto_oscillation() {
let mut material = Ferromagnet::permalloy();
material.alpha = 0.01;
material.ms = 8.0e5;
let h_ext = Vector3::new(0.0, 0.0, 0.0);
let t_fm = 5.0e-9;
let area = PI * (50.0e-9f64).powi(2);
let config = SpinTorqueOscillatorConfig::new(
0.35,
t_fm,
area,
0.0, Vector3::new(0.0, 0.0, 1.0), )
.expect("valid config");
let stno = SpinTorqueOscillator::new(material, h_ext, config).expect("valid STNO");
let ms = stno.solver.material.ms;
let j_th = stno.threshold_current_density();
let m0 = Vector3::new(1.0, 0.0, 0.0) * ms;
let dt = 1.0e-12;
let j = 10.0 * j_th;
let trajectory = stno.simulate(m0, dt, 2000, j);
let power = stno.oscillation_power(&trajectory);
assert!(
power > 1.0e-6,
"oscillation_power = {power:.4e} — expected > 0 at J = 10·J_th = {j:.2e} A/m²"
);
}
#[test]
fn test_below_threshold_stable() {
let stno = py_stno();
let ms = stno.solver.material.ms;
let m0 = Vector3::new(0.0, 0.0, ms); let dt = 5.0e-13;
let trajectory = stno.simulate(m0, dt, 2000, 0.0);
let m_z_final = trajectory.last().map(|m| m.z).unwrap_or(ms);
let drift = (m_z_final - ms).abs();
assert!(
drift < 0.01 * ms,
"J=0 from equilibrium: |m_z_final - Ms| = {drift:.4e} — should be near zero"
);
}
#[test]
fn test_slonczewski_sign() {
let material = Ferromagnet::permalloy();
let h_ext = Vector3::new(0.0, 0.0, 0.0);
let config = SpinTorqueOscillatorConfig::new(
0.35,
5.0e-9,
PI * (50.0e-9f64).powi(2),
0.0, Vector3::new(0.0, 0.0, 1.0), )
.expect("valid config");
let stno = SpinTorqueOscillator::new(material, h_ext, config).expect("valid STNO");
let ms = stno.solver.material.ms;
let m = Vector3::new(1.0, 0.0, 0.0) * ms; let j_positive = 1.0e12;
let tau = stno.slonczewski_torque(m, j_positive);
assert!(
tau.z < 0.0,
"τ_z = {:.4e} — expected < 0 (torque drives m away from p̂ for positive J)",
tau.z
);
assert!(
tau.x.abs() < 1.0e-6,
"τ_x = {:.4e} — should be ≈ 0 (perpendicular to m = x̂)",
tau.x
);
}
#[test]
fn test_a_j_scaling() {
let stno = py_stno();
let j1 = 1.0e11;
let j2 = 2.0e11;
let ratio = stno.a_j(j2) / stno.a_j(j1);
assert!(
(ratio - 2.0).abs() < 1.0e-10,
"a_J should be linear in J; ratio = {ratio}"
);
}
#[test]
fn test_a_j_sign() {
let stno = py_stno();
assert!(stno.a_j(1.0e12) > 0.0);
assert!(stno.a_j(-1.0e12) < 0.0);
}
#[test]
fn test_thermal_linewidth_positive() {
let stno = py_stno();
let j_th = stno.threshold_current_density();
let lw = stno.thermal_linewidth(1.5 * j_th, 300.0);
assert!(lw > 0.0, "linewidth must be positive, got {lw}");
assert!(lw.is_finite(), "linewidth must be finite, got {lw}");
}
#[test]
fn test_thermal_linewidth_temperature_scaling() {
let stno = py_stno();
let j = 1.5 * stno.threshold_current_density();
let lw_300 = stno.thermal_linewidth(j, 300.0);
let lw_600 = stno.thermal_linewidth(j, 600.0);
let ratio = lw_600 / lw_300;
assert!(
(ratio - 2.0).abs() < 1.0e-6,
"Δf should scale linearly with T; ratio = {ratio}"
);
}
#[test]
fn test_injection_locking_range_positive() {
let stno = py_stno();
let delta_f = stno.injection_locking_range(100.0, 0.5);
assert!(
delta_f > 0.0,
"locking range must be positive, got {delta_f}"
);
}
#[test]
fn test_injection_locking_range_zero_q() {
let stno = py_stno();
let delta_f = stno.injection_locking_range(0.0, 0.5);
assert_eq!(delta_f, 0.0, "zero Q should give zero locking range");
}
#[test]
fn test_injection_locking_range_increases_with_injection() {
let stno = py_stno();
let q = 1000.0;
let lw_low = stno.injection_locking_range(q, 0.1);
let lw_high = stno.injection_locking_range(q, 0.9);
assert!(
lw_high > lw_low,
"locking range should increase with injection strength"
);
}
#[test]
fn test_oscillation_frequency_auto_oscillation() {
let stno = py_stno();
let ms = stno.solver.material.ms;
let j_th = stno.threshold_current_density();
let m0 = Vector3::new(0.1, 0.0, 1.0).normalize() * ms;
let dt = 2.0e-13;
let trajectory = stno.simulate(m0, dt, 2000, 3.0 * j_th);
let freq_opt = stno.oscillation_frequency(&trajectory, dt);
if let Some(f) = freq_opt {
assert!(
f > 1.0e6,
"frequency {f:.2e} Hz seems too low for a Py STNO"
);
assert!(f < 1.0e12, "frequency {f:.2e} Hz is unrealistically high");
}
}
#[test]
fn test_oscillation_frequency_no_oscillation() {
let stno = py_stno();
let ms = stno.solver.material.ms;
let m0 = Vector3::new(0.0, 0.0, ms); let dt = 5.0e-13;
let trajectory = stno.simulate(m0, dt, 100, 0.0);
let freq_opt = stno.oscillation_frequency(&trajectory, dt);
assert!(
freq_opt.is_none(),
"expected None for no oscillation, got {:?}",
freq_opt
);
}
#[test]
fn test_permalloy_nanopillar_construction() {
let stno = py_stno();
assert!(stno.solver.material.ms > 0.0);
assert!(stno.solver.material.alpha > 0.0);
assert!(stno.config.spin_polarization > 0.0);
assert!(stno.config.volume > 0.0);
let p_norm = stno.config.p_hat.magnitude();
assert!(
(p_norm - 1.0).abs() < 1.0e-10,
"p̂ not normalized: |p̂| = {p_norm}"
);
}
}