Skip to main content

spin_sim/simulation/
realization.rs

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/// Position in the interval schedule, carried across sampling calls so that an
23/// event due every `k` sweeps stays every `k` sweeps however the run is split.
24#[derive(Clone, Copy, Debug, Default)]
25pub(crate) struct ScheduleState {
26    pub sweeps: usize,
27    /// Overlap events so far, which selects the next of several build modes.
28    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    /// The end of the ladder each system visited last: `NO_END` until it reaches one.
37    last_end: Vec<u8>,
38    /// Per temperature slot, visits by systems that last visited the cold end.
39    flow_up: Vec<u64>,
40    /// Per temperature slot, visits by systems that have visited either end.
41    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    /// Counts the systems of one replica row by the end they last visited. The
139    /// fraction `flow_up / flow_labelled` falls from 1 at the cold end to 0 at the
140    /// hot end, linearly along a well-spaced ladder and steeply across a bottleneck
141    /// (Katzgraber, Trebst, Huse & Troyer, J. Stat. Mech. P03018 (2006)).
142    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
170/// Mutable state for one disorder realization.
171///
172/// Holds the coupling array (fixed after construction), spin configurations for
173/// every replica at every temperature, and bookkeeping for parallel tempering.
174///
175/// With `n_replicas` replicas and `n_temps` temperatures there are
176/// `n_systems = n_replicas * n_temps` independent spin configurations.
177/// Spins are stored in a single flat `Vec` of length `n_systems * n_spins`,
178/// where system `i` occupies `spins[i*n_spins .. (i+1)*n_spins]`.
179pub struct ModelRealization<S: Spin> {
180    /// Forward couplings, length `n_spins * n_neighbors`.
181    pub couplings: Vec<S::Value>,
182    /// All spin configurations, length `n_systems * n_spins` (±1 for Ising, unit vectors for XY).
183    pub spins: Vec<S>,
184    /// Temperature assigned to each system slot, length `n_systems`.
185    pub temperatures: Vec<S::Value>,
186    /// Parallel-tempering permutation: `system_ids[slot]` is the system index
187    /// currently occupying temperature slot `slot`.
188    pub system_ids: Vec<usize>,
189    /// One PRNG per system.
190    pub rngs: Vec<Xoshiro256StarStar>,
191    /// One PRNG per overlap-update pair slot, length `n_temps * (n_replicas / 2)`.
192    pub pair_rngs: Vec<Xoshiro256StarStar>,
193    pub(super) pt: PtState,
194    pub(crate) schedule: ScheduleState,
195    /// Scratch cache of the interaction energy per system (-H/N), length
196    /// `n_systems`. It is current only right after a measurement or tempering step.
197    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    /// Initialize a realization with random spins (±1, or uniform unit vectors for XY).
205    ///
206    /// Seeds independent system and pair streams from domain-separated child seeds.
207    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    /// Re-randomize all spins and reset the tempering permutation.
266    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    /// Validate externally mutable buffers before entering parallel kernels.
294    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        // Kernels divide by temperature in the model's precision, so 1/T must be finite too.
342        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    /// Swap attempts per temperature edge since creation or the last reset. Like the
389    /// acceptances and round trips, they count warmup sweeps too.
390    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    /// Per temperature slot, visits after each tempering event by systems that last
403    /// visited the cold end of the ladder.
404    pub fn pt_flow_up(&self) -> &[u64] {
405        &self.pt.flow_up
406    }
407
408    /// Per temperature slot, visits by systems that have visited either end; the
409    /// denominator of the flow fraction `pt_flow_up / pt_flow_labelled`.
410    pub fn pt_flow_labelled(&self) -> &[u64] {
411        &self.pt.flow_labelled
412    }
413}
414
415/// Stable disorder stream mapping shared by the Rust and Python interfaces.
416pub 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    /// With every swap accepted, the full ladder must move walkers across it; a
451    /// schedule whose consecutive passes undo each other completes no round trips.
452    #[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        // Whoever sits at an end has just visited it.
465        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}