use crate::constants::{E_CHARGE, GAMMA, HBAR, MU_0};
use crate::effect::sot::SpinOrbitTorque;
use crate::error::{invalid_param, Result};
use crate::material::Ferromagnet;
use crate::vector3::Vector3;
#[derive(Debug, Clone)]
pub struct Lcg {
state: u64,
}
impl Lcg {
pub fn new(seed: u64) -> Self {
Lcg {
state: seed.wrapping_add(1),
}
}
#[inline]
pub fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
#[inline]
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
#[inline]
pub fn next_normal(&mut self) -> f64 {
let u1 = self.next_f64().max(1e-15);
let u2 = self.next_f64();
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
}
fn compute_sot_field(
sot: &SpinOrbitTorque,
m: Vector3<f64>,
j: f64,
j_dir: Vector3<f64>,
ms: f64,
t_fm: f64,
) -> Vector3<f64> {
let z_hat = Vector3::new(0.0, 0.0, 1.0);
let sigma_raw = j_dir.cross(&z_hat);
let sigma_hat = if sigma_raw.magnitude() > 1e-12 {
sigma_raw.normalize()
} else {
Vector3::new(0.0, 1.0, 0.0)
};
let h_amplitude = (sot.theta_sh.abs() * HBAR * j.abs()) / (2.0 * E_CHARGE * MU_0 * ms * t_fm);
let sign = if (j < 0.0) ^ (sot.theta_sh < 0.0) {
-1.0_f64
} else {
1.0_f64
};
let h_dl = m.cross(&sigma_hat) * (sign * h_amplitude);
let h_fl = sigma_hat * (0.1 * h_amplitude);
h_dl + h_fl
}
#[inline]
fn llg_rhs(m: Vector3<f64>, h_eff: Vector3<f64>, gamma: f64, alpha: f64) -> Vector3<f64> {
let factor = gamma / (1.0 + alpha * alpha);
let m_cross_h = m.cross(&h_eff);
let m_cross_m_cross_h = m.cross(&m_cross_h);
(m_cross_h + m_cross_m_cross_h * alpha) * (-factor)
}
#[inline]
fn llg_rk4_step(
m: Vector3<f64>,
h_eff: Vector3<f64>,
gamma: f64,
alpha: f64,
dt: f64,
) -> Vector3<f64> {
let k1 = llg_rhs(m, h_eff, gamma, alpha);
let m2 = m + k1 * (dt / 2.0);
let k2 = llg_rhs(m2, h_eff, gamma, alpha);
let m3 = m + k2 * (dt / 2.0);
let k3 = llg_rhs(m3, h_eff, gamma, alpha);
let m4 = m + k3 * dt;
let k4 = llg_rhs(m4, h_eff, gamma, alpha);
m + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0)
}
#[derive(Debug, Clone)]
pub struct SotSwitchingConfig {
pub j_max: f64,
pub t_fm: f64,
pub h_bias: f64,
pub dt: f64,
pub max_steps: usize,
pub n_pulse_steps: usize,
pub switch_threshold: f64,
}
impl Default for SotSwitchingConfig {
fn default() -> Self {
Self {
j_max: 2.0e11,
t_fm: 1.5e-9,
h_bias: 5.0e3,
dt: 1.0e-12,
max_steps: 500,
n_pulse_steps: 10,
switch_threshold: -0.8,
}
}
}
impl SotSwitchingConfig {
pub fn validate(&self) -> Result<()> {
if self.j_max <= 0.0 {
return Err(invalid_param("j_max", "must be positive"));
}
if self.t_fm <= 0.0 {
return Err(invalid_param("t_fm", "must be positive"));
}
if self.dt <= 0.0 {
return Err(invalid_param("dt", "must be positive"));
}
if self.max_steps == 0 {
return Err(invalid_param("max_steps", "must be at least 1"));
}
if self.n_pulse_steps == 0 {
return Err(invalid_param("n_pulse_steps", "must be at least 1"));
}
if self.switch_threshold >= 0.0 {
return Err(invalid_param(
"switch_threshold",
"must be negative for +z → −z switching",
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SotSwitchingEnv {
pub config: SotSwitchingConfig,
pub material: Ferromagnet,
pub sot: SpinOrbitTorque,
pub h_ext: Vector3<f64>,
pub m: Vector3<f64>,
pub step_count: usize,
pub total_reward: f64,
}
impl SotSwitchingEnv {
pub fn new(config: SotSwitchingConfig, material: Ferromagnet, sot: SpinOrbitTorque) -> Self {
let h_bias_x = config.h_bias;
Self {
config,
material,
sot,
h_ext: Vector3::new(h_bias_x, 0.0, 0.0),
m: Vector3::new(0.0, 0.0, 1.0),
step_count: 0,
total_reward: 0.0,
}
}
pub fn default_cofeb_pt() -> Self {
let config = SotSwitchingConfig::default();
let material = Ferromagnet {
alpha: 0.01,
ms: 1.4e6,
anisotropy_k: 1.0e6,
easy_axis: Vector3::new(0.0, 0.0, 1.0),
exchange_a: 2.0e-11,
};
let sot = SpinOrbitTorque::platinum_cofeb();
Self::new(config, material, sot)
}
pub fn reset(&mut self) -> Vector3<f64> {
self.m = Vector3::new(0.01, 0.0, 1.0).normalize();
self.step_count = 0;
self.total_reward = 0.0;
self.m
}
pub fn effective_field(&self, m: Vector3<f64>, j_amplitude: f64) -> Vector3<f64> {
let ms = self.material.ms;
let k = self.material.anisotropy_k;
let h_ani_z = (2.0 * k) / (MU_0 * ms) * m.z;
let h_anisotropy = Vector3::new(0.0, 0.0, h_ani_z);
let j_dir = Vector3::new(1.0, 0.0, 0.0);
let h_sot = compute_sot_field(&self.sot, m, j_amplitude, j_dir, ms, self.config.t_fm);
self.h_ext + h_anisotropy + h_sot
}
pub fn step(&mut self, j_amplitude: f64) -> (Vector3<f64>, f64, bool) {
let h_eff = self.effective_field(self.m, j_amplitude);
let gamma = GAMMA;
let alpha = self.material.alpha;
let dt = self.config.dt;
let m_new = llg_rk4_step(self.m, h_eff, gamma, alpha, dt);
let mag = m_new.magnitude();
self.m = if mag > 1e-15 {
m_new * (1.0 / mag)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
let j_norm = j_amplitude / self.config.j_max;
let energy_penalty = -0.001 * j_norm * j_norm;
let time_penalty = -0.005;
let switched = self.m.z < self.config.switch_threshold;
let switch_reward = if switched { 10.0 } else { 0.0 };
let reward = switch_reward + energy_penalty + time_penalty;
self.step_count += 1;
self.total_reward += reward;
let done = switched || self.step_count >= self.config.max_steps;
(self.m, reward, done)
}
}
#[derive(Debug, Clone)]
pub struct CemPolicy {
pub n_pulse_steps: usize,
pub mu: Vec<f64>,
pub sigma: Vec<f64>,
pub j_max: f64,
}
impl CemPolicy {
pub fn new(n_pulse_steps: usize, j_max: f64) -> Self {
let sigma_init = j_max / 2.0;
Self {
n_pulse_steps,
mu: vec![0.0; n_pulse_steps],
sigma: vec![sigma_init; n_pulse_steps],
j_max,
}
}
pub fn sample_policy(&self, rng: &mut Lcg) -> Vec<f64> {
self.mu
.iter()
.zip(self.sigma.iter())
.map(|(&mu_i, &sigma_i)| {
let sample = mu_i + sigma_i * rng.next_normal();
sample.clamp(-self.j_max, self.j_max)
})
.collect()
}
pub fn update_from_elite(&mut self, elite_policies: &[Vec<f64>]) {
let n = elite_policies.len();
if n == 0 {
return;
}
let n_f = n as f64;
let sigma_floor = self.j_max * 0.01;
for i in 0..self.n_pulse_steps {
let mean = elite_policies.iter().map(|p| p[i]).sum::<f64>() / n_f;
let var = elite_policies
.iter()
.map(|p| {
let diff = p[i] - mean;
diff * diff
})
.sum::<f64>()
/ n_f;
self.mu[i] = mean;
self.sigma[i] = var.sqrt().max(sigma_floor);
}
}
pub fn best_policy(&self) -> Vec<f64> {
self.mu.clone()
}
}
#[derive(Debug, Clone)]
pub struct SotRlResult {
pub best_reward: f64,
pub mean_rewards_per_gen: Vec<f64>,
pub best_rewards_per_gen: Vec<f64>,
pub best_policy: Vec<f64>,
pub switching_achieved: bool,
pub n_generations: usize,
}
#[derive(Debug, Clone)]
pub struct SotRlOptimizer {
pub env: SotSwitchingEnv,
pub policy: CemPolicy,
pub population_size: usize,
pub elite_fraction: f64,
}
impl SotRlOptimizer {
pub fn new(env: SotSwitchingEnv) -> Self {
let n_pulse = env.config.n_pulse_steps;
let j_max = env.config.j_max;
let policy = CemPolicy::new(n_pulse, j_max);
Self {
env,
policy,
population_size: 50,
elite_fraction: 0.2,
}
}
pub fn evaluate_policy(&mut self, pulse_sequence: &[f64]) -> f64 {
self.env.reset();
let mut accumulated = 0.0_f64;
for &j in pulse_sequence {
let (_m, reward, done) = self.env.step(j);
accumulated += reward;
if done {
break;
}
}
accumulated
}
pub fn train(&mut self, n_generations: usize, seed: u64) -> SotRlResult {
let mut rng = Lcg::new(seed);
let elite_count =
((self.population_size as f64 * self.elite_fraction).ceil() as usize).max(1);
let mut mean_rewards_per_gen = Vec::with_capacity(n_generations);
let mut best_rewards_per_gen = Vec::with_capacity(n_generations);
let mut overall_best_reward = f64::NEG_INFINITY;
let mut overall_best_policy = self.policy.best_policy();
let mut switching_achieved = false;
for _gen in 0..n_generations {
let mut population: Vec<(f64, Vec<f64>)> = (0..self.population_size)
.map(|_| {
let p = self.policy.sample_policy(&mut rng);
let r = self.evaluate_policy(&p);
(r, p)
})
.collect();
population.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let gen_best = population[0].0;
let gen_mean =
population.iter().map(|(r, _)| *r).sum::<f64>() / self.population_size as f64;
mean_rewards_per_gen.push(gen_mean);
best_rewards_per_gen.push(gen_best);
if gen_best > overall_best_reward {
overall_best_reward = gen_best;
overall_best_policy = population[0].1.clone();
}
if !switching_achieved && gen_best >= 10.0 - 0.005 * self.env.config.max_steps as f64 {
switching_achieved = gen_best > 0.0;
}
let elite_policies: Vec<Vec<f64>> = population
.iter()
.take(elite_count)
.map(|(_, p)| p.clone())
.collect();
self.policy.update_from_elite(&elite_policies);
}
let greedy = self.policy.best_policy();
let greedy_reward = self.evaluate_policy(&greedy);
if greedy_reward > overall_best_reward {
overall_best_reward = greedy_reward;
overall_best_policy = greedy;
}
if !switching_achieved && overall_best_reward > 0.0 {
switching_achieved = true;
}
SotRlResult {
best_reward: overall_best_reward,
mean_rewards_per_gen,
best_rewards_per_gen,
best_policy: overall_best_policy,
switching_achieved,
n_generations,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_env() -> SotSwitchingEnv {
SotSwitchingEnv::default_cofeb_pt()
}
#[test]
fn test_env_reset() {
let mut env = make_env();
let m = env.reset();
assert!(
(m.z - 1.0).abs() < 0.01,
"m_z = {:.6} after reset, expected ≈ 1.0",
m.z
);
assert_eq!(env.step_count, 0);
assert!((env.total_reward).abs() < 1e-15);
}
#[test]
fn test_step_changes_state() {
let mut env = make_env();
let m0 = env.reset();
let j_max = env.config.j_max;
let (m1, _reward, _done) = env.step(j_max);
let delta = (m1.x - m0.x).abs() + (m1.y - m0.y).abs() + (m1.z - m0.z).abs();
assert!(
delta > 1e-15,
"Magnetization did not change after step with j = j_max"
);
}
#[test]
fn test_norm_preserved() {
let mut env = make_env();
env.reset();
let j_max = env.config.j_max;
for i in 0..100 {
let j = if i % 2 == 0 { j_max } else { -j_max };
let (m, _r, done) = env.step(j);
let norm = m.magnitude();
assert!(
(norm - 1.0).abs() < 1e-3,
"step {}: |m| = {:.8} deviates from unity",
i,
norm
);
if done {
break;
}
}
}
#[test]
fn test_switching_with_large_current() {
let mut env = make_env();
env.reset();
let initial_mz = env.m.z;
let j = env.config.j_max;
let max_steps = env.config.max_steps;
let mut final_mz = initial_mz;
for _ in 0..max_steps {
let (m, _r, done) = env.step(j);
final_mz = m.z;
if done {
break;
}
}
assert!(
final_mz < initial_mz,
"m_z did not decrease: initial = {:.4}, final = {:.4}",
initial_mz,
final_mz
);
}
#[test]
fn test_cem_policy_converges() {
let env = make_env();
let mut opt = SotRlOptimizer::new(env);
opt.population_size = 10;
let result = opt.train(5, 12345);
assert_eq!(result.n_generations, 5);
assert_eq!(result.best_rewards_per_gen.len(), 5);
let first = result.best_rewards_per_gen[0];
let last = *result.best_rewards_per_gen.last().unwrap_or(&first);
assert!(
result.best_reward >= first,
"best_reward ({:.4}) should be >= first gen best ({:.4})",
result.best_reward,
first
);
let _ = last;
}
#[test]
fn test_cem_update_reduces_sigma_toward_elite() {
let env = make_env();
let j_max = env.config.j_max;
let n = env.config.n_pulse_steps;
let mut policy = CemPolicy::new(n, j_max);
let initial_sigma_sum: f64 = policy.sigma.iter().sum();
let elite: Vec<Vec<f64>> = (0..10).map(|_| vec![j_max * 0.5; n]).collect();
policy.update_from_elite(&elite);
let updated_sigma_sum: f64 = policy.sigma.iter().sum();
assert!(
updated_sigma_sum < initial_sigma_sum,
"sigma did not decrease: before = {:.4}, after = {:.4}",
initial_sigma_sum,
updated_sigma_sum
);
let sigma_floor = j_max * 0.01;
for &s in &policy.sigma {
assert!(
(s - sigma_floor).abs() < 1e-15,
"sigma = {:.6e}, expected floor = {:.6e}",
s,
sigma_floor
);
}
}
#[test]
fn test_reward_structure() {
let mut env = make_env();
env.reset();
let (_m, reward, _done) = env.step(0.0);
assert!(
reward < 0.0,
"reward with j=0 should be negative (time penalty), got {:.6}",
reward
);
}
#[test]
fn test_lcg_deterministic() {
let mut rng1 = Lcg::new(42);
let mut rng2 = Lcg::new(42);
for _ in 0..100 {
assert_eq!(rng1.next_u64(), rng2.next_u64());
}
}
#[test]
fn test_lcg_normal_range() {
let mut rng = Lcg::new(99);
let samples: Vec<f64> = (0..1000).map(|_| rng.next_normal()).collect();
let mean = samples.iter().sum::<f64>() / 1000.0;
assert!(mean.abs() < 0.2, "LCG normal mean = {:.4}", mean);
let var = samples.iter().map(|x| x * x).sum::<f64>() / 1000.0;
assert!(
(var.sqrt() - 1.0).abs() < 0.2,
"LCG normal std = {:.4}",
var.sqrt()
);
}
#[test]
fn test_effective_field_no_current() {
let mut env = make_env();
env.reset();
let h = env.effective_field(env.m, 0.0);
assert!(
h.z > 0.0,
"With PMA and m ≈ ẑ, H_eff.z should be positive (got {:.4e})",
h.z
);
}
}