use std::f64::consts::PI;
use crate::error::{invalid_param, numerical_error, Result};
use crate::texture::skyrmion::SkyrmionLattice;
use crate::texture::topology::calculate_skyrmion_number;
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),
}
}
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
}
pub fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
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 * PI * u2).cos()
}
}
#[derive(Debug, Clone)]
pub struct NoiseSchedule {
pub t_max: usize,
pub beta: Vec<f64>,
pub alpha: Vec<f64>,
pub alpha_bar: Vec<f64>,
}
impl NoiseSchedule {
pub fn linear(t_max: usize, beta_start: f64, beta_end: f64) -> Result<Self> {
if t_max == 0 {
return Err(invalid_param("t_max", "must be at least 1"));
}
if beta_start <= 0.0 || beta_start >= 1.0 {
return Err(invalid_param("beta_start", "must be in (0, 1)"));
}
if beta_end <= 0.0 || beta_end >= 1.0 {
return Err(invalid_param("beta_end", "must be in (0, 1)"));
}
if beta_start >= beta_end {
return Err(invalid_param("beta_start", "must be less than beta_end"));
}
let mut beta = Vec::with_capacity(t_max);
let mut alpha = Vec::with_capacity(t_max);
let mut alpha_bar = Vec::with_capacity(t_max);
let mut cumulative_alpha = 1.0_f64;
for i in 0..t_max {
let bt = if t_max == 1 {
beta_start
} else {
beta_start + (beta_end - beta_start) * (i as f64) / ((t_max - 1) as f64)
};
let at = 1.0 - bt;
cumulative_alpha *= at;
beta.push(bt);
alpha.push(at);
alpha_bar.push(cumulative_alpha);
}
Ok(Self {
t_max,
beta,
alpha,
alpha_bar,
})
}
pub fn beta_t(&self, t: usize) -> f64 {
debug_assert!(t >= 1 && t <= self.t_max, "t out of range [1, T]");
self.beta[t - 1]
}
pub fn alpha_bar_t(&self, t: usize) -> f64 {
debug_assert!(t >= 1 && t <= self.t_max, "t out of range [1, T]");
self.alpha_bar[t - 1]
}
}
#[derive(Debug, Clone)]
pub struct SpinTexture {
pub data: Vec<f64>,
pub nx: usize,
pub ny: usize,
}
impl SpinTexture {
pub fn from_skyrmion_lattice(
lattice: &SkyrmionLattice,
nx: usize,
ny: usize,
domain_size: f64,
wall_width: f64,
) -> Result<Self> {
if nx == 0 {
return Err(invalid_param("nx", "must be at least 1"));
}
if ny == 0 {
return Err(invalid_param("ny", "must be at least 1"));
}
let n_comp = 3 * nx * ny;
let mut data = vec![0.0_f64; n_comp];
let dx_step = if nx > 1 {
domain_size / (nx - 1) as f64
} else {
0.0
};
let dy_step = if ny > 1 {
domain_size / (ny - 1) as f64
} else {
0.0
};
for j in 0..ny {
for i in 0..nx {
let x = i as f64 * dx_step;
let y = j as f64 * dy_step;
let mut m = Vector3::new(0.0_f64, 0.0, 1.0);
if !lattice.skyrmions.is_empty() {
let mut best_dist = f64::INFINITY;
let mut best_idx = 0_usize;
for (k, sk) in lattice.skyrmions.iter().enumerate() {
let ddx = x - sk.center.0;
let ddy = y - sk.center.1;
let dist = (ddx * ddx + ddy * ddy).sqrt();
if dist < best_dist {
best_dist = dist;
best_idx = k;
}
}
m = lattice.skyrmions[best_idx].magnetization_at(x, y, wall_width);
}
let mag = (m.x * m.x + m.y * m.y + m.z * m.z).sqrt();
let (mx, my, mz) = if mag > 1e-15 {
(m.x / mag, m.y / mag, m.z / mag)
} else {
(0.0, 0.0, 1.0)
};
let base = (j * nx + i) * 3;
data[base] = mx;
data[base + 1] = my;
data[base + 2] = mz;
}
}
Ok(Self { data, nx, ny })
}
pub fn from_uniform(nx: usize, ny: usize, direction: Vector3<f64>) -> Result<Self> {
if nx == 0 {
return Err(invalid_param("nx", "must be at least 1"));
}
if ny == 0 {
return Err(invalid_param("ny", "must be at least 1"));
}
let mag =
(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z)
.sqrt();
if mag < 1e-15 {
return Err(invalid_param("direction", "must be a non-zero vector"));
}
let (dx, dy, dz) = (direction.x / mag, direction.y / mag, direction.z / mag);
let n_comp = 3 * nx * ny;
let mut data = vec![0.0_f64; n_comp];
for k in 0..nx * ny {
data[k * 3] = dx;
data[k * 3 + 1] = dy;
data[k * 3 + 2] = dz;
}
Ok(Self { data, nx, ny })
}
pub fn n_sites(&self) -> usize {
self.nx * self.ny
}
pub fn n_components(&self) -> usize {
3 * self.nx * self.ny
}
pub fn get_spin(&self, i: usize, j: usize) -> Vector3<f64> {
let base = (j * self.nx + i) * 3;
Vector3::new(self.data[base], self.data[base + 1], self.data[base + 2])
}
pub fn add_noise(&self, alpha_bar: f64, rng: &mut Lcg) -> (Self, Self) {
let n = self.n_components();
let sqrt_ab = alpha_bar.max(0.0).sqrt();
let sqrt_one_minus_ab = (1.0 - alpha_bar).max(0.0).sqrt();
let mut x_t_data = Vec::with_capacity(n);
let mut eps_data = Vec::with_capacity(n);
for &v in &self.data {
let eps = rng.next_normal();
x_t_data.push(sqrt_ab * v + sqrt_one_minus_ab * eps);
eps_data.push(eps);
}
(
Self {
data: x_t_data,
nx: self.nx,
ny: self.ny,
},
Self {
data: eps_data,
nx: self.nx,
ny: self.ny,
},
)
}
pub fn normalize_spins(&mut self) {
let n_sites = self.nx * self.ny;
for k in 0..n_sites {
let base = k * 3;
let mx = self.data[base];
let my = self.data[base + 1];
let mz = self.data[base + 2];
let mag = (mx * mx + my * my + mz * mz).sqrt();
if mag > 1e-15 {
self.data[base] = mx / mag;
self.data[base + 1] = my / mag;
self.data[base + 2] = mz / mag;
} else {
self.data[base] = 0.0;
self.data[base + 1] = 0.0;
self.data[base + 2] = 1.0;
}
}
}
}
#[derive(Debug, Clone)]
struct TwoLayerMlp {
in_dim: usize,
hidden_dim: usize,
out_dim: usize,
}
impl TwoLayerMlp {
fn n_params(&self) -> usize {
let (d_in, h, d_out) = (self.in_dim, self.hidden_dim, self.out_dim);
h * d_in + h + d_out * h + d_out
}
fn offsets(&self) -> (usize, usize, usize, usize) {
let (d_in, h, d_out) = (self.in_dim, self.hidden_dim, self.out_dim);
let w1_start = 0;
let b1_start = w1_start + h * d_in;
let w2_start = b1_start + h;
let b2_start = w2_start + d_out * h;
let _ = d_out; (w1_start, b1_start, w2_start, b2_start)
}
fn init_params(&self, rng: &mut Lcg) -> Vec<f64> {
let (d_in, h, d_out) = (self.in_dim, self.hidden_dim, self.out_dim);
let mut params = vec![0.0_f64; self.n_params()];
let (w1_start, _b1_start, w2_start, _b2_start) = self.offsets();
let a1 = (6.0 / (d_in + h) as f64).sqrt();
for k in 0..h * d_in {
params[w1_start + k] = (rng.next_f64() * 2.0 - 1.0) * a1;
}
let a2 = (6.0 / (h + d_out) as f64).sqrt();
for k in 0..d_out * h {
params[w2_start + k] = (rng.next_f64() * 2.0 - 1.0) * a2;
}
params
}
fn forward(&self, input: &[f64], params: &[f64]) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
let (d_in, h, d_out) = (self.in_dim, self.hidden_dim, self.out_dim);
let (w1_start, b1_start, w2_start, b2_start) = self.offsets();
let mut pre1 = vec![0.0_f64; h];
for j in 0..h {
let mut sum = params[b1_start + j];
for i in 0..d_in {
sum += params[w1_start + j * d_in + i] * input[i];
}
pre1[j] = sum;
}
let hidden: Vec<f64> = pre1.iter().map(|&v| v.tanh()).collect();
let mut output = vec![0.0_f64; d_out];
for k in 0..d_out {
let mut sum = params[b2_start + k];
for j in 0..h {
sum += params[w2_start + k * h + j] * hidden[j];
}
output[k] = sum;
}
(pre1, hidden, output)
}
fn backward(
&self,
input: &[f64],
pre1: &[f64],
hidden: &[f64],
dl_doutput: &[f64],
params: &[f64],
) -> Vec<f64> {
let (d_in, h, d_out) = (self.in_dim, self.hidden_dim, self.out_dim);
let (w1_start, b1_start, w2_start, b2_start) = self.offsets();
let mut grads = vec![0.0_f64; params.len()];
for k in 0..d_out {
grads[b2_start + k] = dl_doutput[k];
for j in 0..h {
grads[w2_start + k * h + j] = dl_doutput[k] * hidden[j];
}
}
let mut dl_dhidden = vec![0.0_f64; h];
for j in 0..h {
let mut sum = 0.0;
for k in 0..d_out {
sum += dl_doutput[k] * params[w2_start + k * h + j];
}
dl_dhidden[j] = sum;
}
let mut dl_dpre1 = vec![0.0_f64; h];
for j in 0..h {
let th = pre1[j].tanh();
dl_dpre1[j] = dl_dhidden[j] * (1.0 - th * th);
}
for j in 0..h {
grads[b1_start + j] = dl_dpre1[j];
for i in 0..d_in {
grads[w1_start + j * d_in + i] = dl_dpre1[j] * input[i];
}
}
grads
}
}
#[derive(Debug, Clone)]
pub struct DiffusionModel {
pub schedule: NoiseSchedule,
pub params: Vec<f64>,
pub n_components: usize,
mlp: TwoLayerMlp,
}
impl DiffusionModel {
pub fn new(
nx: usize,
ny: usize,
hidden_dim: usize,
_depth: usize,
schedule: NoiseSchedule,
) -> Result<Self> {
if nx == 0 {
return Err(invalid_param("nx", "must be at least 1"));
}
if ny == 0 {
return Err(invalid_param("ny", "must be at least 1"));
}
if hidden_dim == 0 {
return Err(invalid_param("hidden_dim", "must be at least 1"));
}
let n_components = 3 * nx * ny;
let mlp = TwoLayerMlp {
in_dim: n_components + 1,
hidden_dim,
out_dim: n_components,
};
let mut rng = Lcg::new(42);
let params = mlp.init_params(&mut rng);
Ok(Self {
schedule,
params,
n_components,
mlp,
})
}
pub fn predict_noise(&self, x_t: &SpinTexture, t: usize) -> Result<Vec<f64>> {
if t == 0 || t > self.schedule.t_max {
return Err(invalid_param("t", "must be in [1, T]"));
}
if x_t.n_components() != self.n_components {
return Err(invalid_param(
"x_t",
"texture dimension does not match model",
));
}
let t_norm = t as f64 / self.schedule.t_max as f64;
let mut input = x_t.data.clone();
input.push(t_norm);
let (_pre1, _hidden, output) = self.mlp.forward(&input, &self.params);
Ok(output)
}
pub fn training_step(&self, x_0: &SpinTexture, t: usize, seed: u64) -> Result<(f64, Vec<f64>)> {
if t == 0 || t > self.schedule.t_max {
return Err(invalid_param("t", "must be in [1, T]"));
}
if x_0.n_components() != self.n_components {
return Err(invalid_param(
"x_0",
"texture dimension does not match model",
));
}
let n = self.n_components;
let alpha_bar = self.schedule.alpha_bar_t(t);
let t_norm = t as f64 / self.schedule.t_max as f64;
let mut rng = Lcg::new(seed);
let (x_t, epsilon) = x_0.add_noise(alpha_bar, &mut rng);
let mut input = x_t.data.clone();
input.push(t_norm);
let (pre1, hidden, output) = self.mlp.forward(&input, &self.params);
let mut loss = 0.0_f64;
let mut dl_doutput = vec![0.0_f64; n];
for i in 0..n {
let diff = output[i] - epsilon.data[i];
loss += diff * diff;
dl_doutput[i] = 2.0 * diff / n as f64;
}
loss /= n as f64;
if !loss.is_finite() {
return Err(numerical_error("loss is not finite in training_step"));
}
let grads = self
.mlp
.backward(&input, &pre1, &hidden, &dl_doutput, &self.params);
Ok((loss, grads))
}
pub fn train(
&mut self,
textures: &[SpinTexture],
n_epochs: usize,
lr: f64,
) -> Result<Vec<f64>> {
if textures.is_empty() {
return Err(invalid_param("textures", "must have at least one texture"));
}
for (idx, tex) in textures.iter().enumerate() {
if tex.n_components() != self.n_components {
return Err(invalid_param(
"textures",
&format!("texture {idx} has wrong dimension"),
));
}
}
let n_params = self.params.len();
let beta1 = 0.9_f64;
let beta2 = 0.999_f64;
let eps_adam = 1e-8_f64;
let mut m_adam = vec![0.0_f64; n_params];
let mut v_adam = vec![0.0_f64; n_params];
let mut t_adam = 0_usize;
let mut loss_curve = Vec::with_capacity(n_epochs);
for epoch in 0..n_epochs {
let mut rng = Lcg::new(epoch as u64 * 31_337 + 7);
let tex_idx = (rng.next_u64() as usize) % textures.len();
let x_0 = &textures[tex_idx];
let t = 1 + (rng.next_u64() as usize) % self.schedule.t_max;
let noise_seed = epoch as u64 * 999_983 + 13;
let (loss, grads) = self.training_step(x_0, t, noise_seed)?;
t_adam += 1;
let t_f = t_adam as f64;
let bc1 = 1.0 - beta1.powf(t_f);
let bc2 = 1.0 - beta2.powf(t_f);
for i in 0..n_params {
m_adam[i] = beta1 * m_adam[i] + (1.0 - beta1) * grads[i];
v_adam[i] = beta2 * v_adam[i] + (1.0 - beta2) * grads[i] * grads[i];
let m_hat = m_adam[i] / bc1;
let v_hat = v_adam[i] / bc2;
self.params[i] -= lr * m_hat / (v_hat.sqrt() + eps_adam);
}
loss_curve.push(loss);
}
Ok(loss_curve)
}
pub fn sample(&self, nx: usize, ny: usize, seed: u64) -> Result<SpinTexture> {
if 3 * nx * ny != self.n_components {
return Err(invalid_param(
"nx/ny",
"resulting texture dimension does not match model",
));
}
let n = self.n_components;
let t_max = self.schedule.t_max;
let mut rng = Lcg::new(seed);
let mut x_data: Vec<f64> = (0..n).map(|_| rng.next_normal()).collect();
for t in (1..=t_max).rev() {
let alpha_bar_t = self.schedule.alpha_bar_t(t);
let t_norm = t as f64 / t_max as f64;
let mut input = x_data.clone();
input.push(t_norm);
let (_pre1, _hidden, eps_pred) = self.mlp.forward(&input, &self.params);
let sqrt_ab = alpha_bar_t.max(1e-20).sqrt();
let sqrt_one_minus_ab = (1.0 - alpha_bar_t).max(0.0).sqrt();
let mut x0_pred: Vec<f64> = x_data
.iter()
.zip(eps_pred.iter())
.map(|(&xt, &ep)| (xt - sqrt_one_minus_ab * ep) / sqrt_ab)
.collect();
for v in &mut x0_pred {
*v = v.clamp(-1.0, 1.0);
}
if t > 1 {
let alpha_bar_prev = self.schedule.alpha_bar_t(t - 1);
let sqrt_ab_prev = alpha_bar_prev.max(0.0).sqrt();
let sqrt_one_minus_prev = (1.0 - alpha_bar_prev).max(0.0).sqrt();
x_data = (0..n)
.map(|i| {
let noise = rng.next_normal();
sqrt_ab_prev * x0_pred[i] + sqrt_one_minus_prev * noise
})
.collect();
} else {
x_data = x0_pred;
}
let any_bad = x_data.iter().any(|v| !v.is_finite());
if any_bad {
return Err(numerical_error("non-finite value in reverse diffusion"));
}
}
let mut texture = SpinTexture {
data: x_data,
nx,
ny,
};
texture.normalize_spins();
Ok(texture)
}
pub fn topological_charge(&self, texture: &SpinTexture, dx: f64, dy: f64) -> f64 {
let nx = texture.nx;
let ny = texture.ny;
let mag: Vec<Vec<Vector3<f64>>> = (0..nx)
.map(|i| (0..ny).map(|j| texture.get_spin(i, j)).collect())
.collect();
if nx < 3 || ny < 3 {
return 0.0;
}
calculate_skyrmion_number(&mag, dx, dy)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::texture::skyrmion::{Chirality, Helicity, SkyrmionLattice};
fn default_schedule() -> NoiseSchedule {
NoiseSchedule::linear(100, 1e-4, 0.02).expect("valid schedule params")
}
fn small_model(nx: usize, ny: usize) -> DiffusionModel {
let schedule = default_schedule();
DiffusionModel::new(nx, ny, 16, 2, schedule).expect("valid model params")
}
fn uniform_texture(nx: usize, ny: usize) -> SpinTexture {
SpinTexture::from_uniform(nx, ny, Vector3::new(0.0, 0.0, 1.0))
.expect("valid uniform texture")
}
#[test]
fn test_noise_schedule_alpha_bar_decreasing() {
let sched = default_schedule();
assert_eq!(sched.alpha_bar.len(), 100);
let ab1 = sched.alpha_bar_t(1);
assert!(ab1 > 0.99, "ᾱ_1 should be close to 1, got {ab1}");
let ab_t = sched.alpha_bar_t(100);
assert!(
ab_t < 0.5,
"ᾱ_T should be substantially less than 1, got {ab_t}"
);
for t in 2..=100 {
let prev = sched.alpha_bar_t(t - 1);
let curr = sched.alpha_bar_t(t);
assert!(
curr <= prev,
"ᾱ should be non-increasing: ᾱ_{t} = {curr} > ᾱ_{} = {prev}",
t - 1
);
}
}
#[test]
fn test_noise_schedule_invalid_params() {
assert!(NoiseSchedule::linear(0, 1e-4, 0.02).is_err());
assert!(NoiseSchedule::linear(10, 0.0, 0.02).is_err());
assert!(NoiseSchedule::linear(10, 1e-4, 1.0).is_err());
assert!(NoiseSchedule::linear(10, 0.02, 1e-4).is_err());
}
#[test]
fn test_add_noise_preserves_shape() {
let nx = 4;
let ny = 4;
let x_0 = uniform_texture(nx, ny);
let n = x_0.data.len();
let mut rng = Lcg::new(1234);
let ab = 0.5;
let (x_t, eps) = x_0.add_noise(ab, &mut rng);
assert_eq!(x_t.data.len(), n, "x_t must have same length as x_0");
assert_eq!(eps.data.len(), n, "epsilon must have same length as x_0");
assert_eq!(x_t.nx, nx);
assert_eq!(x_t.ny, ny);
}
#[test]
fn test_spin_texture_from_uniform() {
let tex =
SpinTexture::from_uniform(3, 3, Vector3::new(0.0, 0.0, 1.0)).expect("uniform texture");
assert_eq!(tex.n_sites(), 9);
assert_eq!(tex.n_components(), 27);
for k in 0..9 {
let base = k * 3;
let (mx, my, mz) = (tex.data[base], tex.data[base + 1], tex.data[base + 2]);
let mag = (mx * mx + my * my + mz * mz).sqrt();
assert!((mag - 1.0).abs() < 1e-12, "spin must be unit length");
}
}
#[test]
fn test_normalize_spins() {
let mut tex =
SpinTexture::from_uniform(2, 2, Vector3::new(1.0, 0.0, 0.0)).expect("uniform texture");
tex.data[0] = 5.0;
tex.data[1] = 0.0;
tex.data[2] = 0.0;
tex.normalize_spins();
let mag = (tex.data[0] * tex.data[0]).sqrt();
assert!((mag - 1.0).abs() < 1e-12);
}
#[test]
fn test_get_spin_layout() {
let nx = 3;
let ny = 2;
let mut tex = SpinTexture {
data: vec![0.0; 3 * nx * ny],
nx,
ny,
};
let base = 3_usize;
tex.data[base] = 1.0;
tex.data[base + 1] = 0.0;
tex.data[base + 2] = 0.0;
let spin = tex.get_spin(1, 0);
assert!((spin.x - 1.0).abs() < 1e-14);
assert!(spin.y.abs() < 1e-14);
assert!(spin.z.abs() < 1e-14);
}
#[test]
fn test_forward_pass_shape() {
let nx = 4;
let ny = 4;
let model = small_model(nx, ny);
let tex = uniform_texture(nx, ny);
let eps_pred = model.predict_noise(&tex, 50).expect("predict_noise");
assert_eq!(
eps_pred.len(),
model.n_components,
"output length must equal n_components"
);
for v in &eps_pred {
assert!(v.is_finite(), "all predictions must be finite");
}
}
#[test]
fn test_predict_noise_invalid_t() {
let model = small_model(2, 2);
let tex = uniform_texture(2, 2);
assert!(model.predict_noise(&tex, 0).is_err());
assert!(model.predict_noise(&tex, 101).is_err());
assert!(model.predict_noise(&tex, 100).is_ok());
}
#[test]
fn test_training_reduces_loss() {
let nx = 2;
let ny = 2;
let schedule = NoiseSchedule::linear(20, 1e-4, 0.02).expect("schedule");
let mut model = DiffusionModel::new(nx, ny, 8, 2, schedule).expect("model");
let tex = uniform_texture(nx, ny);
let losses = model
.train(&[tex], 30, 1e-3)
.expect("training should succeed");
assert_eq!(losses.len(), 30, "should have one loss per epoch");
let first_loss = losses[0];
let last_loss = losses[losses.len() - 1];
assert!(
last_loss <= first_loss * 10.0,
"loss should not explode: first={first_loss:.4e}, last={last_loss:.4e}"
);
}
#[test]
fn test_training_step_returns_finite() {
let model = small_model(2, 2);
let tex = uniform_texture(2, 2);
let (loss, grads) = model.training_step(&tex, 5, 42).expect("training step");
assert!(loss.is_finite(), "loss must be finite");
assert_eq!(grads.len(), model.params.len());
for g in &grads {
assert!(g.is_finite(), "all grads must be finite");
}
}
#[test]
fn test_sample_returns_normalized() {
let nx = 4;
let ny = 4;
let model = small_model(nx, ny);
let result = model.sample(nx, ny, 7).expect("sample should succeed");
assert_eq!(result.n_components(), 3 * nx * ny);
for k in 0..result.n_sites() {
let base = k * 3;
let mx = result.data[base];
let my = result.data[base + 1];
let mz = result.data[base + 2];
let mag = (mx * mx + my * my + mz * mz).sqrt();
assert!(
(mag - 1.0).abs() < 0.1,
"spin {k} has |m| = {mag:.4}, expected ≈ 1"
);
}
}
#[test]
fn test_sample_wrong_dimension_errors() {
let model = small_model(2, 2);
assert!(model.sample(3, 3, 0).is_err());
}
#[test]
fn test_topological_charge_skyrmion() {
let nx = 32;
let ny = 32;
let domain_size = 200.0e-9; let radius = 40.0e-9;
let wall_width = 10.0e-9;
let lattice = SkyrmionLattice::square(
1,
1,
domain_size, radius,
Helicity::Neel,
Chirality::CounterClockwise,
);
let mut custom_lattice = lattice;
custom_lattice.skyrmions[0].center = (domain_size / 2.0, domain_size / 2.0);
let tex =
SpinTexture::from_skyrmion_lattice(&custom_lattice, nx, ny, domain_size, wall_width)
.expect("skyrmion lattice texture");
let schedule = default_schedule();
let model = DiffusionModel::new(nx, ny, 8, 2, schedule).expect("model");
let dx = domain_size / (nx - 1) as f64;
let dy = domain_size / (ny - 1) as f64;
let q = model.topological_charge(&tex, dx, dy);
assert!(
q.abs() > 0.3,
"topological charge should be non-trivial, got Q = {q:.4}"
);
}
#[test]
fn test_lcg_reproducibility() {
let mut rng1 = Lcg::new(12345);
let mut rng2 = Lcg::new(12345);
for _ in 0..100 {
assert_eq!(rng1.next_u64(), rng2.next_u64());
}
}
#[test]
fn test_lcg_normal_finite() {
let mut rng = Lcg::new(99);
for _ in 0..1000 {
let v = rng.next_normal();
assert!(v.is_finite(), "normal sample must be finite");
}
}
#[test]
fn test_two_layer_mlp_forward_backward_shape() {
let mlp = TwoLayerMlp {
in_dim: 5,
hidden_dim: 4,
out_dim: 3,
};
let mut rng = Lcg::new(1);
let params = mlp.init_params(&mut rng);
assert_eq!(params.len(), mlp.n_params());
let input = vec![0.1, -0.2, 0.3, 0.0, 0.5];
let (pre1, hidden, output) = mlp.forward(&input, ¶ms);
assert_eq!(pre1.len(), 4);
assert_eq!(hidden.len(), 4);
assert_eq!(output.len(), 3);
let dl_dout = vec![1.0; 3];
let grads = mlp.backward(&input, &pre1, &hidden, &dl_dout, ¶ms);
assert_eq!(grads.len(), params.len());
for g in &grads {
assert!(g.is_finite());
}
}
#[test]
fn test_two_layer_mlp_gradient_finite_diff() {
let mlp = TwoLayerMlp {
in_dim: 3,
hidden_dim: 4,
out_dim: 3,
};
let mut rng = Lcg::new(7777);
let params = mlp.init_params(&mut rng);
let input = vec![0.5_f64, -0.3, 0.8];
let scalar_loss = |p: &[f64]| -> f64 {
let (_, _, out) = mlp.forward(&input, p);
out.iter().sum()
};
let (pre1, hidden, output) = mlp.forward(&input, ¶ms);
let dl_dout = vec![1.0_f64; output.len()]; let ad_grads = mlp.backward(&input, &pre1, &hidden, &dl_dout, ¶ms);
let delta = 1e-6;
for i in 0..params.len() {
let mut p_plus = params.clone();
let mut p_minus = params.clone();
p_plus[i] += delta;
p_minus[i] -= delta;
let fd_grad = (scalar_loss(&p_plus) - scalar_loss(&p_minus)) / (2.0 * delta);
let ad_grad = ad_grads[i];
let err = (fd_grad - ad_grad).abs();
assert!(
err < 1e-5,
"grad mismatch at param {i}: AD={ad_grad:.6e}, FD={fd_grad:.6e}, err={err:.2e}"
);
}
}
}