1use crate::config::PtSchedule;
2use crate::geometry::Lattice;
3use crate::spins::model::{Real, Spin};
4use rand::SeedableRng;
5use rand_xoshiro::Xoshiro256StarStar;
6
7const SYSTEM_SEED_DOMAIN: u64 = 0x53A9_17E1_4C2D_8B6F;
8const PAIR_SEED_DOMAIN: u64 = 0xA147_5EED_91C3_02D7;
9
10fn splitmix64(mut value: u64) -> u64 {
11 value = value.wrapping_add(0x9E37_79B9_7F4A_7C15);
12 let mut mixed = value;
13 mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
14 mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
15 mixed ^ (mixed >> 31)
16}
17
18fn child_seed(root: u64, domain: u64, index: usize) -> u64 {
19 splitmix64(root ^ domain ^ splitmix64(index as u64))
20}
21
22#[derive(Clone, Copy, Debug, Default)]
25pub(crate) struct ScheduleState {
26 pub sweeps: usize,
27 pub overlap_calls: usize,
29}
30
31pub(super) struct PtState {
32 edge_attempts: Vec<u64>,
33 edge_acceptances: Vec<u64>,
34 round_trips: Vec<u64>,
35 trip_state: Vec<u8>,
36 last_end: Vec<u8>,
38 flow_up: Vec<u64>,
40 flow_labelled: Vec<u64>,
42 cold_slot: usize,
43 hot_slot: usize,
44}
45
46const NO_END: u8 = 0;
47const COLD_END: u8 = 1;
48const HOT_END: u8 = 2;
49
50impl PtState {
51 fn new<T: PartialOrd>(
52 n_replicas: usize,
53 n_temps: usize,
54 system_ids: &[usize],
55 temperatures: &[T],
56 ) -> Self {
57 let mut state = Self {
58 edge_attempts: Vec::new(),
59 edge_acceptances: Vec::new(),
60 round_trips: Vec::new(),
61 trip_state: Vec::new(),
62 last_end: Vec::new(),
63 flow_up: Vec::new(),
64 flow_labelled: Vec::new(),
65 cold_slot: 0,
66 hot_slot: 0,
67 };
68 state.reset(n_replicas, n_temps, system_ids, temperatures);
69 state
70 }
71
72 fn reset<T: PartialOrd>(
73 &mut self,
74 n_replicas: usize,
75 n_temps: usize,
76 system_ids: &[usize],
77 temperatures: &[T],
78 ) {
79 let n_edges = n_temps.saturating_sub(1);
80 self.edge_attempts.resize(n_edges, 0);
81 self.edge_attempts.fill(0);
82 self.edge_acceptances.resize(n_edges, 0);
83 self.edge_acceptances.fill(0);
84 self.round_trips.resize(n_replicas * n_temps, 0);
85 self.round_trips.fill(0);
86 self.trip_state.resize(n_replicas * n_temps, 0);
87 self.trip_state.fill(0);
88 self.last_end.resize(n_replicas * n_temps, NO_END);
89 self.last_end.fill(NO_END);
90 self.flow_up.resize(n_temps, 0);
91 self.flow_up.fill(0);
92 self.flow_labelled.resize(n_temps, 0);
93 self.flow_labelled.fill(0);
94 (self.cold_slot, self.hot_slot) = Self::extreme_temperature_slots(temperatures, n_temps);
95
96 if n_temps == 0 {
97 return;
98 }
99 for replica in 0..n_replicas {
100 let row = replica * n_temps;
101 self.last_end[system_ids[row + self.cold_slot]] = COLD_END;
102 self.last_end[system_ids[row + self.hot_slot]] = HOT_END;
103 self.trip_state[system_ids[row + self.hot_slot]] = 1;
104 }
105 }
106
107 pub(super) fn record_attempt(&mut self, attempt: crate::mcmc::tempering::TemperingAttempt) {
108 self.edge_attempts[attempt.edge] += 1;
109 if !attempt.accepted {
110 return;
111 }
112 self.edge_acceptances[attempt.edge] += 1;
113
114 self.record_arrival(attempt.left_system, attempt.edge + 1);
115 self.record_arrival(attempt.right_system, attempt.edge);
116 }
117
118 fn extreme_temperature_slots<T: PartialOrd>(
119 temperatures: &[T],
120 n_temps: usize,
121 ) -> (usize, usize) {
122 if n_temps == 0 {
123 return (0, 0);
124 }
125 let mut cold_slot = 0;
126 let mut hot_slot = 0;
127 for slot in 1..n_temps {
128 if temperatures[slot] < temperatures[cold_slot] {
129 cold_slot = slot;
130 }
131 if temperatures[slot] > temperatures[hot_slot] {
132 hot_slot = slot;
133 }
134 }
135 (cold_slot, hot_slot)
136 }
137
138 fn record_visits(&mut self, row: &[usize]) {
143 for (slot, &system) in row.iter().enumerate() {
144 let end = self.last_end[system];
145 self.flow_labelled[slot] += u64::from(end != NO_END);
146 self.flow_up[slot] += u64::from(end == COLD_END);
147 }
148 }
149
150 fn record_arrival(&mut self, system: usize, slot: usize) {
151 if slot == self.cold_slot {
152 self.last_end[system] = COLD_END;
153 }
154 if slot == self.hot_slot {
155 self.last_end[system] = HOT_END;
156 }
157 if slot == self.hot_slot {
158 if self.trip_state[system] == 2 {
159 self.round_trips[system] += 1;
160 }
161 self.trip_state[system] = 1;
162 return;
163 }
164 if slot == self.cold_slot && self.trip_state[system] == 1 {
165 self.trip_state[system] = 2;
166 }
167 }
168}
169
170pub struct ModelRealization<S: Spin> {
180 pub couplings: Vec<S::Value>,
182 pub spins: Vec<S>,
184 pub temperatures: Vec<S::Value>,
186 pub system_ids: Vec<usize>,
189 pub rngs: Vec<Xoshiro256StarStar>,
191 pub pair_rngs: Vec<Xoshiro256StarStar>,
193 pub(super) pt: PtState,
194 pub(crate) schedule: ScheduleState,
195 pub energies: Vec<S::Value>,
198}
199
200pub type Realization = ModelRealization<i8>;
201pub type XyRealization = ModelRealization<[f64; 2]>;
202
203impl<S: Spin> ModelRealization<S> {
204 pub fn new(
208 lattice: &Lattice,
209 couplings: Vec<S::Value>,
210 temps: &[S::Value],
211 n_replicas: usize,
212 base_seed: u64,
213 ) -> Self {
214 let n_spins = lattice.n_spins;
215 let n_temps = temps.len();
216 let n_systems = n_replicas * n_temps;
217
218 let temperatures = temps.repeat(n_replicas);
219
220 let mut rngs = Vec::with_capacity(n_systems);
221 for i in 0..n_systems {
222 rngs.push(Xoshiro256StarStar::seed_from_u64(child_seed(
223 base_seed,
224 SYSTEM_SEED_DOMAIN,
225 i,
226 )));
227 }
228
229 let mut spins = vec![S::default(); n_systems * n_spins];
230 for (i, rng) in rngs.iter_mut().enumerate() {
231 for j in 0..n_spins {
232 spins[i * n_spins + j] = S::random(rng);
233 }
234 }
235
236 let system_ids: Vec<usize> = (0..n_systems).collect();
237
238 let n_pairs = n_replicas / 2;
239 let mut pair_rngs = Vec::with_capacity(n_temps * n_pairs);
240 for i in 0..n_temps * n_pairs {
241 pair_rngs.push(Xoshiro256StarStar::seed_from_u64(child_seed(
242 base_seed,
243 PAIR_SEED_DOMAIN,
244 i,
245 )));
246 }
247
248 let mut energies = vec![S::Value::default(); n_systems];
249 S::interactions(lattice, &spins, &couplings, &mut energies);
250
251 let pt = PtState::new(n_replicas, n_temps, &system_ids, &temperatures);
252 Self {
253 couplings,
254 spins,
255 temperatures,
256 system_ids,
257 rngs,
258 pair_rngs,
259 pt,
260 schedule: ScheduleState::default(),
261 energies,
262 }
263 }
264
265 pub fn reset(&mut self, lattice: &Lattice, n_replicas: usize, n_temps: usize, base_seed: u64) {
267 let n_spins = lattice.n_spins;
268 let n_systems = n_replicas * n_temps;
269
270 for i in 0..n_systems {
271 self.rngs[i] =
272 Xoshiro256StarStar::seed_from_u64(child_seed(base_seed, SYSTEM_SEED_DOMAIN, i));
273 for j in 0..n_spins {
274 self.spins[i * n_spins + j] = S::random(&mut self.rngs[i]);
275 }
276 }
277
278 self.system_ids = (0..n_systems).collect();
279
280 let n_pairs = n_replicas / 2;
281 for i in 0..n_temps * n_pairs {
282 self.pair_rngs[i] =
283 Xoshiro256StarStar::seed_from_u64(child_seed(base_seed, PAIR_SEED_DOMAIN, i));
284 }
285
286 self.energies.resize(n_systems, S::Value::default());
287 S::interactions(lattice, &self.spins, &self.couplings, &mut self.energies);
288 self.pt
289 .reset(n_replicas, n_temps, &self.system_ids, &self.temperatures);
290 self.schedule = ScheduleState::default();
291 }
292
293 pub fn validate(
295 &self,
296 lattice: &Lattice,
297 n_replicas: usize,
298 n_temps: usize,
299 ) -> Result<(), String> {
300 if lattice.n_spins == 0 {
301 return Err("lattice has no sites".into());
302 }
303 if lattice.has_self_neighbors() {
304 return Err(
305 "a neighbor offset maps sites to themselves (lattice extent 1 along it)".into(),
306 );
307 }
308 let n = n_replicas
309 .checked_mul(n_temps)
310 .ok_or("system count overflow")?;
311 if n == 0
312 || self.spins.len()
313 != n.checked_mul(lattice.n_spins)
314 .ok_or("spin count overflow")?
315 || self.rngs.len() != n
316 || self.system_ids.len() != n
317 || self.temperatures.len() != n
318 || self.energies.len() != n
319 || self.couplings.len() != lattice.n_spins * lattice.n_neighbors
320 || self.pair_rngs.len() != n_temps * (n_replicas / 2)
321 {
322 return Err("invalid realization buffer dimensions".into());
323 }
324 if self
325 .temperatures
326 .chunks_exact(n_temps)
327 .any(|ladder| ladder != &self.temperatures[..n_temps])
328 {
329 return Err("temperature ladders must agree across replicas".into());
330 }
331 let mut seen = vec![false; n];
332 for (slot, &id) in self.system_ids.iter().enumerate() {
333 if id >= n || seen[id] || id / n_temps != slot / n_temps {
334 return Err("invalid temperature permutation".into());
335 }
336 seen[id] = true;
337 }
338 if self.couplings.iter().any(|x| !x.to_f64().is_finite()) {
339 return Err("couplings must be finite".into());
340 }
341 if self.temperatures.iter().any(|&x| {
343 let inverse = S::Value::from_f64(1.0).to_f64() / x.to_f64();
344 !x.to_f64().is_finite()
345 || x.to_f64() <= 0.0
346 || !S::Value::from_f64(inverse).to_f64().is_finite()
347 }) {
348 return Err("temperatures must be positive and finite with a finite inverse".into());
349 }
350 Ok(())
351 }
352
353 pub(crate) fn temper(
354 &mut self,
355 n_spins: usize,
356 n_replicas: usize,
357 n_temps: usize,
358 schedule: PtSchedule,
359 ) {
360 for replica in 0..n_replicas {
361 let offset = replica * n_temps;
362 let ids = &mut self.system_ids[offset..offset + n_temps];
363 let temps = &self.temperatures[offset..offset + n_temps];
364 let mut record = |attempt| self.pt.record_attempt(attempt);
365 match schedule {
366 PtSchedule::SingleRandomEdge => crate::mcmc::tempering::parallel_tempering(
367 &self.energies,
368 temps,
369 ids,
370 n_spins,
371 &mut self.rngs[offset],
372 &mut record,
373 ),
374 PtSchedule::FullLadder => crate::mcmc::tempering::parallel_tempering_full_ladder(
375 &self.energies,
376 temps,
377 ids,
378 n_spins,
379 &mut self.rngs[offset],
380 &mut record,
381 ),
382 }
383 self.pt
384 .record_visits(&self.system_ids[offset..offset + n_temps]);
385 }
386 }
387
388 pub fn pt_edge_attempts(&self) -> &[u64] {
391 &self.pt.edge_attempts
392 }
393
394 pub fn pt_edge_acceptances(&self) -> &[u64] {
395 &self.pt.edge_acceptances
396 }
397
398 pub fn pt_round_trips(&self) -> &[u64] {
399 &self.pt.round_trips
400 }
401
402 pub fn pt_flow_up(&self) -> &[u64] {
405 &self.pt.flow_up
406 }
407
408 pub fn pt_flow_labelled(&self) -> &[u64] {
411 &self.pt.flow_labelled
412 }
413}
414
415pub fn realization_seed(root: u64, realization: usize) -> u64 {
417 splitmix64(root ^ splitmix64(realization as u64))
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::mcmc::tempering::TemperingAttempt;
424
425 #[test]
426 fn rejects_lattices_with_self_neighbors() {
427 let lattice = Lattice::new(vec![4, 1]);
428 let couplings = vec![1.0; lattice.n_spins * lattice.n_neighbors];
429 let realization = Realization::new(&lattice, couplings, &[1.0], 1, 0);
430 let error = realization.validate(&lattice, 1, 1).unwrap_err();
431 assert!(error.contains("themselves"), "{error}");
432 }
433
434 #[test]
435 fn reset_replays_spins_and_pt_state() {
436 let lattice = Lattice::new(vec![3, 3]);
437 let couplings = vec![1.0; lattice.n_spins * lattice.n_neighbors];
438 let mut realization = Realization::new(&lattice, couplings, &[1.0, 2.0], 2, 17);
439 let initial_spins = realization.spins.clone();
440 realization.system_ids.swap(0, 1);
441 realization.pt.edge_attempts.fill(9);
442
443 realization.reset(&lattice, 2, 2, 17);
444
445 assert_eq!(realization.spins, initial_spins);
446 assert_eq!(realization.system_ids, vec![0, 1, 2, 3]);
447 assert_eq!(realization.pt_edge_attempts(), &[0]);
448 }
449
450 #[test]
453 fn full_ladder_moves_walkers_across_the_ladder() {
454 let lattice = Lattice::new(vec![2, 2]);
455 let couplings = vec![1.0; lattice.n_spins * lattice.n_neighbors];
456 let temps = [1.0, 2.0, 3.0, 4.0, 5.0];
457 let mut realization = Realization::new(&lattice, couplings, &temps, 1, 3);
458 realization.energies.fill(0.0);
459 for _ in 0..40 {
460 realization.temper(lattice.n_spins, 1, temps.len(), PtSchedule::FullLadder);
461 }
462 assert!(realization.pt_edge_acceptances().iter().all(|&a| a == 40));
463 assert!(realization.pt_round_trips().iter().sum::<u64>() > 0);
464 let (up, labelled) = (realization.pt_flow_up(), realization.pt_flow_labelled());
466 assert_eq!((up[0], up[4]), (labelled[0], 0));
467 assert_eq!((labelled[0], labelled[4]), (40, 40));
468 assert!(up.iter().zip(labelled).all(|(up, labelled)| up <= labelled));
469 }
470
471 #[test]
472 fn tracks_hot_cold_hot_across_attempts() {
473 let lattice = Lattice::new(vec![2, 2]);
474 let couplings = vec![1.0; lattice.n_spins * lattice.n_neighbors];
475 let mut realization = Realization::new(&lattice, couplings, &[0.5, 1.0, 2.0], 1, 7);
476
477 for (edge, left_system, right_system) in [(1, 1, 2), (0, 0, 2), (0, 2, 0), (1, 2, 1)] {
478 realization.pt.record_attempt(TemperingAttempt {
479 edge,
480 accepted: true,
481 left_system,
482 right_system,
483 });
484 }
485
486 assert_eq!(realization.pt_edge_attempts(), &[2, 2]);
487 assert_eq!(realization.pt_edge_acceptances(), &[2, 2]);
488 assert_eq!(realization.pt_round_trips()[2], 1);
489 }
490}