Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
163
vendor/ruvector/crates/thermorust/src/dynamics.rs
vendored
Normal file
163
vendor/ruvector/crates/thermorust/src/dynamics.rs
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
//! Stochastic dynamics: Metropolis-Hastings (discrete) and overdamped Langevin (continuous).
|
||||
|
||||
use crate::energy::EnergyModel;
|
||||
use crate::noise::{langevin_noise, poisson_spike};
|
||||
use crate::state::State;
|
||||
use rand::Rng;
|
||||
|
||||
/// Parameters governing thermal dynamics and Landauer dissipation accounting.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Params {
|
||||
/// Inverse temperature β = 1/(kT). Higher β → colder, less noise.
|
||||
pub beta: f32,
|
||||
/// Step size η for continuous (Langevin) updates.
|
||||
pub eta: f32,
|
||||
/// Joules of heat attributed to each accepted irreversible transition.
|
||||
/// Landauer's limit: kT ln2 ≈ 2.87 × 10⁻²¹ J at 300 K.
|
||||
pub irreversible_cost: f64,
|
||||
/// Which unit indices are clamped (fixed inputs).
|
||||
pub clamp_mask: Vec<bool>,
|
||||
}
|
||||
|
||||
impl Params {
|
||||
/// Sensible defaults: room-temperature Landauer limit, no clamping.
|
||||
pub fn default_n(n: usize) -> Self {
|
||||
Self {
|
||||
beta: 2.0,
|
||||
eta: 0.05,
|
||||
irreversible_cost: 2.87e-21, // kT ln2 at 300 K in Joules
|
||||
clamp_mask: vec![false; n],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_clamped(&self, i: usize) -> bool {
|
||||
self.clamp_mask.get(i).copied().unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// **Metropolis-Hastings** single spin-flip update for *discrete* Ising states.
|
||||
///
|
||||
/// Proposes flipping spin `i` (chosen uniformly at random), accepts with the
|
||||
/// Boltzmann probability, and charges `p.irreversible_cost` on each accepted
|
||||
/// non-zero-ΔE transition.
|
||||
pub fn step_discrete<M: EnergyModel>(model: &M, s: &mut State, p: &Params, rng: &mut impl Rng) {
|
||||
let n = s.x.len();
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
let i: usize = rng.gen_range(0..n);
|
||||
if p.is_clamped(i) {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_e = model.energy(s);
|
||||
let old_si = s.x[i];
|
||||
s.x[i] = -old_si;
|
||||
let new_e = model.energy(s);
|
||||
let d_e = (new_e - old_e) as f64;
|
||||
|
||||
let accept = d_e <= 0.0 || {
|
||||
let prob = (-p.beta as f64 * d_e).exp();
|
||||
rng.gen::<f64>() < prob
|
||||
};
|
||||
|
||||
if accept {
|
||||
if d_e != 0.0 {
|
||||
s.dissipated_j += p.irreversible_cost;
|
||||
}
|
||||
} else {
|
||||
s.x[i] = old_si;
|
||||
}
|
||||
}
|
||||
|
||||
/// **Overdamped Langevin** update for *continuous* activations.
|
||||
///
|
||||
/// For each unclamped unit `i`:
|
||||
/// xᵢ ← xᵢ − η · ∂H/∂xᵢ + √(2/β) · ξ
|
||||
/// where ξ ~ N(0,1). The gradient is estimated by central differences.
|
||||
///
|
||||
/// Optionally clips activations to `[-1, 1]` after the update.
|
||||
pub fn step_continuous<M: EnergyModel>(model: &M, s: &mut State, p: &Params, rng: &mut impl Rng) {
|
||||
let n = s.x.len();
|
||||
let eps = 1e-3_f32;
|
||||
|
||||
for i in 0..n {
|
||||
if p.is_clamped(i) {
|
||||
continue;
|
||||
}
|
||||
let old = s.x[i];
|
||||
|
||||
// Central-difference gradient ∂H/∂xᵢ
|
||||
s.x[i] = old + eps;
|
||||
let e_plus = model.energy(s);
|
||||
s.x[i] = old - eps;
|
||||
let e_minus = model.energy(s);
|
||||
s.x[i] = old;
|
||||
|
||||
let grad = (e_plus - e_minus) / (2.0 * eps);
|
||||
let noise = langevin_noise(p.beta, rng);
|
||||
let dx = -p.eta * grad + noise;
|
||||
|
||||
let old_e = model.energy(s);
|
||||
s.x[i] = (old + dx).clamp(-1.0, 1.0);
|
||||
let new_e = model.energy(s);
|
||||
|
||||
if (new_e as f64) < (old_e as f64) {
|
||||
s.dissipated_j += p.irreversible_cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `steps` discrete Metropolis updates, recording every `record_every`th
|
||||
/// step into the optional `trace`.
|
||||
pub fn anneal_discrete<M: EnergyModel>(
|
||||
model: &M,
|
||||
s: &mut State,
|
||||
p: &Params,
|
||||
steps: usize,
|
||||
record_every: usize,
|
||||
rng: &mut impl Rng,
|
||||
) -> crate::metrics::Trace {
|
||||
let mut trace = crate::metrics::Trace::new();
|
||||
for step in 0..steps {
|
||||
step_discrete(model, s, p, rng);
|
||||
if record_every > 0 && step % record_every == 0 {
|
||||
trace.push(model.energy(s), s.dissipated_j);
|
||||
}
|
||||
}
|
||||
trace
|
||||
}
|
||||
|
||||
/// Run `steps` Langevin updates, recording every `record_every`th step.
|
||||
pub fn anneal_continuous<M: EnergyModel>(
|
||||
model: &M,
|
||||
s: &mut State,
|
||||
p: &Params,
|
||||
steps: usize,
|
||||
record_every: usize,
|
||||
rng: &mut impl Rng,
|
||||
) -> crate::metrics::Trace {
|
||||
let mut trace = crate::metrics::Trace::new();
|
||||
for step in 0..steps {
|
||||
step_continuous(model, s, p, rng);
|
||||
if record_every > 0 && step % record_every == 0 {
|
||||
trace.push(model.energy(s), s.dissipated_j);
|
||||
}
|
||||
}
|
||||
trace
|
||||
}
|
||||
|
||||
/// Inject Poisson spike noise into `s`, bypassing thermal Boltzmann acceptance.
|
||||
///
|
||||
/// Each unit has an independent probability `rate` (per step) of receiving a
|
||||
/// kick of magnitude `kick`, with a random sign.
|
||||
pub fn inject_spikes(s: &mut State, p: &Params, rate: f64, kick: f32, rng: &mut impl Rng) {
|
||||
for (i, xi) in s.x.iter_mut().enumerate() {
|
||||
if p.is_clamped(i) {
|
||||
continue;
|
||||
}
|
||||
let dk = poisson_spike(rate, kick, rng);
|
||||
*xi = (*xi + dk).clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
126
vendor/ruvector/crates/thermorust/src/energy.rs
vendored
Normal file
126
vendor/ruvector/crates/thermorust/src/energy.rs
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
//! Energy models: Ising/Hopfield Hamiltonian and the `EnergyModel` trait.
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
/// Coupling weights and local fields for a fully-connected motif.
|
||||
///
|
||||
/// `j` is a flattened row-major `n×n` symmetric matrix; `h` is the `n`-vector
|
||||
/// of local (bias) fields.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Couplings {
|
||||
/// Symmetric coupling matrix J_ij (row-major, length n²).
|
||||
pub j: Vec<f32>,
|
||||
/// Local field h_i (length n).
|
||||
pub h: Vec<f32>,
|
||||
}
|
||||
|
||||
impl Couplings {
|
||||
/// Build zero-coupling weights for `n` units.
|
||||
pub fn zeros(n: usize) -> Self {
|
||||
Self {
|
||||
j: vec![0.0; n * n],
|
||||
h: vec![0.0; n],
|
||||
}
|
||||
}
|
||||
|
||||
/// Build ferromagnetic ring couplings: J_{i, i+1} = strength.
|
||||
pub fn ferromagnetic_ring(n: usize, strength: f32) -> Self {
|
||||
let mut j = vec![0.0; n * n];
|
||||
for i in 0..n {
|
||||
let next = (i + 1) % n;
|
||||
j[i * n + next] = strength;
|
||||
j[next * n + i] = strength;
|
||||
}
|
||||
Self { j, h: vec![0.0; n] }
|
||||
}
|
||||
|
||||
/// Build random Hopfield memory couplings from a list of patterns.
|
||||
///
|
||||
/// Patterns should be `±1` binary vectors of length `n`.
|
||||
pub fn hopfield_memory(n: usize, patterns: &[Vec<f32>]) -> Self {
|
||||
let mut j = vec![0.0f32; n * n];
|
||||
let scale = 1.0 / n as f32;
|
||||
for pat in patterns {
|
||||
assert_eq!(pat.len(), n, "pattern length must equal n");
|
||||
for i in 0..n {
|
||||
for k in (i + 1)..n {
|
||||
let dj = scale * pat[i] * pat[k];
|
||||
j[i * n + k] += dj;
|
||||
j[k * n + i] += dj;
|
||||
}
|
||||
}
|
||||
}
|
||||
Self { j, h: vec![0.0; n] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait implemented by any Hamiltonian that can return a scalar energy.
|
||||
pub trait EnergyModel {
|
||||
/// Compute the total energy of `state`.
|
||||
fn energy(&self, state: &State) -> f32;
|
||||
}
|
||||
|
||||
/// Ising/Hopfield Hamiltonian:
|
||||
/// H = −Σᵢ hᵢ xᵢ − Σᵢ<ⱼ Jᵢⱼ xᵢ xⱼ
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ising {
|
||||
pub c: Couplings,
|
||||
}
|
||||
|
||||
impl Ising {
|
||||
pub fn new(c: Couplings) -> Self {
|
||||
Self { c }
|
||||
}
|
||||
}
|
||||
|
||||
impl EnergyModel for Ising {
|
||||
fn energy(&self, s: &State) -> f32 {
|
||||
let n = s.x.len();
|
||||
debug_assert_eq!(self.c.h.len(), n);
|
||||
let mut e = 0.0_f32;
|
||||
for i in 0..n {
|
||||
e -= self.c.h[i] * s.x[i];
|
||||
for j in (i + 1)..n {
|
||||
e -= self.c.j[i * n + j] * s.x[i] * s.x[j];
|
||||
}
|
||||
}
|
||||
e
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft-spin (XY-like) model with continuous activations.
|
||||
///
|
||||
/// Adds a quartic double-well self-energy per unit: −a·x² + b·x⁴
|
||||
/// which promotes ±1 attractors.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SoftSpin {
|
||||
pub c: Couplings,
|
||||
/// Well depth coefficient (>0 pushes spins toward ±1).
|
||||
pub a: f32,
|
||||
/// Quartic stiffness (>0 keeps spins bounded).
|
||||
pub b: f32,
|
||||
}
|
||||
|
||||
impl SoftSpin {
|
||||
pub fn new(c: Couplings, a: f32, b: f32) -> Self {
|
||||
Self { c, a, b }
|
||||
}
|
||||
}
|
||||
|
||||
impl EnergyModel for SoftSpin {
|
||||
fn energy(&self, s: &State) -> f32 {
|
||||
let n = s.x.len();
|
||||
let mut e = 0.0_f32;
|
||||
for i in 0..n {
|
||||
let xi = s.x[i];
|
||||
// Double-well self-energy
|
||||
e += -self.a * xi * xi + self.b * xi * xi * xi * xi;
|
||||
// Local field
|
||||
e -= self.c.h[i] * xi;
|
||||
for j in (i + 1)..n {
|
||||
e -= self.c.j[i * n + j] * xi * s.x[j];
|
||||
}
|
||||
}
|
||||
e
|
||||
}
|
||||
}
|
||||
45
vendor/ruvector/crates/thermorust/src/lib.rs
vendored
Normal file
45
vendor/ruvector/crates/thermorust/src/lib.rs
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
//! # thermorust
|
||||
//!
|
||||
//! A minimal thermodynamic neural-motif crate for Rust.
|
||||
//!
|
||||
//! Treats computation as **energy-driven state transitions** with
|
||||
//! Landauer-style dissipation and Langevin/Metropolis noise baked in.
|
||||
//!
|
||||
//! ## Core abstractions
|
||||
//!
|
||||
//! | Module | What it provides |
|
||||
//! |--------|-----------------|
|
||||
//! | [`state`] | `State` – activation vector + dissipated-joules counter |
|
||||
//! | [`energy`] | `EnergyModel` trait, `Ising`, `SoftSpin`, `Couplings` |
|
||||
//! | [`dynamics`] | `step_discrete` (MH), `step_continuous` (Langevin), annealers |
|
||||
//! | [`noise`] | Langevin & Poisson spike noise sources |
|
||||
//! | [`metrics`] | Magnetisation, overlap, entropy, free energy, `Trace` |
|
||||
//! | [`motifs`] | Pre-wired ring / fully-connected / Hopfield / soft-spin motifs |
|
||||
//!
|
||||
//! ## Quick start
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use thermorust::{motifs::IsingMotif, dynamics::{Params, anneal_discrete}};
|
||||
//! use rand::SeedableRng;
|
||||
//!
|
||||
//! let mut motif = IsingMotif::ring(16, 0.2);
|
||||
//! let params = Params::default_n(16);
|
||||
//! let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||
//!
|
||||
//! let trace = anneal_discrete(&motif.model, &mut motif.state, ¶ms, 10_000, 100, &mut rng);
|
||||
//! println!("Mean energy: {:.3}", trace.mean_energy());
|
||||
//! println!("Heat shed: {:.3e} J", trace.total_dissipation());
|
||||
//! ```
|
||||
|
||||
pub mod dynamics;
|
||||
pub mod energy;
|
||||
pub mod metrics;
|
||||
pub mod motifs;
|
||||
pub mod noise;
|
||||
pub mod state;
|
||||
|
||||
// Re-export the most commonly used items at the crate root.
|
||||
pub use dynamics::{anneal_continuous, anneal_discrete, step_continuous, step_discrete, Params};
|
||||
pub use energy::{Couplings, EnergyModel, Ising, SoftSpin};
|
||||
pub use metrics::{magnetisation, overlap, Trace};
|
||||
pub use state::State;
|
||||
95
vendor/ruvector/crates/thermorust/src/metrics.rs
vendored
Normal file
95
vendor/ruvector/crates/thermorust/src/metrics.rs
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
//! Thermodynamic observables: magnetisation, entropy, free energy, overlap.
|
||||
|
||||
use crate::state::State;
|
||||
|
||||
/// Mean magnetisation: m = (1/n) Σᵢ xᵢ ∈ [−1, 1].
|
||||
pub fn magnetisation(s: &State) -> f32 {
|
||||
if s.x.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
s.x.iter().sum::<f32>() / s.x.len() as f32
|
||||
}
|
||||
|
||||
/// Mean-squared activation: ⟨x²⟩.
|
||||
pub fn mean_sq(s: &State) -> f32 {
|
||||
if s.x.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
s.x.iter().map(|xi| xi * xi).sum::<f32>() / s.x.len() as f32
|
||||
}
|
||||
|
||||
/// Pattern overlap (Hopfield order parameter):
|
||||
/// m_μ = (1/n) Σᵢ ξᵢ^μ xᵢ
|
||||
///
|
||||
/// Returns `None` if lengths differ.
|
||||
pub fn overlap(s: &State, pattern: &[f32]) -> Option<f32> {
|
||||
let n = s.x.len();
|
||||
if pattern.len() != n || n == 0 {
|
||||
return None;
|
||||
}
|
||||
let sum: f32 = s.x.iter().zip(pattern.iter()).map(|(xi, pi)| xi * pi).sum();
|
||||
Some(sum / n as f32)
|
||||
}
|
||||
|
||||
/// Approximate configurational entropy (binary case) via:
|
||||
/// S ≈ −n [ p ln p + (1−p) ln(1−p) ]
|
||||
/// where p = fraction of spins at +1.
|
||||
///
|
||||
/// Returns 0 for edge cases (all ±1).
|
||||
pub fn binary_entropy(s: &State) -> f32 {
|
||||
let n = s.x.len();
|
||||
if n == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let p_up = s.x.iter().filter(|&&xi| xi > 0.0).count() as f32 / n as f32;
|
||||
let p_dn = 1.0 - p_up;
|
||||
let h = |p: f32| {
|
||||
if p <= 0.0 || p >= 1.0 {
|
||||
0.0
|
||||
} else {
|
||||
-p * p.ln() - (1.0 - p) * (1.0 - p).ln()
|
||||
}
|
||||
};
|
||||
n as f32 * h(p_up) * 0.5 + n as f32 * h(p_dn) * 0.5
|
||||
}
|
||||
|
||||
/// Estimate free energy: F ≈ E − T·S = E − S/β.
|
||||
///
|
||||
/// `energy` should be `model.energy(s)`.
|
||||
pub fn free_energy(energy: f32, entropy: f32, beta: f32) -> f32 {
|
||||
energy - entropy / beta
|
||||
}
|
||||
|
||||
/// Running statistics accumulator for energy / dissipation traces.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Trace {
|
||||
/// Energy samples (one per recorded step).
|
||||
pub energies: Vec<f32>,
|
||||
/// Cumulative dissipation at each recorded step.
|
||||
pub dissipation: Vec<f64>,
|
||||
}
|
||||
|
||||
impl Trace {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Record one observation.
|
||||
pub fn push(&mut self, energy: f32, dissipated_j: f64) {
|
||||
self.energies.push(energy);
|
||||
self.dissipation.push(dissipated_j);
|
||||
}
|
||||
|
||||
/// Mean energy over all recorded steps.
|
||||
pub fn mean_energy(&self) -> f32 {
|
||||
if self.energies.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
self.energies.iter().sum::<f32>() / self.energies.len() as f32
|
||||
}
|
||||
|
||||
/// Total heat shed over all steps.
|
||||
pub fn total_dissipation(&self) -> f64 {
|
||||
self.dissipation.last().copied().unwrap_or(0.0)
|
||||
}
|
||||
}
|
||||
77
vendor/ruvector/crates/thermorust/src/motifs.rs
vendored
Normal file
77
vendor/ruvector/crates/thermorust/src/motifs.rs
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Pre-wired motif factories: ring, fully-connected, and Hopfield memory nets.
|
||||
|
||||
use crate::energy::{Couplings, Ising, SoftSpin};
|
||||
use crate::state::State;
|
||||
|
||||
/// A self-contained motif: initial state + Ising Hamiltonian + default params.
|
||||
pub struct IsingMotif {
|
||||
pub state: State,
|
||||
pub model: Ising,
|
||||
}
|
||||
|
||||
impl IsingMotif {
|
||||
/// Ferromagnetic ring of `n` spins. J_{i,i+1} = `strength`.
|
||||
pub fn ring(n: usize, strength: f32) -> Self {
|
||||
Self {
|
||||
state: State::ones(n),
|
||||
model: Ising::new(Couplings::ferromagnetic_ring(n, strength)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fully connected ferromagnet: J_ij = strength for all i≠j.
|
||||
pub fn fully_connected(n: usize, strength: f32) -> Self {
|
||||
let mut j = vec![0.0_f32; n * n];
|
||||
for i in 0..n {
|
||||
for k in (i + 1)..n {
|
||||
j[i * n + k] = strength;
|
||||
j[k * n + i] = strength;
|
||||
}
|
||||
}
|
||||
Self {
|
||||
state: State::ones(n),
|
||||
model: Ising::new(Couplings { j, h: vec![0.0; n] }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hopfield associative memory loaded with `patterns` (±1 binary vectors).
|
||||
pub fn hopfield(n: usize, patterns: &[Vec<f32>]) -> Self {
|
||||
Self {
|
||||
state: State::ones(n),
|
||||
model: Ising::new(Couplings::hopfield_memory(n, patterns)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft-spin motif with double-well on-site potential for continuous activations.
|
||||
pub struct SoftSpinMotif {
|
||||
pub state: State,
|
||||
pub model: SoftSpin,
|
||||
}
|
||||
|
||||
impl SoftSpinMotif {
|
||||
/// Random-coupling soft-spin motif seeded with `seed`.
|
||||
pub fn random(n: usize, a: f32, b: f32, seed: u64) -> Self {
|
||||
use rand::{Rng, SeedableRng};
|
||||
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
|
||||
let j: Vec<f32> = (0..n * n).map(|_| rng.gen_range(-0.5_f32..0.5)).collect();
|
||||
// Symmetrise
|
||||
let mut j_sym = vec![0.0_f32; n * n];
|
||||
for i in 0..n {
|
||||
for k in 0..n {
|
||||
j_sym[i * n + k] = (j[i * n + k] + j[k * n + i]) * 0.5;
|
||||
}
|
||||
}
|
||||
let x: Vec<f32> = (0..n).map(|_| rng.gen_range(-0.1_f32..0.1)).collect();
|
||||
Self {
|
||||
state: State::from_vec(x),
|
||||
model: SoftSpin::new(
|
||||
Couplings {
|
||||
j: j_sym,
|
||||
h: vec![0.0; n],
|
||||
},
|
||||
a,
|
||||
b,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
48
vendor/ruvector/crates/thermorust/src/noise.rs
vendored
Normal file
48
vendor/ruvector/crates/thermorust/src/noise.rs
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Thermal noise sources: Gaussian (Langevin) and Poisson spike noise.
|
||||
|
||||
use rand::Rng;
|
||||
use rand_distr::{Distribution, Normal, Poisson};
|
||||
|
||||
/// Draw a Gaussian noise sample with standard deviation σ = √(2/β).
|
||||
///
|
||||
/// This matches the fluctuation-dissipation theorem for overdamped Langevin:
|
||||
/// the noise amplitude must be √(2kT) = √(2/β) in dimensionless units.
|
||||
#[inline]
|
||||
pub fn langevin_noise(beta: f32, rng: &mut impl Rng) -> f32 {
|
||||
if beta <= 0.0 || !beta.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
let sigma = (2.0 / beta).sqrt();
|
||||
Normal::new(0.0_f32, sigma)
|
||||
.unwrap_or_else(|_| Normal::new(0.0_f32, 1e-6).unwrap())
|
||||
.sample(rng)
|
||||
}
|
||||
|
||||
/// Draw `n` independent Langevin noise samples.
|
||||
pub fn langevin_noise_vec(beta: f32, n: usize, rng: &mut impl Rng) -> Vec<f32> {
|
||||
if beta <= 0.0 || !beta.is_finite() {
|
||||
return vec![0.0; n];
|
||||
}
|
||||
let sigma = (2.0 / beta).sqrt();
|
||||
let dist = Normal::new(0.0_f32, sigma).unwrap_or_else(|_| Normal::new(0.0_f32, 1e-6).unwrap());
|
||||
(0..n).map(|_| dist.sample(rng)).collect()
|
||||
}
|
||||
|
||||
/// Poisson spike noise: add a random kick of magnitude `kick` with rate λ.
|
||||
///
|
||||
/// Returns the kick to add to a single activation (0.0 if no spike this step).
|
||||
#[inline]
|
||||
pub fn poisson_spike(rate: f64, kick: f32, rng: &mut impl Rng) -> f32 {
|
||||
if rate <= 0.0 || !rate.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
let dist = Poisson::new(rate).unwrap_or_else(|_| Poisson::new(1e-6).unwrap());
|
||||
let count = dist.sample(rng) as u64;
|
||||
if count > 0 {
|
||||
// Random sign
|
||||
let sign = if rng.gen::<bool>() { 1.0 } else { -1.0 };
|
||||
sign * kick * count as f32
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
59
vendor/ruvector/crates/thermorust/src/state.rs
vendored
Normal file
59
vendor/ruvector/crates/thermorust/src/state.rs
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
//! System state: continuous activations or binary spins with dissipation bookkeeping.
|
||||
|
||||
/// State of a thermodynamic motif.
|
||||
///
|
||||
/// Activations are stored as `f32` in `[-1.0, 1.0]` (or `{-1.0, +1.0}` for
|
||||
/// discrete Ising spins). `dissipated_j` accumulates the total Joules of heat
|
||||
/// shed over all accepted irreversible transitions.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct State {
|
||||
/// Spin / activation vector.
|
||||
pub x: Vec<f32>,
|
||||
/// Cumulative heat dissipated (Joules).
|
||||
pub dissipated_j: f64,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Construct a new state with all spins set to `+1`.
|
||||
pub fn ones(n: usize) -> Self {
|
||||
Self {
|
||||
x: vec![1.0; n],
|
||||
dissipated_j: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new state with all spins set to `-1`.
|
||||
pub fn neg_ones(n: usize) -> Self {
|
||||
Self {
|
||||
x: vec![-1.0; n],
|
||||
dissipated_j: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a state from an explicit activation vector.
|
||||
pub fn from_vec(x: Vec<f32>) -> Self {
|
||||
Self {
|
||||
x,
|
||||
dissipated_j: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of units in the motif.
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.x.len()
|
||||
}
|
||||
|
||||
/// True if the motif has no units.
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.x.is_empty()
|
||||
}
|
||||
|
||||
/// Clamp all activations to `[-1.0, 1.0]`.
|
||||
pub fn clamp(&mut self) {
|
||||
for xi in &mut self.x {
|
||||
*xi = xi.clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user