1use crate::geometry::Lattice;
2
3use super::Statistics;
4
5pub struct OverlapStats {
9 pub overlap: Vec<f64>,
11 pub overlap2: Vec<f64>,
13 pub overlap4: Vec<f64>,
15 pub link_overlap: Vec<f64>,
17 pub link_overlap2: Vec<f64>,
19 pub link_overlap4: Vec<f64>,
21 pub histogram: Vec<Vec<u64>>,
24 pub ql_at_q_sum: Vec<Vec<f64>>,
27 pub ql2_at_q_sum: Vec<Vec<f64>>,
30 pub per_sample_histogram: Vec<Vec<Vec<u64>>>,
33 pub per_sample_ql_at_q_sum: Vec<Vec<Vec<f64>>>,
36 pub per_sample_ql2_at_q_sum: Vec<Vec<Vec<f64>>>,
39}
40
41impl OverlapStats {
42 pub fn empty() -> Self {
43 Self {
44 overlap: vec![],
45 overlap2: vec![],
46 overlap4: vec![],
47 link_overlap: vec![],
48 link_overlap2: vec![],
49 link_overlap4: vec![],
50 histogram: vec![],
51 ql_at_q_sum: vec![],
52 ql2_at_q_sum: vec![],
53 per_sample_histogram: vec![],
54 per_sample_ql_at_q_sum: vec![],
55 per_sample_ql2_at_q_sum: vec![],
56 }
57 }
58
59 pub fn aggregate(results: &[&Self]) -> Self {
61 Self::aggregate_impl(results, true)
62 }
63
64 pub(crate) fn aggregate_without_samples(results: &[&Self]) -> Self {
65 Self::aggregate_impl(results, false)
66 }
67
68 fn aggregate_impl(results: &[&Self], retain_samples: bool) -> Self {
69 if results.first().is_none_or(|r| r.overlap.is_empty()) {
70 return Self::empty();
71 }
72
73 let n = results.len() as f64;
74 let n_temps = results[0].overlap.len();
75 let n_hist = results[0].histogram.len();
76 let hist_bins = results[0].histogram[0].len();
77 let n_ql = results[0].ql_at_q_sum.len();
78 let ql_bins = results[0].ql_at_q_sum.first().map_or(0, |v| v.len());
79
80 let mut agg = Self {
81 overlap: vec![0.0; n_temps],
82 overlap2: vec![0.0; n_temps],
83 overlap4: vec![0.0; n_temps],
84 link_overlap: vec![0.0; n_temps],
85 link_overlap2: vec![0.0; n_temps],
86 link_overlap4: vec![0.0; n_temps],
87 histogram: (0..n_hist).map(|_| vec![0u64; hist_bins]).collect(),
88 ql_at_q_sum: (0..n_ql).map(|_| vec![0.0; ql_bins]).collect(),
89 ql2_at_q_sum: (0..n_ql).map(|_| vec![0.0; ql_bins]).collect(),
90 per_sample_histogram: if retain_samples {
91 results.iter().map(|r| r.histogram.clone()).collect()
92 } else {
93 vec![]
94 },
95 per_sample_ql_at_q_sum: if retain_samples {
96 results.iter().map(|r| r.ql_at_q_sum.clone()).collect()
97 } else {
98 vec![]
99 },
100 per_sample_ql2_at_q_sum: if retain_samples {
101 results.iter().map(|r| r.ql2_at_q_sum.clone()).collect()
102 } else {
103 vec![]
104 },
105 };
106
107 for r in results {
108 for (a, &v) in agg.overlap.iter_mut().zip(r.overlap.iter()) {
109 *a += v;
110 }
111 for (a, &v) in agg.overlap2.iter_mut().zip(r.overlap2.iter()) {
112 *a += v;
113 }
114 for (a, &v) in agg.overlap4.iter_mut().zip(r.overlap4.iter()) {
115 *a += v;
116 }
117 for (a, &v) in agg.link_overlap.iter_mut().zip(r.link_overlap.iter()) {
118 *a += v;
119 }
120 for (a, &v) in agg.link_overlap2.iter_mut().zip(r.link_overlap2.iter()) {
121 *a += v;
122 }
123 for (a, &v) in agg.link_overlap4.iter_mut().zip(r.link_overlap4.iter()) {
124 *a += v;
125 }
126 for (a, s) in agg.histogram.iter_mut().zip(r.histogram.iter()) {
127 for (ah, &sh) in a.iter_mut().zip(s.iter()) {
128 *ah += sh;
129 }
130 }
131 for (a, s) in agg.ql_at_q_sum.iter_mut().zip(r.ql_at_q_sum.iter()) {
132 for (ah, &sh) in a.iter_mut().zip(s.iter()) {
133 *ah += sh;
134 }
135 }
136 for (a, s) in agg.ql2_at_q_sum.iter_mut().zip(r.ql2_at_q_sum.iter()) {
137 for (ah, &sh) in a.iter_mut().zip(s.iter()) {
138 *ah += sh;
139 }
140 }
141 }
142
143 for v in agg
144 .overlap
145 .iter_mut()
146 .chain(agg.overlap2.iter_mut())
147 .chain(agg.overlap4.iter_mut())
148 .chain(agg.link_overlap.iter_mut())
149 .chain(agg.link_overlap2.iter_mut())
150 .chain(agg.link_overlap4.iter_mut())
151 {
152 *v /= n;
153 }
154
155 agg
156 }
157}
158
159pub struct OverlapAccum {
161 n_pairs: usize,
162 n_replicas: usize,
164 n_temps: usize,
165 n_spins: usize,
166 n_bonds: usize,
167 equil_diag: bool,
168 collect_q2_ac: bool,
169
170 overlap_stat: Statistics,
171 overlap2_stat: Statistics,
172 overlap4_stat: Statistics,
173 link_overlap_stat: Statistics,
174 link_overlap2_stat: Statistics,
175 link_overlap4_stat: Statistics,
176
177 histogram: Vec<Vec<u64>>,
178 ql_at_q_sum: Vec<Vec<f64>>,
179 ql2_at_q_sum: Vec<Vec<f64>>,
180
181 overlaps_buf: Vec<f32>,
182 overlaps2_buf: Vec<f32>,
183 overlaps4_buf: Vec<f32>,
184 link_overlaps_buf: Vec<f32>,
185 link_overlaps2_buf: Vec<f32>,
186 link_overlaps4_buf: Vec<f32>,
187
188 pub diag_ql_buf: Vec<f32>,
189 pub diag_q2_buf: Vec<f32>,
191 pub q2_ac_buf: Vec<f64>,
192 site_overlap: Vec<i8>,
193}
194
195pub(crate) fn pair_q2_sum<'a>(
198 n_replicas: usize,
199 replica: impl Fn(usize) -> &'a [i8],
200) -> (f64, usize) {
201 let mut sum = 0.0;
202 let mut pairs = 0;
203 for a in 0..n_replicas {
204 let spins_a = replica(a);
205 let n = spins_a.len() as f64;
206 for b in a + 1..n_replicas {
207 let dot: i64 = spins_a
208 .iter()
209 .zip(replica(b))
210 .map(|(&x, &y)| i64::from(x * y))
211 .sum();
212 let q = dot as f64 / n;
213 sum += q * q;
214 pairs += 1;
215 }
216 }
217 (sum, pairs)
218}
219
220fn overlap_sums(lattice: &Lattice, q: &[i8]) -> (i64, i64) {
225 if let Some((l0, l1)) = lattice.square_shape() {
226 return overlap_sums_strided(&[l0, l1], q);
227 }
228 if let Some(shape) = lattice.cubic_shape() {
229 return overlap_sums_strided(&shape, q);
230 }
231 overlap_sums_chunked(lattice, q, i32::MAX as usize / (lattice.n_neighbors + 1))
232}
233
234fn dot_i8(a: &[i8], b: &[i8]) -> i64 {
236 const CHUNK: usize = 1 << 15;
238 a.chunks(CHUNK)
239 .zip(b.chunks(CHUNK))
240 .map(|(a, b)| {
241 i64::from(
242 a.iter()
243 .zip(b)
244 .map(|(&x, &y)| i32::from(x * y))
245 .sum::<i32>(),
246 )
247 })
248 .sum()
249}
250
251fn overlap_sums_strided(extents: &[usize], q: &[i8]) -> (i64, i64) {
257 if q.is_empty() {
258 return (0, 0);
259 }
260 let dot_spin = q
261 .chunks(1 << 15)
262 .map(|chunk| i64::from(chunk.iter().map(|&v| i32::from(v)).sum::<i32>()))
263 .sum();
264 let mut dot_link = 0i64;
265 let mut stride = q.len();
266 for &extent in extents {
267 let period = stride;
268 stride /= extent;
269 for block in q.chunks_exact(period) {
270 dot_link += dot_i8(&block[..period - stride], &block[stride..]);
271 dot_link += dot_i8(&block[period - stride..], &block[..stride]);
272 }
273 }
274 (dot_spin, dot_link)
275}
276
277fn overlap_sums_chunked(lattice: &Lattice, q: &[i8], chunk: usize) -> (i64, i64) {
278 let n_neighbors = lattice.n_neighbors;
279 let chunk = chunk.max(1);
280 let mut dot_spin = 0i64;
281 let mut dot_link = 0i64;
282 for start in (0..q.len()).step_by(chunk) {
283 let mut spin_acc = 0i32;
284 let mut link_acc = 0i32;
285 for j in start..q.len().min(start + chunk) {
286 let qj = q[j] as i32;
287 let mut neighbors = 0i32;
288 for d in 0..n_neighbors {
289 neighbors += q[lattice.neighbor_fwd(j, d)] as i32;
290 }
291 spin_acc += qj;
292 link_acc += qj * neighbors;
293 }
294 dot_spin += spin_acc as i64;
295 dot_link += link_acc as i64;
296 }
297 (dot_spin, dot_link)
298}
299
300impl OverlapAccum {
301 pub fn new(
302 n_temps: usize,
303 n_spins: usize,
304 n_pairs: usize,
305 n_neighbors: usize,
306 equil_diag: bool,
307 collect_q2_ac: bool,
308 ) -> Self {
309 let has_pairs = n_pairs > 0;
310 Self {
311 n_pairs,
312 n_replicas: 2 * n_pairs,
313 n_temps,
314 n_spins,
315 n_bonds: n_spins * n_neighbors,
316 equil_diag,
317 collect_q2_ac,
318
319 overlap_stat: Statistics::new(n_temps, 1),
320 overlap2_stat: Statistics::new(n_temps, 1),
321 overlap4_stat: Statistics::new(n_temps, 1),
322 link_overlap_stat: Statistics::new(n_temps, 1),
323 link_overlap2_stat: Statistics::new(n_temps, 1),
324 link_overlap4_stat: Statistics::new(n_temps, 1),
325
326 histogram: if has_pairs {
327 (0..n_temps).map(|_| vec![0u64; n_spins + 1]).collect()
328 } else {
329 vec![]
330 },
331 ql_at_q_sum: if has_pairs {
332 (0..n_temps).map(|_| vec![0.0f64; n_spins + 1]).collect()
333 } else {
334 vec![]
335 },
336 ql2_at_q_sum: if has_pairs {
337 (0..n_temps).map(|_| vec![0.0f64; n_spins + 1]).collect()
338 } else {
339 vec![]
340 },
341
342 overlaps_buf: vec![0.0f32; n_temps],
343 overlaps2_buf: vec![0.0f32; n_temps],
344 overlaps4_buf: vec![0.0f32; n_temps],
345 link_overlaps_buf: vec![0.0f32; n_temps],
346 link_overlaps2_buf: vec![0.0f32; n_temps],
347 link_overlaps4_buf: vec![0.0f32; n_temps],
348
349 diag_ql_buf: vec![0.0; if equil_diag { n_temps } else { 0 }],
350 diag_q2_buf: vec![0.0; if equil_diag { n_temps } else { 0 }],
351 q2_ac_buf: if collect_q2_ac {
352 vec![0.0f64; n_temps]
353 } else {
354 vec![]
355 },
356 site_overlap: if has_pairs { vec![0; n_spins] } else { vec![] },
357 }
358 }
359
360 pub fn without_conditional_link_overlap(mut self) -> Self {
363 self.ql_at_q_sum = vec![];
364 self.ql2_at_q_sum = vec![];
365 self
366 }
367
368 pub fn with_replicas(mut self, n_replicas: usize) -> Self {
371 self.n_replicas = n_replicas;
372 self
373 }
374
375 #[inline]
376 pub fn collect(&mut self, lattice: &Lattice, spins: &[i8], system_ids: &[usize], record: bool) {
377 if self.equil_diag {
378 self.diag_ql_buf.fill(0.0);
379 self.diag_q2_buf.fill(0.0);
380 }
381 for pair_idx in 0..self.n_pairs {
382 let r_a = 2 * pair_idx;
383 let r_b = 2 * pair_idx + 1;
384
385 for t in 0..self.n_temps {
386 let sys_a = system_ids[r_a * self.n_temps + t];
387 let sys_b = system_ids[r_b * self.n_temps + t];
388 let spins_a = &spins[sys_a * self.n_spins..(sys_a + 1) * self.n_spins];
389 let spins_b = &spins[sys_b * self.n_spins..(sys_b + 1) * self.n_spins];
390 for ((q, &sa), &sb) in self.site_overlap.iter_mut().zip(spins_a).zip(spins_b) {
391 *q = sa * sb;
392 }
393 let (dot_spin, dot_link) = overlap_sums(lattice, &self.site_overlap);
394
395 let ql = dot_link as f32 / self.n_bonds as f32;
396
397 let q = dot_spin as f32 / self.n_spins as f32;
398 let q2 = q * q;
399 if self.equil_diag {
400 self.diag_ql_buf[t] += ql;
401 self.diag_q2_buf[t] += q2;
402 }
403 if !record {
404 continue;
405 }
406
407 self.overlaps_buf[t] = q;
408 self.overlaps2_buf[t] = q2;
409 self.overlaps4_buf[t] = q2 * q2;
410
411 let ql2 = ql * ql;
412 self.link_overlaps_buf[t] = ql;
413 self.link_overlaps2_buf[t] = ql2;
414 self.link_overlaps4_buf[t] = ql2 * ql2;
415
416 let idx = ((dot_spin + self.n_spins as i64) / 2) as usize;
417 self.histogram[t][idx] += 1;
418 if let Some(sum) = self.ql_at_q_sum.get_mut(t) {
419 sum[idx] += ql as f64;
420 self.ql2_at_q_sum[t][idx] += (ql * ql) as f64;
421 }
422 }
423
424 if !record {
425 continue;
426 }
427
428 self.overlap_stat.update(&self.overlaps_buf);
429 self.overlap2_stat.update(&self.overlaps2_buf);
430 self.overlap4_stat.update(&self.overlaps4_buf);
431 self.link_overlap_stat.update(&self.link_overlaps_buf);
432 self.link_overlap2_stat.update(&self.link_overlaps2_buf);
433 self.link_overlap4_stat.update(&self.link_overlaps4_buf);
434 }
435
436 if self.equil_diag {
437 let inv = 1.0 / self.n_pairs as f32;
438 for v in self.diag_ql_buf.iter_mut().chain(&mut self.diag_q2_buf) {
439 *v *= inv;
440 }
441 }
442 if self.collect_q2_ac && record {
443 self.average_q2_over_all_pairs(spins, system_ids);
444 }
445 }
446
447 fn average_q2_over_all_pairs(&mut self, spins: &[i8], system_ids: &[usize]) {
453 let n = self.n_spins;
454 for t in 0..self.n_temps {
455 let (sum, pairs) = pair_q2_sum(self.n_replicas, |replica| {
456 let system = system_ids[replica * self.n_temps + t];
457 &spins[system * n..(system + 1) * n]
458 });
459 self.q2_ac_buf[t] = sum / pairs as f64;
460 }
461 }
462
463 pub fn finish(self) -> OverlapStats {
464 if self.n_pairs == 0 {
465 return OverlapStats::empty();
466 }
467 OverlapStats {
468 overlap: self.overlap_stat.average(),
469 overlap2: self.overlap2_stat.average(),
470 overlap4: self.overlap4_stat.average(),
471 link_overlap: self.link_overlap_stat.average(),
472 link_overlap2: self.link_overlap2_stat.average(),
473 link_overlap4: self.link_overlap4_stat.average(),
474 histogram: self.histogram,
475 ql_at_q_sum: self.ql_at_q_sum,
476 ql2_at_q_sum: self.ql2_at_q_sum,
477 per_sample_histogram: vec![],
478 per_sample_ql_at_q_sum: vec![],
479 per_sample_ql2_at_q_sum: vec![],
480 }
481 }
482}
483
484#[cfg(test)]
485mod tests {
486
487 #[test]
490 fn q2_autocorrelation_observable_is_relabel_invariant() {
491 let lattice = Lattice::new(vec![4, 4]);
492 let n = lattice.n_spins;
493 let n_temps = 2;
494 let spins: Vec<i8> = (0..4 * n_temps * n)
495 .map(|k| if (k * 37 + k / 5) % 7 < 3 { -1 } else { 1 })
496 .collect();
497 let observable = |system_ids: &[usize], n_pairs: usize| {
498 let mut accum =
499 OverlapAccum::new(n_temps, n, n_pairs, lattice.n_neighbors, false, true);
500 accum.collect(&lattice, &spins, system_ids, true);
501 accum.q2_ac_buf.clone()
502 };
503 let identity: Vec<usize> = (0..4 * n_temps).collect();
504 let mut relabelled = identity.clone();
506 relabelled.swap(n_temps, 2 * n_temps);
507 assert_eq!(observable(&identity, 2), observable(&relabelled, 2));
508
509 let three = |system_ids: &[usize]| {
511 let mut accum =
512 OverlapAccum::new(n_temps, n, 1, lattice.n_neighbors, false, true).with_replicas(3);
513 accum.collect(&lattice, &spins, system_ids, true);
514 accum.q2_ac_buf.clone()
515 };
516 let mut rotated = identity[..3 * n_temps].to_vec();
517 rotated.swap(0, 2 * n_temps);
518 assert_eq!(three(&identity[..3 * n_temps]), three(&rotated));
519 assert_ne!(
520 three(&identity[..3 * n_temps]),
521 observable(&identity[..2 * n_temps], 1)
522 );
523
524 let two = observable(&identity[..2 * n_temps], 1);
525 for (t, value) in two.iter().enumerate() {
526 let dot: i64 = (0..n)
527 .map(|i| i64::from(spins[t * n + i] * spins[(n_temps + t) * n + i]))
528 .sum();
529 let q = dot as f64 / n as f64;
530 assert!((value - q * q).abs() < 1e-12);
531 }
532 }
533
534 use super::*;
535 use rand::{Rng, SeedableRng};
536 use rand_xoshiro::Xoshiro256StarStar;
537
538 #[test]
541 fn strided_overlap_sums_match_the_table_walk() {
542 let mut rng = Xoshiro256StarStar::seed_from_u64(21);
543 for shape in [
544 vec![2, 2],
545 vec![3, 7],
546 vec![8, 5],
547 vec![2, 3, 4],
548 vec![5, 2, 3],
549 vec![4, 4, 4],
550 ] {
551 let lattice = Lattice::new(shape.clone());
552 let q: Vec<i8> = (0..lattice.n_spins)
553 .map(|_| if rng.gen::<bool>() { 1 } else { -1 })
554 .collect();
555 assert_eq!(
556 overlap_sums(&lattice, &q),
557 overlap_sums_chunked(&lattice, &q, 7),
558 "{shape:?}"
559 );
560 }
561 }
562
563 #[test]
564 fn chunked_overlap_sums_match_direct_definition() {
565 let lattice = Lattice::with_offsets(vec![5, 6], vec![vec![1, 0], vec![0, 1], vec![1, -1]]);
566 let mut rng = Xoshiro256StarStar::seed_from_u64(9);
567 let q: Vec<i8> = (0..lattice.n_spins)
568 .map(|_| if rng.gen::<bool>() { 1 } else { -1 })
569 .collect();
570 let spin: i64 = q.iter().map(|&v| v as i64).sum();
571 let link: i64 = (0..lattice.n_spins)
572 .flat_map(|i| (0..lattice.n_neighbors).map(move |d| (i, d)))
573 .map(|(i, d)| q[i] as i64 * q[lattice.neighbor_fwd(i, d)] as i64)
574 .sum();
575 assert_eq!(overlap_sums(&lattice, &q), (spin, link));
576 for chunk in [1, 7, 29, 30, 1000] {
577 assert_eq!(overlap_sums_chunked(&lattice, &q, chunk), (spin, link));
578 }
579 }
580
581 #[test]
582 fn empty_aggregate_is_empty() {
583 let empty = OverlapStats::aggregate(&[]);
584 assert!(empty.overlap.is_empty() && empty.per_sample_histogram.is_empty());
585 }
586}