use super::projection::{Projection, ProjectionId};
const THERMAL_BYTES: usize = 5 * 4;
#[derive(Debug, Clone, Copy)]
pub struct ThermalProjection {
pub spin: f32,
pub bias: f32,
pub temperature: f32,
pub energy: f32,
pub magnetization: f32,
}
impl Default for ThermalProjection {
fn default() -> Self {
Self {
spin: 1.0,
bias: 0.0,
temperature: 1.0,
energy: 0.0,
magnetization: 0.0,
}
}
}
impl Projection for ThermalProjection {
fn byte_size() -> usize {
THERMAL_BYTES
}
fn id() -> ProjectionId {
ProjectionId::Thermal
}
fn read(buf: &[u8]) -> Self {
assert!(buf.len() >= THERMAL_BYTES);
Self {
spin: f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
bias: f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
temperature: f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
energy: f32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
magnetization: f32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
}
}
fn write(&self, buf: &mut [u8]) {
assert!(buf.len() >= THERMAL_BYTES);
buf[0..4].copy_from_slice(&self.spin.to_le_bytes());
buf[4..8].copy_from_slice(&self.bias.to_le_bytes());
buf[8..12].copy_from_slice(&self.temperature.to_le_bytes());
buf[12..16].copy_from_slice(&self.energy.to_le_bytes());
buf[16..20].copy_from_slice(&self.magnetization.to_le_bytes());
}
fn shape_hash_contribution(&self) -> u32 {
let mut hash = 0x811c_9dc5u32;
for byte in self.spin.to_bits().to_le_bytes() {
hash ^= byte as u32;
hash = hash.wrapping_mul(0x0100_0193);
}
for byte in self.magnetization.to_bits().to_le_bytes() {
hash ^= byte as u32;
hash = hash.wrapping_mul(0x0100_0193);
}
hash
}
}
pub fn gibbs_step(
_spin_i: f32,
bias_i: f32,
neighbor_coupling_sum: f32,
beta: f32,
entropy_seed: f32,
) -> (f32, f32) {
let local_field = bias_i + neighbor_coupling_sum;
let arg = 2.0 * beta * local_field;
let prob_up = 1.0 / (1.0 + (-arg).exp());
let seed = entropy_seed.clamp(0.0, 0.9999);
let new_spin = if seed < prob_up { 1.0 } else { -1.0 };
let energy = -new_spin * local_field;
(new_spin, energy)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_thermal_byte_size() {
assert_eq!(ThermalProjection::byte_size(), 20);
}
#[test]
fn test_thermal_roundtrip() {
let proj = ThermalProjection {
spin: -1.0,
bias: 0.5,
temperature: 2.0,
energy: -0.3,
magnetization: 0.7,
};
let mut buf = vec![0u8; ThermalProjection::byte_size()];
proj.write(&mut buf);
let restored = ThermalProjection::read(&buf);
assert!((restored.spin - (-1.0)).abs() < 1e-6);
assert!((restored.bias - 0.5).abs() < 1e-6);
assert!((restored.temperature - 2.0).abs() < 1e-6);
assert!((restored.energy - (-0.3)).abs() < 1e-6);
assert!((restored.magnetization - 0.7).abs() < 1e-6);
}
#[test]
fn test_thermal_default() {
let proj = ThermalProjection::default();
assert!((proj.spin - 1.0).abs() < 1e-6);
assert!((proj.bias).abs() < 1e-6);
assert!((proj.temperature - 1.0).abs() < 1e-6);
}
#[test]
fn test_gibbs_step_strong_field_up() {
let (spin, energy) = gibbs_step(1.0, 10.0, 0.0, 5.0, 0.5);
assert!((spin - 1.0).abs() < 1e-6, "spin = {}", spin);
assert!(energy < 0.0, "energy should be negative for aligned spin");
}
#[test]
fn test_gibbs_step_strong_field_down() {
let (spin, _energy) = gibbs_step(1.0, -10.0, 0.0, 5.0, 0.5);
assert!((spin - (-1.0)).abs() < 1e-6, "spin = {}", spin);
}
#[test]
fn test_gibbs_step_deterministic() {
let a = gibbs_step(1.0, 0.5, 0.3, 2.0, 0.4);
let b = gibbs_step(1.0, 0.5, 0.3, 2.0, 0.4);
assert!((a.0 - b.0).abs() < 1e-6);
assert!((a.1 - b.1).abs() < 1e-6);
}
#[test]
fn test_gibbs_step_entropy_clamp() {
let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, 0.0);
let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, 1.0);
let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, -0.5);
let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, 1.5);
}
#[test]
fn test_shape_hash_changes_with_state() {
let a = ThermalProjection {
spin: 1.0,
..Default::default()
};
let b = ThermalProjection {
spin: -1.0,
..Default::default()
};
assert_ne!(a.shape_hash_contribution(), b.shape_hash_contribution());
}
}