Skip to main content

spin_sim/statistics/
results.rs

1use super::equilibration::EquilCheckpoint;
2use super::overlap::OverlapStats;
3
4pub struct ClusterSnapshot {
5    pub sweep_id: usize,
6    pub mode_idx: usize,
7    pub cluster_ids: Vec<Vec<u32>>,
8    pub blue_ids: Option<Vec<Vec<u32>>>,
9    pub spins: Vec<[Vec<i8>; 2]>,
10    pub system_ids: Vec<[usize; 2]>,
11}
12
13/// Running totals of one overlap move's group updates at one temperature.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub struct OverlapMoveCounts {
16    /// Group updates (one per replica group and call).
17    pub groups: u64,
18    /// Sites the updates acted on, summed over groups. A move that touches more than
19    /// half of the active sites mostly relabels replicas.
20    pub moved_sites: u64,
21    /// Groups whose active sites were counted: all but the Wolff depth-first search,
22    /// which never looks at every site.
23    pub counted_groups: u64,
24    /// Active sites summed over the counted groups. For two replicas they give the
25    /// grouped pair's overlap, `q = 1 - 2 active / N`.
26    pub active_sites: u64,
27}
28
29impl OverlapMoveCounts {
30    fn add(&mut self, other: &Self) {
31        self.groups += other.groups;
32        self.moved_sites += other.moved_sites;
33        self.counted_groups += other.counted_groups;
34        self.active_sites += other.active_sites;
35    }
36}
37
38/// Running totals of replica Monte Carlo at one temperature edge `(t, t + 1)`.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub struct RmcEdgeCounts {
41    /// τ-domains proposed for a joint flip.
42    pub proposed: u64,
43    /// Proposed domains whose flip was accepted.
44    pub accepted: u64,
45    /// Sites in the accepted domains.
46    pub flipped_sites: u64,
47}
48
49impl RmcEdgeCounts {
50    fn add(&mut self, other: &Self) {
51        self.proposed += other.proposed;
52        self.accepted += other.accepted;
53        self.flipped_sites += other.flipped_sites;
54    }
55}
56
57/// Sums per-slot counters into per-group ones; `group_of` maps a slot to its group.
58pub(crate) fn fold_counts<T: Copy + Default>(
59    slots: &[T],
60    n_groups: usize,
61    group_of: impl Fn(usize) -> usize,
62    add: impl Fn(&mut T, &T),
63) -> Vec<T> {
64    let mut folded = vec![T::default(); n_groups];
65    for (slot, counts) in slots.iter().enumerate() {
66        add(&mut folded[group_of(slot)], counts);
67    }
68    folded
69}
70
71pub(crate) fn add_move_counts(total: &mut OverlapMoveCounts, part: &OverlapMoveCounts) {
72    total.add(part);
73}
74
75pub(crate) fn add_rmc_counts(total: &mut RmcEdgeCounts, part: &RmcEdgeCounts) {
76    total.add(part);
77}
78
79pub struct ClusterStats {
80    /// FK cluster size histogram per temperature: `hist[s]` = count of size-`s` clusters.
81    pub fk_csd: Vec<Vec<u64>>,
82    /// Per-mode overlap cluster size histogram: `[n_modes][n_temps][n_spins+1]`.
83    /// Counts clusters of the sites a move acts on; the sites it leaves alone are
84    /// not size-1 clusters. CMR acts on every site.
85    pub overlap_csd: Vec<Vec<Vec<u64>>>,
86    /// Per-mode average relative size of k-th largest overlap cluster: `[n_modes][n_temps][4]`.
87    /// Zero for a mode that never ran during measurement, and where fewer than `k`
88    /// clusters exist.
89    pub top_cluster_sizes: Vec<Vec<[f64; 4]>>,
90    /// Recorded FK cluster updates per temperature, summed over replicas. Empty
91    /// unless FK clusters update spins.
92    pub fk_updates: Vec<u64>,
93    /// Spins those updates visited: the flipped cluster for Wolff, every spin for
94    /// Swendsen-Wang. For a ferromagnet the mean Wolff cluster size
95    /// `fk_visited_spins / fk_updates` estimates `N ⟨m²⟩`, and for any model it
96    /// converts autocorrelation times from cluster updates to sweeps.
97    pub fk_visited_spins: Vec<u64>,
98    /// Per-mode move counters of the recorded overlap updates, `[n_modes][n_temps]`.
99    /// Empty for a mode that publishes none (replica Monte Carlo).
100    pub overlap_moves: Vec<Vec<OverlapMoveCounts>>,
101    /// Replica Monte Carlo counters of the recorded updates per temperature edge,
102    /// `[n_temps - 1]`; empty without an `rmc` mode.
103    pub rmc_edges: Vec<RmcEdgeCounts>,
104}
105
106#[derive(Clone, Debug)]
107pub struct GraphObservationSummary {
108    pub observation_count: Vec<u64>,
109    pub cluster_size_counts: Vec<Vec<u64>>,
110    pub top_four_component_fractions: Vec<[f64; 4]>,
111    pub active_bond_density: Vec<f64>,
112    pub large_component_count: Vec<f64>,
113    /// On a canonical square lattice, the probabilities that some cluster wraps
114    /// along dimension 0, along dimension 1, along either, and that one cluster
115    /// wraps along both.
116    pub winding: Option<Vec<[f64; 4]>>,
117    /// Probability that some cluster wraps the torus along each lattice dimension,
118    /// `[n_temps][n_dims]`. Like the other wrapping fields, `None` unless the lattice
119    /// is a canonical square or cubic one.
120    pub wrapping_axes: Option<Vec<Vec<f64>>>,
121    /// Probability that some cluster wraps along at least one dimension.
122    pub wrapping_any: Option<Vec<f64>>,
123    /// Probability that a single cluster wraps along every dimension.
124    pub wrapping_all: Option<Vec<f64>>,
125    /// Mean number of wrapping clusters; values near two mark two giant clusters.
126    pub wrapping_cluster_count: Option<Vec<f64>>,
127    /// Mean fraction of sites in wrapping clusters, the percolation order parameter.
128    pub wrapping_fraction: Option<Vec<f64>>,
129    /// `⟨S₂⟩`, `⟨S₄⟩` and `⟨S₂²⟩` with `S_k = Σ_C |C|^k / N^k` per observed graph. For
130    /// FK clusters of an unfrustrated model, `⟨m²⟩ = ⟨S₂⟩` and
131    /// `⟨m⁴⟩ = 3⟨S₂²⟩ - 2⟨S₄⟩` are the improved estimators.
132    pub cluster_s2: Vec<f64>,
133    pub cluster_s4: Vec<f64>,
134    pub cluster_s2_squared: Vec<f64>,
135}
136
137#[derive(Clone, Debug, Default)]
138pub struct ClusterObservations {
139    pub fk: Option<GraphObservationSummary>,
140    pub houdayer: Option<GraphObservationSummary>,
141    pub jorg: Option<GraphObservationSummary>,
142    pub cmr_blue: Option<GraphObservationSummary>,
143}
144
145pub struct Diagnostics {
146    /// Integrated autocorrelation time τ_int(m²) per temperature, in measured sweeps.
147    /// Empty if autocorrelation_max_lag is None. A series without variance (frozen,
148    /// or never measured) has no autocorrelation time and reports NaN; after
149    /// aggregation this is the mean over the disorder samples with a defined τ.
150    pub mags2_tau: Vec<f64>,
151    /// τ_int(q²) per temperature, with q² averaged over all replica pairs.
152    /// Empty if autocorrelation_max_lag is None or n_replicas < 2.
153    pub overlap2_tau: Vec<f64>,
154    /// τ_int of the replica-averaged energy per temperature.
155    /// Empty if autocorrelation_max_lag is None.
156    pub energy_tau: Vec<f64>,
157    /// Whether the Sokal window of each τ closed within the estimated lags. A τ
158    /// whose window did not close is truncated and is a lower bound. After
159    /// aggregation: whether it closed in every disorder sample.
160    pub mags2_tau_converged: Vec<bool>,
161    pub overlap2_tau_converged: Vec<bool>,
162    pub energy_tau_converged: Vec<bool>,
163    /// Largest lag that was estimated: the requested lag capped at a quarter of the
164    /// measured sweeps. `None` if autocorrelation_max_lag is None.
165    pub autocorrelation_lag: Option<usize>,
166    /// Equilibration diagnostic checkpoints: energy and link overlap averaged over
167    /// the log-binned window of each checkpoint (see [`EquilCheckpoint`]).
168    /// Empty if equilibration_diagnostic is false.
169    pub equil_checkpoints: Vec<EquilCheckpoint>,
170}
171
172impl Diagnostics {
173    fn empty() -> Self {
174        Self {
175            mags2_tau: vec![],
176            overlap2_tau: vec![],
177            energy_tau: vec![],
178            mags2_tau_converged: vec![],
179            overlap2_tau_converged: vec![],
180            energy_tau_converged: vec![],
181            autocorrelation_lag: None,
182            equil_checkpoints: vec![],
183        }
184    }
185}
186
187/// Thermal averages of one disorder realization, per temperature.
188///
189/// The disorder mean alone carries no error bar; these per-sample values give the
190/// standard error of any disorder average and feed jackknife or bootstrap errors of
191/// ratios such as the Binder parameter. Overlap vectors are empty when
192/// `n_replicas < 2`, and τ vectors when autocorrelation is off.
193#[derive(Clone, Debug, Default)]
194pub struct DisorderSample {
195    pub mags2: Vec<f64>,
196    pub mags4: Vec<f64>,
197    pub energies: Vec<f64>,
198    pub energy_variance: Vec<f64>,
199    pub overlap2: Vec<f64>,
200    pub overlap4: Vec<f64>,
201    pub link_overlap: Vec<f64>,
202    pub link_overlap2: Vec<f64>,
203    pub link_overlap4: Vec<f64>,
204    pub mags2_tau: Vec<f64>,
205    pub overlap2_tau: Vec<f64>,
206    pub energy_tau: Vec<f64>,
207    pub mags2_tau_converged: Vec<bool>,
208    pub overlap2_tau_converged: Vec<bool>,
209    pub energy_tau_converged: Vec<bool>,
210}
211
212/// Per-temperature observables averaged over measurement sweeps and replicas.
213///
214/// All vectors are indexed by temperature index and have length `n_temps`.
215/// Overlap vectors are empty when `n_replicas < 2`. After
216/// [`SweepResult::aggregate`] every moment is additionally averaged over disorder.
217pub struct SweepResult {
218    /// ⟨m⟩ — mean magnetization per spin.
219    pub mags: Vec<f64>,
220    /// ⟨m²⟩.
221    pub mags2: Vec<f64>,
222    /// ⟨m⁴⟩.
223    pub mags4: Vec<f64>,
224    /// ⟨e⟩ with `e = -H/N = Σ_⟨ij⟩ J_ij s_i s_j / N`, the interaction sum per spin.
225    /// This is minus the physical energy per spin; the opt-in physics collector
226    /// ([`super::physics::PhysicsResult`]) reports `+H/N` instead.
227    pub energies: Vec<f64>,
228    /// ⟨e²⟩.
229    pub energies2: Vec<f64>,
230    /// Thermal variance `⟨e²⟩ - ⟨e⟩²` of one disorder sample; after aggregation, its
231    /// disorder average. The heat capacity per spin is `N · energy_variance / T²`.
232    /// Unlike `energies2 - energies²` after aggregation, it excludes the
233    /// disorder variance of `⟨e⟩`.
234    pub energy_variance: Vec<f64>,
235    pub overlap_stats: OverlapStats,
236    pub cluster_stats: ClusterStats,
237    pub per_disorder_physics: Vec<super::physics::PhysicsResult>,
238    pub per_disorder_cluster_observations: Vec<ClusterObservations>,
239    /// One entry per disorder realization, in order.
240    pub per_disorder: Vec<DisorderSample>,
241    pub diagnostics: Diagnostics,
242    pub cluster_snapshots: Vec<ClusterSnapshot>,
243}
244
245/// Element-wise mean over `results` of the vector that `field` selects, with the
246/// length of the first.
247fn mean_over<'a, R>(results: &'a [R], field: impl Fn(&'a R) -> &'a [f64]) -> Vec<f64> {
248    let mut mean = vec![0.0; results.first().map_or(0, |first| field(first).len())];
249    for result in results {
250        for (total, &value) in mean.iter_mut().zip(field(result)) {
251            *total += value;
252        }
253    }
254    let n = results.len() as f64;
255    for value in &mut mean {
256        *value /= n;
257    }
258    mean
259}
260
261/// [`mean_over`] that skips NaN entries; NaN where every entry is NaN.
262fn defined_mean_over<'a, R>(results: &'a [R], field: impl Fn(&'a R) -> &'a [f64]) -> Vec<f64> {
263    let len = results.first().map_or(0, |first| field(first).len());
264    (0..len)
265        .map(|index| {
266            let defined = results
267                .iter()
268                .filter_map(|result| field(result).get(index).copied())
269                .filter(|value| !value.is_nan());
270            let (sum, count) = defined.fold((0.0, 0usize), |(sum, count), value| {
271                (sum + value, count + 1)
272            });
273            sum / count as f64
274        })
275        .collect()
276}
277
278/// Element-wise sum over `results` of the counters that `field` selects.
279fn sum_over<'a, R>(results: &'a [R], field: impl Fn(&'a R) -> &'a [u64]) -> Vec<u64> {
280    let mut total = vec![0; results.first().map_or(0, |first| field(first).len())];
281    for result in results {
282        for (total, &count) in total.iter_mut().zip(field(result)) {
283            *total += count;
284        }
285    }
286    total
287}
288
289/// Element-wise conjunction over `results`.
290fn all_over<'a, R>(results: &'a [R], field: impl Fn(&'a R) -> &'a [bool]) -> Vec<bool> {
291    let len = results.first().map_or(0, |first| field(first).len());
292    (0..len)
293        .map(|index| results.iter().all(|result| field(result)[index]))
294        .collect()
295}
296
297impl SweepResult {
298    /// A result with no temperatures, returned when aggregating nothing.
299    pub fn empty() -> Self {
300        Self {
301            mags: vec![],
302            mags2: vec![],
303            mags4: vec![],
304            energies: vec![],
305            energies2: vec![],
306            energy_variance: vec![],
307            overlap_stats: OverlapStats::empty(),
308            cluster_stats: ClusterStats {
309                fk_csd: vec![],
310                overlap_csd: vec![],
311                top_cluster_sizes: vec![],
312                fk_updates: vec![],
313                fk_visited_spins: vec![],
314                overlap_moves: vec![],
315                rmc_edges: vec![],
316            },
317            per_disorder_physics: vec![],
318            per_disorder_cluster_observations: vec![],
319            per_disorder: vec![],
320            diagnostics: Diagnostics::empty(),
321            cluster_snapshots: vec![],
322        }
323    }
324
325    /// Average [`SweepResult`]s across disorder realizations with equal weights.
326    ///
327    /// Thermal variances (`energy_variance`) are averaged per sample, so they do not
328    /// pick up disorder fluctuations. An empty slice yields [`SweepResult::empty`].
329    pub fn aggregate(results: &[Self]) -> Self {
330        Self::aggregate_impl(results, true)
331    }
332
333    pub(crate) fn aggregate_without_overlap_samples(results: &[Self]) -> Self {
334        Self::aggregate_impl(results, false)
335    }
336
337    fn aggregate_impl(results: &[Self], retain_overlap_samples: bool) -> Self {
338        if results.is_empty() {
339            return Self::empty();
340        }
341        let n = results.len() as f64;
342        let n_temps = results[0].mags.len();
343        let first = &results[0].cluster_stats;
344
345        let mut fk_csd = first.fk_csd.clone();
346        for histogram in &mut fk_csd {
347            histogram.fill(0);
348        }
349        let mut overlap_csd = first.overlap_csd.clone();
350        for histogram in overlap_csd.iter_mut().flatten() {
351            histogram.fill(0);
352        }
353        // A mode that never measured in one sample reports no temperatures there.
354        let mut top_cluster_sizes: Vec<Vec<[f64; 4]>> = (0..first.top_cluster_sizes.len())
355            .map(|mode| {
356                let n_top_temps = results
357                    .iter()
358                    .filter_map(|r| r.cluster_stats.top_cluster_sizes.get(mode))
359                    .map(Vec::len)
360                    .max()
361                    .unwrap_or(0);
362                vec![[0.0; 4]; n_top_temps]
363            })
364            .collect();
365        for stats in results.iter().map(|r| &r.cluster_stats) {
366            for (total, part) in fk_csd.iter_mut().zip(&stats.fk_csd) {
367                for (total, &count) in total.iter_mut().zip(part) {
368                    *total += count;
369                }
370            }
371            for (total, part) in overlap_csd
372                .iter_mut()
373                .flatten()
374                .zip(stats.overlap_csd.iter().flatten())
375            {
376                for (total, &count) in total.iter_mut().zip(part) {
377                    *total += count;
378                }
379            }
380            for (total, part) in top_cluster_sizes.iter_mut().zip(&stats.top_cluster_sizes) {
381                for (total, sizes) in total.iter_mut().zip(part) {
382                    for k in 0..4 {
383                        total[k] += sizes[k];
384                    }
385                }
386            }
387        }
388        for size in top_cluster_sizes.iter_mut().flatten().flatten() {
389            *size /= n;
390        }
391
392        let overlap_results: Vec<_> = results.iter().map(|r| &r.overlap_stats).collect();
393        let overlap_stats = if retain_overlap_samples {
394            OverlapStats::aggregate(&overlap_results)
395        } else {
396            OverlapStats::aggregate_without_samples(&overlap_results)
397        };
398
399        let checkpoints = &results[0].diagnostics.equil_checkpoints;
400        let equil_checkpoints = (0..checkpoints.len())
401            .map(|i| EquilCheckpoint {
402                sweep: checkpoints[i].sweep,
403                energy_avg: mean_over(results, |r| &r.diagnostics.equil_checkpoints[i].energy_avg),
404                link_overlap_avg: mean_over(results, |r| {
405                    &r.diagnostics.equil_checkpoints[i].link_overlap_avg
406                }),
407                overlap2_avg: mean_over(results, |r| {
408                    &r.diagnostics.equil_checkpoints[i].overlap2_avg
409                }),
410            })
411            .collect();
412        debug_assert!(results.iter().all(|r| r.mags.len() == n_temps));
413
414        SweepResult {
415            mags: mean_over(results, |r| &r.mags),
416            mags2: mean_over(results, |r| &r.mags2),
417            mags4: mean_over(results, |r| &r.mags4),
418            energies: mean_over(results, |r| &r.energies),
419            energies2: mean_over(results, |r| &r.energies2),
420            energy_variance: mean_over(results, |r| &r.energy_variance),
421            overlap_stats,
422            cluster_stats: ClusterStats {
423                fk_csd,
424                overlap_csd,
425                top_cluster_sizes,
426                fk_updates: sum_over(results, |r| &r.cluster_stats.fk_updates),
427                fk_visited_spins: sum_over(results, |r| &r.cluster_stats.fk_visited_spins),
428                overlap_moves: (0..first.overlap_moves.len())
429                    .map(|mode| {
430                        let mut total = first.overlap_moves[mode].clone();
431                        for stats in results[1..].iter().map(|r| &r.cluster_stats) {
432                            for (total, part) in total.iter_mut().zip(&stats.overlap_moves[mode]) {
433                                total.add(part);
434                            }
435                        }
436                        total
437                    })
438                    .collect(),
439                rmc_edges: {
440                    let mut total = first.rmc_edges.clone();
441                    for stats in results[1..].iter().map(|r| &r.cluster_stats) {
442                        for (total, part) in total.iter_mut().zip(&stats.rmc_edges) {
443                            total.add(part);
444                        }
445                    }
446                    total
447                },
448            },
449            per_disorder_cluster_observations: results
450                .iter()
451                .flat_map(|r| r.per_disorder_cluster_observations.iter().cloned())
452                .collect(),
453            per_disorder_physics: results
454                .iter()
455                .flat_map(|r| r.per_disorder_physics.iter().cloned())
456                .collect(),
457            per_disorder: results
458                .iter()
459                .flat_map(|r| r.per_disorder.iter().cloned())
460                .collect(),
461            diagnostics: Diagnostics {
462                mags2_tau: defined_mean_over(results, |r| &r.diagnostics.mags2_tau),
463                overlap2_tau: defined_mean_over(results, |r| &r.diagnostics.overlap2_tau),
464                energy_tau: defined_mean_over(results, |r| &r.diagnostics.energy_tau),
465                mags2_tau_converged: all_over(results, |r| &r.diagnostics.mags2_tau_converged),
466                overlap2_tau_converged: all_over(results, |r| {
467                    &r.diagnostics.overlap2_tau_converged
468                }),
469                energy_tau_converged: all_over(results, |r| &r.diagnostics.energy_tau_converged),
470                autocorrelation_lag: results[0].diagnostics.autocorrelation_lag,
471                equil_checkpoints,
472            },
473            cluster_snapshots: Vec::new(),
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    /// A per-sample result whose energies have the given thermal mean and variance.
483    fn sample(mean: f64, variance: f64, top: Vec<Vec<[f64; 4]>>) -> SweepResult {
484        let mut result = SweepResult::empty();
485        result.mags = vec![0.0];
486        result.mags2 = vec![0.0];
487        result.mags4 = vec![0.0];
488        result.energies = vec![mean];
489        result.energies2 = vec![variance + mean * mean];
490        result.energy_variance = vec![variance];
491        result.cluster_stats.top_cluster_sizes = top;
492        result
493    }
494
495    #[test]
496    fn aggregate_averages_per_sample_thermal_variance() {
497        let agg = SweepResult::aggregate(&[sample(-1.0, 0.25, vec![]), sample(-2.0, 0.75, vec![])]);
498        assert_eq!(agg.energies, vec![-1.5]);
499        assert_eq!(agg.energy_variance, vec![0.5]);
500        // Pooling across disorder would add Var_J(⟨e⟩) = 0.25.
501        assert_eq!(agg.energies2[0] - agg.energies[0].powi(2), 0.75);
502    }
503
504    #[test]
505    fn empty_aggregate_is_empty() {
506        let agg = SweepResult::aggregate(&[]);
507        assert!(agg.energies.is_empty() && agg.cluster_stats.top_cluster_sizes.is_empty());
508    }
509
510    #[test]
511    fn top_cluster_modes_keep_their_own_lengths() {
512        let tops = |first: Vec<[f64; 4]>| vec![first, vec![[0.5, 0.25, 0.0, 0.0]; 2]];
513        let agg = SweepResult::aggregate(&[
514            sample(0.0, 0.0, tops(vec![])),
515            sample(0.0, 0.0, tops(vec![[1.0; 4]; 2])),
516        ]);
517        let sizes = &agg.cluster_stats.top_cluster_sizes;
518        assert_eq!(sizes[0], vec![[0.5; 4]; 2]);
519        assert_eq!(sizes[1], vec![[0.5, 0.25, 0.0, 0.0]; 2]);
520    }
521}