Skip to main content

spin_sim/
simulation.rs

1use crate::lattice::Lattice;
2use crate::stats::Statistics;
3use crate::{clusters, energy, sweep, tempering};
4use rand::{Rng, SeedableRng};
5use rand_xoshiro::Xoshiro256StarStar;
6
7/// Mutable state for one disorder realization.
8///
9/// Holds the coupling array (fixed after construction), spin configurations for
10/// every replica at every temperature, and bookkeeping for parallel tempering.
11///
12/// With `n_replicas` replicas and `n_temps` temperatures there are
13/// `n_systems = n_replicas * n_temps` independent spin configurations.
14/// Spins are stored in a single flat `Vec` of length `n_systems * n_spins`,
15/// where system `i` occupies `spins[i*n_spins .. (i+1)*n_spins]`.
16pub struct Realization {
17    /// Forward couplings, length `n_spins * n_dims`.
18    pub couplings: Vec<f32>,
19    /// All spin configurations, length `n_systems * n_spins` (+1/−1).
20    pub spins: Vec<i8>,
21    /// Temperature assigned to each system slot, length `n_systems`.
22    pub temperatures: Vec<f32>,
23    /// Parallel-tempering permutation: `system_ids[slot]` is the system index
24    /// currently occupying temperature slot `slot`.
25    pub system_ids: Vec<usize>,
26    /// One PRNG per system.
27    pub rngs: Vec<Xoshiro256StarStar>,
28    /// Cached total energy per system (E / N), length `n_systems`.
29    pub energies: Vec<f32>,
30    /// Cached per-bond interactions (s_i * s_j * J_ij), length `n_systems * n_spins * n_dims`.
31    pub interactions: Vec<f32>,
32}
33
34impl Realization {
35    /// Initialize a realization with random ±1 spins.
36    ///
37    /// Seeds replica RNGs deterministically as `base_seed, base_seed+1, …`.
38    pub fn new(
39        lattice: &Lattice,
40        couplings: Vec<f32>,
41        temps: &[f32],
42        n_replicas: usize,
43        base_seed: u64,
44    ) -> Self {
45        let n_spins = lattice.n_spins;
46        let n_temps = temps.len();
47        let n_systems = n_replicas * n_temps;
48
49        let temperatures = temps.repeat(n_replicas);
50
51        let mut rngs = Vec::with_capacity(n_systems);
52        for i in 0..n_systems {
53            rngs.push(Xoshiro256StarStar::seed_from_u64(base_seed + i as u64));
54        }
55
56        let mut spins = vec![0i8; n_systems * n_spins];
57        for (i, rng) in rngs.iter_mut().enumerate() {
58            for j in 0..n_spins {
59                spins[i * n_spins + j] = if rng.gen::<f32>() < 0.5 { -1 } else { 1 };
60            }
61        }
62
63        let system_ids: Vec<usize> = (0..n_systems).collect();
64
65        let (energies, interactions) =
66            energy::compute_energies(lattice, &spins, &couplings, n_systems, true);
67        let interactions = interactions.unwrap();
68
69        Self {
70            couplings,
71            spins,
72            temperatures,
73            system_ids,
74            rngs,
75            energies,
76            interactions,
77        }
78    }
79
80    /// Re-randomize all spins and reset the tempering permutation.
81    pub fn reset(&mut self, lattice: &Lattice, n_replicas: usize, n_temps: usize, base_seed: u64) {
82        let n_spins = lattice.n_spins;
83        let n_systems = n_replicas * n_temps;
84
85        for i in 0..n_systems {
86            self.rngs[i] = Xoshiro256StarStar::seed_from_u64(base_seed + i as u64);
87            for j in 0..n_spins {
88                self.spins[i * n_spins + j] = if self.rngs[i].gen::<f32>() < 0.5 {
89                    -1
90                } else {
91                    1
92                };
93            }
94        }
95
96        self.system_ids = (0..n_systems).collect();
97
98        let (energies, interactions) =
99            energy::compute_energies(lattice, &self.spins, &self.couplings, n_systems, true);
100        self.energies = energies;
101        self.interactions = interactions.unwrap();
102    }
103}
104
105/// Per-temperature observables averaged over measurement sweeps and replicas.
106///
107/// All vectors are indexed by temperature index and have length `n_temps`.
108/// Overlap vectors are empty when `n_replicas < 2`.
109pub struct SweepResult {
110    /// ⟨m⟩ — mean magnetization per spin.
111    pub mags: Vec<f64>,
112    /// ⟨m²⟩.
113    pub mags2: Vec<f64>,
114    /// ⟨m⁴⟩.
115    pub mags4: Vec<f64>,
116    /// ⟨E⟩ — mean energy per spin.
117    pub energies: Vec<f64>,
118    /// ⟨E²⟩.
119    pub energies2: Vec<f64>,
120    /// ⟨q⟩ — mean replica overlap.
121    pub overlap: Vec<f64>,
122    /// ⟨q²⟩.
123    pub overlap2: Vec<f64>,
124    /// ⟨q⁴⟩.
125    pub overlap4: Vec<f64>,
126}
127
128/// Run the full Monte Carlo loop (warmup + measurement) for one [`Realization`].
129///
130/// Each sweep consists of:
131/// 1. A full single-spin pass (`sweep_mode`: `"metropolis"` or `"gibbs"`)
132/// 2. An optional cluster update (`cluster_mode`: `"wolff"` or `"sw"`,
133///    every `cluster_update_interval` sweeps)
134/// 3. Measurement (after `warmup_sweeps`)
135/// 4. Optional Houdayer ICM (every `houdayer_interval` sweeps, requires `n_replicas ≥ 2`)
136/// 5. Optional parallel tempering (every `pt_interval` sweeps)
137///
138/// `on_sweep` is called once per sweep (useful for progress bars).
139#[allow(clippy::too_many_arguments)]
140pub fn run_sweep_loop(
141    lattice: &Lattice,
142    real: &mut Realization,
143    n_replicas: usize,
144    n_temps: usize,
145    n_sweeps: usize,
146    warmup_sweeps: usize,
147    sweep_mode: &str,
148    cluster_update_interval: Option<usize>,
149    cluster_mode: &str,
150    pt_interval: Option<usize>,
151    houdayer_interval: Option<usize>,
152    on_sweep: &(dyn Fn() + Sync),
153) -> SweepResult {
154    let n_spins = lattice.n_spins;
155    let n_systems = n_replicas * n_temps;
156
157    let mut mags_stat = Statistics::new(n_temps, 1);
158    let mut mags2_stat = Statistics::new(n_temps, 1);
159    let mut mags4_stat = Statistics::new(n_temps, 1);
160    let mut energies_stat = Statistics::new(n_temps, 1);
161    let mut energies2_stat = Statistics::new(n_temps, 2);
162
163    let n_pairs = n_replicas / 2;
164    let mut overlap_stat = Statistics::new(n_temps, 1);
165    let mut overlap2_stat = Statistics::new(n_temps, 1);
166    let mut overlap4_stat = Statistics::new(n_temps, 1);
167
168    for sweep_id in 0..n_sweeps {
169        on_sweep();
170        let record = sweep_id >= warmup_sweeps;
171
172        match sweep_mode {
173            "metropolis" => sweep::metropolis_sweep(
174                lattice,
175                &mut real.spins,
176                &real.couplings,
177                &real.temperatures,
178                &real.system_ids,
179                &mut real.rngs,
180            ),
181            "gibbs" => sweep::gibbs_sweep(
182                lattice,
183                &mut real.spins,
184                &real.couplings,
185                &real.temperatures,
186                &real.system_ids,
187                &mut real.rngs,
188            ),
189            _ => unreachable!(),
190        }
191
192        let do_cluster = cluster_update_interval.is_some_and(|interval| sweep_id % interval == 0);
193
194        if do_cluster {
195            match cluster_mode {
196                "wolff" => {
197                    clusters::wolff_update(
198                        lattice,
199                        &mut real.spins,
200                        &real.couplings,
201                        &real.temperatures,
202                        &real.system_ids,
203                        &mut real.rngs,
204                    );
205                    (real.energies, _) = energy::compute_energies(
206                        lattice,
207                        &real.spins,
208                        &real.couplings,
209                        n_systems,
210                        false,
211                    );
212                }
213                "sw" => {
214                    let (energies, interactions) = energy::compute_energies(
215                        lattice,
216                        &real.spins,
217                        &real.couplings,
218                        n_systems,
219                        true,
220                    );
221                    real.energies = energies;
222                    real.interactions = interactions.unwrap();
223
224                    clusters::sw_update(
225                        lattice,
226                        &mut real.spins,
227                        &real.interactions,
228                        &real.temperatures,
229                        &real.system_ids,
230                        &mut real.rngs,
231                    );
232
233                    (real.energies, _) = energy::compute_energies(
234                        lattice,
235                        &real.spins,
236                        &real.couplings,
237                        n_systems,
238                        false,
239                    );
240                }
241                _ => unreachable!(),
242            }
243        } else {
244            (real.energies, _) =
245                energy::compute_energies(lattice, &real.spins, &real.couplings, n_systems, false);
246        }
247
248        if record {
249            let mut mags = vec![0.0f32; n_temps];
250            let mut mags2 = vec![0.0f32; n_temps];
251            let mut mags4 = vec![0.0f32; n_temps];
252            let mut energies_ordered = vec![0.0f32; n_temps];
253
254            for r in 0..n_replicas {
255                let offset = r * n_temps;
256                for t in 0..n_temps {
257                    let system_id = real.system_ids[offset + t];
258                    let spin_base = system_id * n_spins;
259                    let mut sum = 0i64;
260                    for j in 0..n_spins {
261                        sum += real.spins[spin_base + j] as i64;
262                    }
263                    let mag = sum as f32 / n_spins as f32;
264                    let m2 = mag * mag;
265                    mags[t] = mag;
266                    mags2[t] = m2;
267                    mags4[t] = m2 * m2;
268                    energies_ordered[t] = real.energies[system_id];
269                }
270
271                mags_stat.update(&mags);
272                mags2_stat.update(&mags2);
273                mags4_stat.update(&mags4);
274                energies_stat.update(&energies_ordered);
275                energies2_stat.update(&energies_ordered);
276            }
277
278            for pair_idx in 0..n_pairs {
279                let r_a = 2 * pair_idx;
280                let r_b = 2 * pair_idx + 1;
281                let mut overlaps = vec![0.0f32; n_temps];
282                let mut overlaps2 = vec![0.0f32; n_temps];
283                let mut overlaps4 = vec![0.0f32; n_temps];
284
285                for t in 0..n_temps {
286                    let sys_a = real.system_ids[r_a * n_temps + t];
287                    let sys_b = real.system_ids[r_b * n_temps + t];
288                    let base_a = sys_a * n_spins;
289                    let base_b = sys_b * n_spins;
290                    let mut dot = 0i64;
291                    for j in 0..n_spins {
292                        dot += (real.spins[base_a + j] as i64) * (real.spins[base_b + j] as i64);
293                    }
294                    let q = dot as f32 / n_spins as f32;
295                    let q2 = q * q;
296                    overlaps[t] = q;
297                    overlaps2[t] = q2;
298                    overlaps4[t] = q2 * q2;
299                }
300
301                overlap_stat.update(&overlaps);
302                overlap2_stat.update(&overlaps2);
303                overlap4_stat.update(&overlaps4);
304            }
305        }
306
307        if let Some(interval) = houdayer_interval {
308            if sweep_id % interval == 0 && n_replicas >= 2 {
309                clusters::houdayer_update(
310                    lattice,
311                    &mut real.spins,
312                    &real.system_ids,
313                    n_replicas,
314                    n_temps,
315                    &mut real.rngs[0],
316                );
317                (real.energies, _) = energy::compute_energies(
318                    lattice,
319                    &real.spins,
320                    &real.couplings,
321                    n_systems,
322                    false,
323                );
324            }
325        }
326
327        if let Some(interval) = pt_interval {
328            if sweep_id % interval == 0 {
329                for r in 0..n_replicas {
330                    let offset = r * n_temps;
331                    let sid_slice = &mut real.system_ids[offset..offset + n_temps];
332                    let temp_slice = &real.temperatures[offset..offset + n_temps];
333                    tempering::parallel_tempering(
334                        &real.energies,
335                        temp_slice,
336                        sid_slice,
337                        n_spins,
338                        &mut real.rngs[offset],
339                    );
340                }
341            }
342        }
343    }
344
345    SweepResult {
346        mags: mags_stat.average(),
347        mags2: mags2_stat.average(),
348        mags4: mags4_stat.average(),
349        energies: energies_stat.average(),
350        energies2: energies2_stat.average(),
351        overlap: if n_pairs > 0 {
352            overlap_stat.average()
353        } else {
354            vec![]
355        },
356        overlap2: if n_pairs > 0 {
357            overlap2_stat.average()
358        } else {
359            vec![]
360        },
361        overlap4: if n_pairs > 0 {
362            overlap4_stat.average()
363        } else {
364            vec![]
365        },
366    }
367}
368
369/// Average [`SweepResult`]s across disorder realizations.
370pub fn aggregate_results(results: &[SweepResult]) -> SweepResult {
371    let n = results.len() as f64;
372    let n_temps = results[0].mags.len();
373    let n_overlap = results[0].overlap.len();
374
375    let mut agg = SweepResult {
376        mags: vec![0.0; n_temps],
377        mags2: vec![0.0; n_temps],
378        mags4: vec![0.0; n_temps],
379        energies: vec![0.0; n_temps],
380        energies2: vec![0.0; n_temps],
381        overlap: vec![0.0; n_overlap],
382        overlap2: vec![0.0; n_overlap],
383        overlap4: vec![0.0; n_overlap],
384    };
385
386    for r in results {
387        for (a, &v) in agg.mags.iter_mut().zip(r.mags.iter()) {
388            *a += v;
389        }
390        for (a, &v) in agg.mags2.iter_mut().zip(r.mags2.iter()) {
391            *a += v;
392        }
393        for (a, &v) in agg.mags4.iter_mut().zip(r.mags4.iter()) {
394            *a += v;
395        }
396        for (a, &v) in agg.energies.iter_mut().zip(r.energies.iter()) {
397            *a += v;
398        }
399        for (a, &v) in agg.energies2.iter_mut().zip(r.energies2.iter()) {
400            *a += v;
401        }
402        for (a, &v) in agg.overlap.iter_mut().zip(r.overlap.iter()) {
403            *a += v;
404        }
405        for (a, &v) in agg.overlap2.iter_mut().zip(r.overlap2.iter()) {
406            *a += v;
407        }
408        for (a, &v) in agg.overlap4.iter_mut().zip(r.overlap4.iter()) {
409            *a += v;
410        }
411    }
412
413    for v in agg
414        .mags
415        .iter_mut()
416        .chain(agg.mags2.iter_mut())
417        .chain(agg.mags4.iter_mut())
418        .chain(agg.energies.iter_mut())
419        .chain(agg.energies2.iter_mut())
420        .chain(agg.overlap.iter_mut())
421        .chain(agg.overlap2.iter_mut())
422        .chain(agg.overlap4.iter_mut())
423    {
424        *v /= n;
425    }
426
427    agg
428}