terminals_core/substrate/
thermal.rs1use super::projection::{Projection, ProjectionId};
9
10const THERMAL_BYTES: usize = 5 * 4;
12
13#[derive(Debug, Clone, Copy)]
15pub struct ThermalProjection {
16 pub spin: f32,
18 pub bias: f32,
20 pub temperature: f32,
22 pub energy: f32,
24 pub magnetization: f32,
26}
27
28impl Default for ThermalProjection {
29 fn default() -> Self {
30 Self {
31 spin: 1.0,
32 bias: 0.0,
33 temperature: 1.0,
34 energy: 0.0,
35 magnetization: 0.0,
36 }
37 }
38}
39
40impl Projection for ThermalProjection {
41 fn byte_size() -> usize {
42 THERMAL_BYTES
43 }
44
45 fn id() -> ProjectionId {
46 ProjectionId::Thermal
47 }
48
49 fn read(buf: &[u8]) -> Self {
50 assert!(buf.len() >= THERMAL_BYTES);
51 Self {
52 spin: f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]),
53 bias: f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
54 temperature: f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
55 energy: f32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]),
56 magnetization: f32::from_le_bytes([buf[16], buf[17], buf[18], buf[19]]),
57 }
58 }
59
60 fn write(&self, buf: &mut [u8]) {
61 assert!(buf.len() >= THERMAL_BYTES);
62 buf[0..4].copy_from_slice(&self.spin.to_le_bytes());
63 buf[4..8].copy_from_slice(&self.bias.to_le_bytes());
64 buf[8..12].copy_from_slice(&self.temperature.to_le_bytes());
65 buf[12..16].copy_from_slice(&self.energy.to_le_bytes());
66 buf[16..20].copy_from_slice(&self.magnetization.to_le_bytes());
67 }
68
69 fn shape_hash_contribution(&self) -> u32 {
70 let mut hash = 0x811c_9dc5u32;
71 for byte in self.spin.to_bits().to_le_bytes() {
72 hash ^= byte as u32;
73 hash = hash.wrapping_mul(0x0100_0193);
74 }
75 for byte in self.magnetization.to_bits().to_le_bytes() {
76 hash ^= byte as u32;
77 hash = hash.wrapping_mul(0x0100_0193);
78 }
79 hash
80 }
81}
82
83pub fn gibbs_step(
99 _spin_i: f32,
100 bias_i: f32,
101 neighbor_coupling_sum: f32,
102 beta: f32,
103 entropy_seed: f32,
104) -> (f32, f32) {
105 let local_field = bias_i + neighbor_coupling_sum;
107
108 let arg = 2.0 * beta * local_field;
112 let prob_up = 1.0 / (1.0 + (-arg).exp());
113
114 let seed = entropy_seed.clamp(0.0, 0.9999);
115 let new_spin = if seed < prob_up { 1.0 } else { -1.0 };
116
117 let energy = -new_spin * local_field;
119
120 (new_spin, energy)
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_thermal_byte_size() {
129 assert_eq!(ThermalProjection::byte_size(), 20);
130 }
131
132 #[test]
133 fn test_thermal_roundtrip() {
134 let proj = ThermalProjection {
135 spin: -1.0,
136 bias: 0.5,
137 temperature: 2.0,
138 energy: -0.3,
139 magnetization: 0.7,
140 };
141 let mut buf = vec![0u8; ThermalProjection::byte_size()];
142 proj.write(&mut buf);
143 let restored = ThermalProjection::read(&buf);
144 assert!((restored.spin - (-1.0)).abs() < 1e-6);
145 assert!((restored.bias - 0.5).abs() < 1e-6);
146 assert!((restored.temperature - 2.0).abs() < 1e-6);
147 assert!((restored.energy - (-0.3)).abs() < 1e-6);
148 assert!((restored.magnetization - 0.7).abs() < 1e-6);
149 }
150
151 #[test]
152 fn test_thermal_default() {
153 let proj = ThermalProjection::default();
154 assert!((proj.spin - 1.0).abs() < 1e-6);
155 assert!((proj.bias).abs() < 1e-6);
156 assert!((proj.temperature - 1.0).abs() < 1e-6);
157 }
158
159 #[test]
160 fn test_gibbs_step_strong_field_up() {
161 let (spin, energy) = gibbs_step(1.0, 10.0, 0.0, 5.0, 0.5);
163 assert!((spin - 1.0).abs() < 1e-6, "spin = {}", spin);
164 assert!(energy < 0.0, "energy should be negative for aligned spin");
165 }
166
167 #[test]
168 fn test_gibbs_step_strong_field_down() {
169 let (spin, _energy) = gibbs_step(1.0, -10.0, 0.0, 5.0, 0.5);
171 assert!((spin - (-1.0)).abs() < 1e-6, "spin = {}", spin);
172 }
173
174 #[test]
175 fn test_gibbs_step_deterministic() {
176 let a = gibbs_step(1.0, 0.5, 0.3, 2.0, 0.4);
178 let b = gibbs_step(1.0, 0.5, 0.3, 2.0, 0.4);
179 assert!((a.0 - b.0).abs() < 1e-6);
180 assert!((a.1 - b.1).abs() < 1e-6);
181 }
182
183 #[test]
184 fn test_gibbs_step_entropy_clamp() {
185 let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, 0.0);
187 let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, 1.0);
188 let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, -0.5);
189 let _ = gibbs_step(1.0, 0.0, 0.0, 1.0, 1.5);
190 }
191
192 #[test]
193 fn test_shape_hash_changes_with_state() {
194 let a = ThermalProjection {
195 spin: 1.0,
196 ..Default::default()
197 };
198 let b = ThermalProjection {
199 spin: -1.0,
200 ..Default::default()
201 };
202 assert_ne!(a.shape_hash_contribution(), b.shape_hash_contribution());
203 }
204}