use crate::error::{dimension_mismatch, invalid_param, Result};
use crate::vector3::Vector3;
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
let state = if seed == 0 {
0xDEAD_BEEF_CAFE_BABE
} else {
seed
};
Self { state }
}
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
}
fn next_unit(&mut self) -> f64 {
let bits = self.next_u64() >> 11;
(bits as f64) / ((1u64 << 53) as f64)
}
fn next_uniform(&mut self, low: f64, high: f64) -> f64 {
low + (high - low) * self.next_unit()
}
fn next_normal(&mut self) -> f64 {
loop {
let u = 2.0 * self.next_unit() - 1.0;
let v = 2.0 * self.next_unit() - 1.0;
let s = u * u + v * v;
if s > 0.0 && s < 1.0 {
let factor = (-2.0 * s.ln() / s).sqrt();
return u * factor;
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EquivariantConfig {
pub n_scalar_in: usize,
pub n_vector_in: usize,
pub n_scalar_out: usize,
pub n_vector_out: usize,
}
impl EquivariantConfig {
pub const fn new(
n_scalar_in: usize,
n_vector_in: usize,
n_scalar_out: usize,
n_vector_out: usize,
) -> Self {
Self {
n_scalar_in,
n_vector_in,
n_scalar_out,
n_vector_out,
}
}
pub const fn has_paired_sv(&self) -> bool {
self.n_scalar_in == self.n_vector_in && self.n_scalar_in > 0
}
pub fn validate(&self) -> Result<()> {
if self.n_scalar_in == 0 && self.n_vector_in == 0 {
return Err(invalid_param(
"EquivariantConfig",
"must have at least one input channel (scalar or vector)",
));
}
if self.n_scalar_out == 0 && self.n_vector_out == 0 {
return Err(invalid_param(
"EquivariantConfig",
"must have at least one output channel (scalar or vector)",
));
}
Ok(())
}
}
pub struct EquivariantLinear {
pub config: EquivariantConfig,
pub w_ss: Vec<Vec<f64>>,
pub w_vs: Vec<Vec<f64>>,
pub w_sv: Vec<Vec<f64>>,
pub w_vv: Vec<Vec<f64>>,
pub bias_s: Vec<f64>,
}
impl EquivariantLinear {
pub fn new(config: EquivariantConfig, rng_seed: u64) -> Result<Self> {
config.validate()?;
let mut rng = Lcg::new(rng_seed);
let w_ss = if config.n_scalar_in > 0 && config.n_scalar_out > 0 {
let a = (6.0 / (config.n_scalar_in + config.n_scalar_out) as f64).sqrt();
(0..config.n_scalar_out)
.map(|_| {
(0..config.n_scalar_in)
.map(|_| rng.next_uniform(-a, a))
.collect()
})
.collect()
} else {
vec![vec![0.0; config.n_scalar_in]; config.n_scalar_out]
};
let w_vs = if config.n_vector_in > 0 && config.n_scalar_out > 0 {
let std_dev = (2.0 / config.n_vector_in as f64).sqrt();
(0..config.n_scalar_out)
.map(|_| {
(0..config.n_vector_in)
.map(|_| std_dev * rng.next_normal())
.collect()
})
.collect()
} else {
vec![vec![0.0; config.n_vector_in]; config.n_scalar_out]
};
let w_sv = if config.has_paired_sv() && config.n_vector_out > 0 {
let std_dev = (2.0 / config.n_scalar_in as f64).sqrt();
(0..config.n_vector_out)
.map(|_| {
(0..config.n_scalar_in)
.map(|_| std_dev * rng.next_normal())
.collect()
})
.collect()
} else {
vec![vec![0.0; config.n_scalar_in]; config.n_vector_out]
};
let w_vv = if config.n_vector_in > 0 && config.n_vector_out > 0 {
let a = (6.0 / (config.n_vector_in + config.n_vector_out) as f64).sqrt();
(0..config.n_vector_out)
.map(|_| {
(0..config.n_vector_in)
.map(|_| rng.next_uniform(-a, a))
.collect()
})
.collect()
} else {
vec![vec![0.0; config.n_vector_in]; config.n_vector_out]
};
let bias_s = vec![0.0_f64; config.n_scalar_out];
Ok(Self {
config,
w_ss,
w_vs,
w_sv,
w_vv,
bias_s,
})
}
pub fn forward(
&self,
scalars: &[f64],
vectors: &[Vector3<f64>],
) -> Result<(Vec<f64>, Vec<Vector3<f64>>)> {
if scalars.len() != self.config.n_scalar_in {
return Err(dimension_mismatch(
&format!("{} scalar inputs", self.config.n_scalar_in),
&format!("{} scalar inputs", scalars.len()),
));
}
if vectors.len() != self.config.n_vector_in {
return Err(dimension_mismatch(
&format!("{} vector inputs", self.config.n_vector_in),
&format!("{} vector inputs", vectors.len()),
));
}
let mut mags = Vec::with_capacity(vectors.len());
for v in vectors {
mags.push(v.magnitude());
}
let mut out_scalars = vec![0.0_f64; self.config.n_scalar_out];
for (i, out_s) in out_scalars.iter_mut().enumerate() {
let mut acc = self.bias_s[i];
for (j, &sj) in scalars.iter().enumerate() {
acc += self.w_ss[i][j] * sj;
}
for (k, &mk) in mags.iter().enumerate() {
acc += self.w_vs[i][k] * mk;
}
*out_s = acc;
}
let mut out_vectors = vec![Vector3::<f64>::zero(); self.config.n_vector_out];
let paired = self.config.has_paired_sv();
for (i, out_v) in out_vectors.iter_mut().enumerate() {
let mut acc = Vector3::<f64>::zero();
for (k, vk) in vectors.iter().enumerate() {
acc = acc + (*vk * self.w_vv[i][k]);
}
if paired {
for (j, &sj) in scalars.iter().enumerate() {
let coef = self.w_sv[i][j] * sj;
acc = acc + vectors[j] * coef;
}
}
*out_v = acc;
}
Ok((out_scalars, out_vectors))
}
pub fn n_params(&self) -> usize {
let c = &self.config;
c.n_scalar_out * c.n_scalar_in + c.n_scalar_out * c.n_vector_in + c.n_vector_out * c.n_scalar_in + c.n_vector_out * c.n_vector_in + c.n_scalar_out }
pub fn params_flat(&self) -> Vec<f64> {
let mut v = Vec::with_capacity(self.n_params());
for row in &self.w_ss {
v.extend_from_slice(row);
}
for row in &self.w_vs {
v.extend_from_slice(row);
}
for row in &self.w_sv {
v.extend_from_slice(row);
}
for row in &self.w_vv {
v.extend_from_slice(row);
}
v.extend_from_slice(&self.bias_s);
v
}
pub fn set_params(&mut self, flat: &[f64]) -> Result<()> {
let expected = self.n_params();
if flat.len() != expected {
return Err(dimension_mismatch(
&format!("{expected} params"),
&format!("{} params", flat.len()),
));
}
let c = self.config;
let mut cur = 0_usize;
for i in 0..c.n_scalar_out {
for j in 0..c.n_scalar_in {
self.w_ss[i][j] = flat[cur];
cur += 1;
}
}
for i in 0..c.n_scalar_out {
for k in 0..c.n_vector_in {
self.w_vs[i][k] = flat[cur];
cur += 1;
}
}
for i in 0..c.n_vector_out {
for j in 0..c.n_scalar_in {
self.w_sv[i][j] = flat[cur];
cur += 1;
}
}
for i in 0..c.n_vector_out {
for k in 0..c.n_vector_in {
self.w_vv[i][k] = flat[cur];
cur += 1;
}
}
for i in 0..c.n_scalar_out {
self.bias_s[i] = flat[cur];
cur += 1;
}
Ok(())
}
}
pub struct EquivariantMlp {
pub layers: Vec<EquivariantLinear>,
pub input_config: EquivariantConfig,
pub output_config: EquivariantConfig,
}
impl EquivariantMlp {
pub fn new(layer_configs: &[EquivariantConfig], rng_seed: u64) -> Result<Self> {
if layer_configs.is_empty() {
return Err(invalid_param("layer_configs", "must contain ≥ 1 layer"));
}
for window in layer_configs.windows(2) {
let prev = window[0];
let next = window[1];
if prev.n_scalar_out != next.n_scalar_in {
return Err(invalid_param(
"layer_configs",
"scalar output of layer k must equal scalar input of layer k+1",
));
}
if prev.n_vector_out != next.n_vector_in {
return Err(invalid_param(
"layer_configs",
"vector output of layer k must equal vector input of layer k+1",
));
}
}
let mut layers = Vec::with_capacity(layer_configs.len());
for (k, cfg) in layer_configs.iter().enumerate() {
let sub_seed = rng_seed.wrapping_add((k as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
layers.push(EquivariantLinear::new(*cfg, sub_seed)?);
}
Ok(Self {
input_config: layer_configs[0],
output_config: *layer_configs.last().expect("non-empty checked above"),
layers,
})
}
pub fn forward(
&self,
scalars: &[f64],
vectors: &[Vector3<f64>],
) -> Result<(Vec<f64>, Vec<Vector3<f64>>)> {
let mut s = scalars.to_vec();
let mut v = vectors.to_vec();
let last_idx = self.layers.len() - 1;
for (k, layer) in self.layers.iter().enumerate() {
let (out_s, out_v) = layer.forward(&s, &v)?;
s = out_s;
v = out_v;
if k != last_idx {
for sj in s.iter_mut() {
*sj = sj.tanh();
}
}
}
Ok((s, v))
}
pub fn energy(&self, spins: &[Vector3<f64>]) -> Result<f64> {
if self.output_config.n_scalar_out == 0 {
return Err(invalid_param(
"EquivariantMlp",
"network must expose ≥ 1 scalar output to act as an energy",
));
}
let scalars: Vec<f64> = Vec::new();
let (out_s, _out_v) = self.forward(&scalars, spins)?;
Ok(out_s[0])
}
pub fn n_params(&self) -> usize {
self.layers.iter().map(|l| l.n_params()).sum()
}
pub fn params_flat(&self) -> Vec<f64> {
let mut v = Vec::with_capacity(self.n_params());
for layer in &self.layers {
v.extend(layer.params_flat());
}
v
}
pub fn set_params(&mut self, flat: &[f64]) -> Result<()> {
let expected = self.n_params();
if flat.len() != expected {
return Err(dimension_mismatch(
&format!("{expected} params"),
&format!("{} params", flat.len()),
));
}
let mut cursor = 0_usize;
for layer in &mut self.layers {
let n = layer.n_params();
layer.set_params(&flat[cursor..cursor + n])?;
cursor += n;
}
Ok(())
}
}
pub fn rotate_vector(r: &[[f64; 3]; 3], v: Vector3<f64>) -> Vector3<f64> {
Vector3::new(
r[0][0] * v.x + r[0][1] * v.y + r[0][2] * v.z,
r[1][0] * v.x + r[1][1] * v.y + r[1][2] * v.z,
r[2][0] * v.x + r[2][1] * v.y + r[2][2] * v.z,
)
}
pub fn random_so3(rng_seed: u64) -> [[f64; 3]; 3] {
let mut rng = Lcg::new(rng_seed);
let mut axis_x = rng.next_normal();
let mut axis_y = rng.next_normal();
let mut axis_z = rng.next_normal();
let norm = (axis_x * axis_x + axis_y * axis_y + axis_z * axis_z).sqrt();
if norm < 1e-14 {
axis_x = 0.0;
axis_y = 0.0;
axis_z = 1.0;
} else {
axis_x /= norm;
axis_y /= norm;
axis_z /= norm;
}
let theta = std::f64::consts::PI * rng.next_unit();
let c = theta.cos();
let s = theta.sin();
let cm1 = 1.0 - c;
let (ax, ay, az) = (axis_x, axis_y, axis_z);
[
[
c + ax * ax * cm1,
ax * ay * cm1 - az * s,
ax * az * cm1 + ay * s,
],
[
ay * ax * cm1 + az * s,
c + ay * ay * cm1,
ay * az * cm1 - ax * s,
],
[
az * ax * cm1 - ay * s,
az * ay * cm1 + ax * s,
c + az * az * cm1,
],
]
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn vec_approx(a: Vector3<f64>, b: Vector3<f64>, tol: f64) -> bool {
approx(a.x, b.x, tol) && approx(a.y, b.y, tol) && approx(a.z, b.z, tol)
}
#[test]
fn test_config_validation() {
let good = EquivariantConfig::new(0, 3, 4, 0);
assert!(good.validate().is_ok());
let no_input = EquivariantConfig::new(0, 0, 1, 1);
assert!(no_input.validate().is_err());
let no_output = EquivariantConfig::new(2, 2, 0, 0);
assert!(no_output.validate().is_err());
assert!(EquivariantLinear::new(no_input, 0).is_err());
}
#[test]
fn test_n_params_accounting() {
let cfg = EquivariantConfig::new(3, 3, 4, 2);
let layer = EquivariantLinear::new(cfg, 13).unwrap();
let expected = 4 * 3 + 4 * 3 + 2 * 3 + 2 * 3 + 4; assert_eq!(layer.n_params(), expected);
assert_eq!(layer.params_flat().len(), expected);
}
#[test]
fn test_params_roundtrip() {
let cfg = EquivariantConfig::new(2, 2, 3, 3);
let mut layer = EquivariantLinear::new(cfg, 99).unwrap();
let original = layer.params_flat();
let mut perturbed = original.clone();
for (i, v) in perturbed.iter_mut().enumerate() {
*v += 0.01 * (i as f64);
}
layer.set_params(&perturbed).unwrap();
let rt = layer.params_flat();
for (a, b) in rt.iter().zip(perturbed.iter()) {
assert_eq!(a.to_bits(), b.to_bits());
}
}
#[test]
fn test_rotation_invariance_of_energy() {
let layer1 = EquivariantConfig::new(0, 3, 4, 4);
let layer2 = EquivariantConfig::new(4, 4, 1, 0);
let mlp = EquivariantMlp::new(&[layer1, layer2], 42).unwrap();
let spins = vec![
Vector3::new(0.5, -0.3, 0.7),
Vector3::new(-0.2, 0.8, 0.1),
Vector3::new(0.9, 0.1, -0.4),
];
let r = random_so3(7);
let rotated: Vec<Vector3<f64>> = spins.iter().map(|s| rotate_vector(&r, *s)).collect();
let e0 = mlp.energy(&spins).unwrap();
let er = mlp.energy(&rotated).unwrap();
assert!(
approx(e0, er, 1e-10),
"energy not invariant: {} vs {}",
e0,
er
);
}
#[test]
fn test_rotation_equivariance_of_vector_output() {
let cfg = EquivariantConfig::new(0, 3, 0, 2);
let layer = EquivariantLinear::new(cfg, 11).unwrap();
let spins = vec![
Vector3::new(0.3, 0.4, -0.5),
Vector3::new(-0.6, 0.2, 0.8),
Vector3::new(0.1, -0.9, 0.4),
];
let r = random_so3(123);
let rotated_in: Vec<Vector3<f64>> = spins.iter().map(|s| rotate_vector(&r, *s)).collect();
let (_s0, v0) = layer.forward(&[], &spins).unwrap();
let (_sr, vr) = layer.forward(&[], &rotated_in).unwrap();
for (orig, after) in v0.iter().zip(vr.iter()) {
let expected = rotate_vector(&r, *orig);
assert!(
vec_approx(expected, *after, 1e-10),
"vector equivariance broken: expected R·v = ({},{},{}) got ({},{},{})",
expected.x,
expected.y,
expected.z,
after.x,
after.y,
after.z,
);
}
}
#[test]
fn test_zero_weights_zero_output() {
let cfg = EquivariantConfig::new(0, 2, 1, 2);
let mut layer = EquivariantLinear::new(cfg, 0).unwrap();
let zeros = vec![0.0_f64; layer.n_params()];
layer.set_params(&zeros).unwrap();
let (out_s, out_v) = layer
.forward(
&[],
&[Vector3::new(1.0, 2.0, 3.0), Vector3::new(0.5, 0.5, 0.5)],
)
.unwrap();
assert_eq!(out_s, vec![0.0]);
for v in &out_v {
assert!(vec_approx(*v, Vector3::zero(), 1e-15));
}
}
#[test]
fn test_fm_vs_disordered_energy_differs() {
let layer1 = EquivariantConfig::new(0, 4, 5, 4);
let layer2 = EquivariantConfig::new(5, 4, 1, 0);
let mlp = EquivariantMlp::new(&[layer1, layer2], 2024).unwrap();
let fm = vec![Vector3::unit_z(); 4];
let disordered = vec![
Vector3::unit_z(),
Vector3::unit_x(),
Vector3::unit_y(),
Vector3::new(0.0, 0.0, -1.0),
];
let e_fm = mlp.energy(&fm).unwrap();
let e_dis = mlp.energy(&disordered).unwrap();
assert!(e_fm.is_finite() && e_dis.is_finite());
assert!(
(e_fm - e_dis).abs() > 1e-8,
"FM and disordered should differ, got {} vs {}",
e_fm,
e_dis
);
}
#[test]
fn test_multi_layer_invariance() {
let cfgs = [
EquivariantConfig::new(0, 3, 5, 5),
EquivariantConfig::new(5, 5, 4, 4),
EquivariantConfig::new(4, 4, 1, 1),
];
let mlp = EquivariantMlp::new(&cfgs, 314).unwrap();
let spins = vec![
Vector3::new(0.2, 0.3, 0.6),
Vector3::new(-0.5, 0.4, 0.1),
Vector3::new(0.7, -0.2, 0.0),
];
let r = random_so3(2718);
let rotated: Vec<Vector3<f64>> = spins.iter().map(|s| rotate_vector(&r, *s)).collect();
let (s0, v0) = mlp.forward(&[], &spins).unwrap();
let (sr, vr) = mlp.forward(&[], &rotated).unwrap();
assert!(approx(s0[0], sr[0], 1e-10));
let expected_v = rotate_vector(&r, v0[0]);
assert!(vec_approx(expected_v, vr[0], 1e-10));
}
#[test]
fn test_rotate_vector_preserves_magnitude() {
let r = random_so3(8);
let v = Vector3::new(0.7, -0.3, 1.2);
let m0 = v.magnitude();
let mr = rotate_vector(&r, v).magnitude();
assert!(approx(m0, mr, 1e-12));
}
#[test]
fn test_random_so3_is_orthonormal() {
let r = random_so3(31415);
let mut rrt = [[0.0_f64; 3]; 3];
for (i, row_i) in r.iter().enumerate() {
for (j, row_j) in r.iter().enumerate() {
let acc: f64 = row_i.iter().zip(row_j.iter()).map(|(a, b)| a * b).sum();
rrt[i][j] = acc;
}
}
for (i, row) in rrt.iter().enumerate() {
for (j, &val) in row.iter().enumerate() {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
approx(val, expected, 1e-12),
"RRᵀ[{}][{}] = {} ≠ {}",
i,
j,
val,
expected
);
}
}
}
#[test]
fn test_seed_reproducibility() {
let cfg = EquivariantConfig::new(2, 2, 3, 3);
let l1 = EquivariantLinear::new(cfg, 555).unwrap();
let l2 = EquivariantLinear::new(cfg, 555).unwrap();
let p1 = l1.params_flat();
let p2 = l2.params_flat();
for (a, b) in p1.iter().zip(p2.iter()) {
assert_eq!(a.to_bits(), b.to_bits());
}
let r1 = random_so3(777);
let r2 = random_so3(777);
for i in 0..3 {
for j in 0..3 {
assert_eq!(r1[i][j].to_bits(), r2[i][j].to_bits());
}
}
}
#[test]
fn test_gated_activation_preserves_equivariance() {
let cfgs = [
EquivariantConfig::new(2, 2, 4, 4),
EquivariantConfig::new(4, 4, 3, 3),
EquivariantConfig::new(3, 3, 1, 2),
];
let mlp = EquivariantMlp::new(&cfgs, 654321).unwrap();
let scalars = vec![0.4_f64, -0.2];
let spins = vec![Vector3::new(0.1, 0.5, -0.2), Vector3::new(0.3, -0.6, 0.7)];
let r = random_so3(987);
let rotated_spins: Vec<Vector3<f64>> =
spins.iter().map(|s| rotate_vector(&r, *s)).collect();
let (s0, v0) = mlp.forward(&scalars, &spins).unwrap();
let (sr, vr) = mlp.forward(&scalars, &rotated_spins).unwrap();
assert!(approx(s0[0], sr[0], 1e-10));
for (o, n) in v0.iter().zip(vr.iter()) {
let expected = rotate_vector(&r, *o);
assert!(vec_approx(expected, *n, 1e-10));
}
}
#[test]
fn test_large_input_runs() {
let n_spins = 64;
let cfgs = [
EquivariantConfig::new(0, n_spins, 8, n_spins),
EquivariantConfig::new(8, n_spins, 1, 0),
];
let mlp = EquivariantMlp::new(&cfgs, 4242).unwrap();
let mut spins = Vec::with_capacity(n_spins);
for i in 0..n_spins {
let phase = (i as f64) * 0.15;
spins.push(Vector3::new(phase.cos(), phase.sin(), 0.0));
}
let e = mlp.energy(&spins).unwrap();
assert!(e.is_finite(), "energy must be finite for many-spin input");
}
}