use rand::SeedableRng;
use thermorust::{
dynamics::{step_discrete, Params},
energy::{Couplings, EnergyModel, Ising},
metrics::magnetisation,
State,
};
#[derive(Clone, Debug)]
pub struct ThermoConfig {
pub n: usize,
pub beta: f32,
pub coupling: f32,
pub steps_per_call: usize,
pub irreversible_cost: f64,
pub seed: u64,
}
impl Default for ThermoConfig {
fn default() -> Self {
Self {
n: 16,
beta: 3.0,
coupling: 0.2,
steps_per_call: 20,
irreversible_cost: 2.87e-21, seed: 0,
}
}
}
#[derive(Clone, Debug)]
pub struct ThermoSignal {
pub lambda: f32,
pub magnetisation: f32,
pub dissipation_j: f64,
pub energy_after: f32,
}
pub struct ThermoLayer {
model: Ising,
state: State,
params: Params,
rng: rand::rngs::SmallRng,
}
impl ThermoLayer {
pub fn new(cfg: ThermoConfig) -> Self {
let couplings = Couplings::ferromagnetic_ring(cfg.n, cfg.coupling);
let model = Ising::new(couplings);
let state = State::ones(cfg.n);
let params = Params {
beta: cfg.beta,
eta: 0.05,
irreversible_cost: cfg.irreversible_cost,
clamp_mask: vec![false; cfg.n],
};
let rng = rand::rngs::SmallRng::seed_from_u64(cfg.seed);
Self {
model,
state,
params,
rng,
}
}
pub fn run(&mut self, activations: &mut [f32], steps: usize) -> ThermoSignal {
let n = self.state.len().min(activations.len());
for i in 0..n {
self.state.x[i] = activations[i].clamp(-1.0, 1.0).signum();
}
let e_before = self.model.energy(&self.state);
for _ in 0..steps {
step_discrete(&self.model, &mut self.state, &self.params, &mut self.rng);
}
let e_after = self.model.energy(&self.state);
let d_e = e_after - e_before;
let lambda = if e_before.abs() > 1e-9 {
-d_e / e_before.abs()
} else {
0.0
};
for i in 0..n {
activations[i] = self.state.x[i];
}
ThermoSignal {
lambda,
magnetisation: magnetisation(&self.state),
dissipation_j: self.state.dissipated_j,
energy_after: e_after,
}
}
pub fn reset(&mut self) {
for xi in &mut self.state.x {
*xi = 1.0;
}
self.state.dissipated_j = 0.0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thermo_layer_runs_without_panic() {
let cfg = ThermoConfig {
n: 8,
steps_per_call: 10,
..Default::default()
};
let mut layer = ThermoLayer::new(cfg);
let mut acts = vec![1.0_f32; 8];
let sig = layer.run(&mut acts, 10);
assert!(sig.lambda.is_finite());
assert!(sig.magnetisation >= -1.0 && sig.magnetisation <= 1.0);
assert!(sig.dissipation_j >= 0.0);
}
#[test]
fn activations_are_binarised() {
let cfg = ThermoConfig {
n: 4,
steps_per_call: 0,
..Default::default()
};
let mut layer = ThermoLayer::new(cfg);
let mut acts = vec![0.7_f32, -0.3, 0.1, -0.9];
layer.run(&mut acts, 0);
for a in &acts {
assert!(
(*a - 1.0).abs() < 1e-6 || (*a + 1.0).abs() < 1e-6,
"not ±1: {a}"
);
}
}
#[test]
fn lambda_finite_after_many_steps() {
let cfg = ThermoConfig {
n: 16,
beta: 5.0,
..Default::default()
};
let mut layer = ThermoLayer::new(cfg);
for _ in 0..10 {
let mut acts = vec![1.0_f32; 16];
let sig = layer.run(&mut acts, 50);
assert!(sig.lambda.is_finite());
}
}
}