1use crate::{
3 geometry::Lattice,
4 spins::{
5 model::{Real, Spin},
6 xy::{cross, dot},
7 },
8};
9use std::{
10 collections::BTreeMap,
11 f64::consts::{PI, TAU},
12};
13
14#[derive(Clone, Debug, Default)]
15pub struct PhysicsOptions {
16 pub displacements: Vec<Vec<isize>>,
17 pub vortices: bool,
18 pub block_size: Option<usize>,
20}
21
22impl PhysicsOptions {
23 pub fn validate(&self, lattice: &Lattice, components: usize) -> Result<(), String> {
25 if self.block_size == Some(0) {
26 return Err("block_size must be positive".into());
27 }
28 if self.vortices && (lattice.n_dims != 2 || components != 2) {
29 return Err("angle vortices require a two-dimensional XY lattice".into());
30 }
31 if self.displacements.iter().any(|r| r.len() != lattice.n_dims) {
32 return Err("correlation displacements must match lattice dimensionality".into());
33 }
34 Ok(())
35 }
36}
37
38#[derive(Clone, Copy, Debug)]
40pub struct CachedTotals {
41 pub energy: f64,
43 pub magnetization: [f64; 2],
45}
46
47#[derive(Clone, Debug)]
48pub struct MomentField {
49 pub name: &'static str,
50 pub width: usize,
51 pub offset: usize,
52}
53
54#[derive(Clone, Debug)]
55pub struct MomentBlock {
56 pub sums: Vec<Vec<f64>>,
58 pub count: u64,
60 pub sweeps: usize,
61}
62
63#[derive(Clone, Debug)]
68pub struct PhysicsResult {
69 pub fields: Vec<MomentField>,
70 pub moments: Vec<Vec<f64>>,
71 pub count: u64,
72 pub blocks: Vec<MomentBlock>,
73}
74
75pub type PhysicsValues = BTreeMap<&'static str, Vec<Vec<f64>>>;
76impl PhysicsResult {
77 pub fn values(
78 &self,
79 shape: &[usize],
80 temperatures: &[f64],
81 components: usize,
82 ) -> PhysicsValues {
83 let n: f64 = shape.iter().product::<usize>() as f64;
84 let mut out: PhysicsValues = self
85 .fields
86 .iter()
87 .map(|f| {
88 (
89 f.name,
90 self.moments
91 .iter()
92 .map(|row| row[f.offset..f.offset + f.width].to_vec())
93 .collect(),
94 )
95 })
96 .collect();
97 let mut heat = Vec::new();
98 let mut binder = Vec::new();
99 let mut s0 = Vec::new();
100 let mut chi = Vec::new();
101 let mut helicity = Vec::new();
102 let mut xi = Vec::new();
103 let mut xi_ratio = Vec::new();
104 for (t, &temperature) in temperatures.iter().enumerate() {
105 let beta = 1.0 / temperature;
106 let m2 = out["mags2"][t][0];
107 heat.push(vec![
108 beta * beta * n * (out["energies2"][t][0] - out["energies"][t][0].powi(2)),
109 ]);
110 let denominator = (components as f64 + 2.0) / components as f64 * m2 * m2;
111 binder.push(vec![if denominator > 0.0 {
112 1.0 - out["mags4"][t][0] / denominator
113 } else {
114 f64::NAN
115 }]);
116 s0.push(vec![n * m2]);
117 chi.push(vec![beta * n * m2 / components as f64]);
118 let (length, ratio) =
119 second_moment_lengths(n * m2, &out["structure_factor_min"][t], shape);
120 if components == 2 {
121 helicity.push(
122 (0..shape.len())
123 .map(|d| (out["helicity_d"][t][d] - beta * out["helicity_i2"][t][d]) / n)
124 .collect(),
125 );
126 }
127 xi.push(length);
128 xi_ratio.push(ratio);
129 }
130 if out.contains_key("overlap2") {
131 let mut sg_binder = Vec::new();
132 let mut sg_s0 = Vec::new();
133 let mut sg_xi = Vec::new();
134 let mut sg_xi_ratio = Vec::new();
135 let rows = out["overlap2"]
136 .iter()
137 .zip(&out["overlap4"])
138 .zip(&out["overlap_structure_factor_min"]);
139 for ((q2, q4), sk_min) in rows {
140 let (q2, q4) = (q2[0], q4[0]);
141 sg_binder.push(vec![if q2 > 0.0 {
142 1.0 - q4 / (3.0 * q2 * q2)
143 } else {
144 f64::NAN
145 }]);
146 sg_s0.push(vec![n * q2]);
147 let (length, ratio) = second_moment_lengths(n * q2, sk_min, shape);
148 sg_xi.push(length);
149 sg_xi_ratio.push(ratio);
150 }
151 out.insert("sg_binder", sg_binder);
152 out.insert("sg_structure_factor_0", sg_s0);
153 out.insert("sg_correlation_length", sg_xi);
154 out.insert("sg_correlation_length_ratio", sg_xi_ratio);
155 }
156 for (name, values) in [
157 ("heat_capacity", heat),
158 ("binder_cumulant", binder),
159 ("structure_factor_0", s0),
160 ("susceptibility", chi),
161 ("correlation_length", xi),
162 ("correlation_length_ratio", xi_ratio),
163 ] {
164 out.insert(name, values);
165 }
166 if components == 2 {
167 out.insert("helicity_modulus", helicity);
168 }
169 out
170 }
171
172 pub fn aggregate<'a>(
174 results: impl IntoIterator<Item = &'a Self>,
175 shape: &[usize],
176 temperatures: &[f64],
177 components: usize,
178 ) -> PhysicsValues {
179 let results: Vec<_> = results.into_iter().collect();
180 assert!(!results.is_empty());
181 let mut pooled = Self {
182 fields: results[0].fields.clone(),
183 moments: vec![vec![0.0; results[0].moments[0].len()]; temperatures.len()],
184 count: 0,
185 blocks: vec![],
186 };
187 for result in &results {
188 for (dst, src) in pooled.moments.iter_mut().zip(&result.moments) {
189 for (a, b) in dst.iter_mut().zip(src) {
190 *a += b / results.len() as f64;
191 }
192 }
193 }
194 let mut out = pooled.values(shape, temperatures, components);
195 let mut heat = vec![vec![0.0]; temperatures.len()];
196 for result in &results {
197 let values = result.values(shape, temperatures, components);
198 for (dst, src) in heat.iter_mut().zip(&values["heat_capacity"]) {
199 dst[0] += src[0] / results.len() as f64;
200 }
201 }
202 out.insert("heat_capacity", heat);
203 out
204 }
205}
206
207impl PhysicsResult {
208 pub fn jackknife_stderr<'a>(
214 results: impl IntoIterator<Item = &'a Self>,
215 shape: &[usize],
216 temperatures: &[f64],
217 components: usize,
218 ) -> PhysicsValues {
219 let results: Vec<_> = results.into_iter().collect();
220 assert!(!results.is_empty());
221 let n = results.len();
222 let full = Self::aggregate(results.iter().copied(), shape, temperatures, components);
223 let undefined = |values: &PhysicsValues| -> PhysicsValues {
224 values
225 .iter()
226 .map(|(&name, rows)| {
227 (
228 name,
229 rows.iter().map(|row| vec![f64::NAN; row.len()]).collect(),
230 )
231 })
232 .collect()
233 };
234 if n < 2 {
235 return undefined(&full);
236 }
237
238 let width = results[0].moments[0].len();
239 let mut total = vec![vec![0.0; width]; temperatures.len()];
240 for result in &results {
241 for (sum, moment) in total
242 .iter_mut()
243 .flatten()
244 .zip(result.moments.iter().flatten())
245 {
246 *sum += moment;
247 }
248 }
249 let heats: Vec<Vec<f64>> = results
251 .iter()
252 .map(|result| {
253 result.values(shape, temperatures, components)["heat_capacity"]
254 .iter()
255 .map(|row| row[0])
256 .collect()
257 })
258 .collect();
259 let heat_total: Vec<f64> = (0..temperatures.len())
260 .map(|t| heats.iter().map(|heat| heat[t]).sum())
261 .collect();
262
263 let left_out = (n - 1) as f64;
264 let leave_one_out: Vec<PhysicsValues> = (0..n)
265 .map(|omitted| {
266 let pooled = Self {
267 fields: results[0].fields.clone(),
268 moments: total
269 .iter()
270 .zip(&results[omitted].moments)
271 .map(|(sum, moment)| {
272 sum.iter()
273 .zip(moment)
274 .map(|(sum, moment)| (sum - moment) / left_out)
275 .collect()
276 })
277 .collect(),
278 count: 0,
279 blocks: vec![],
280 };
281 let mut values = pooled.values(shape, temperatures, components);
282 let heat = (0..temperatures.len())
283 .map(|t| vec![(heat_total[t] - heats[omitted][t]) / left_out])
284 .collect();
285 values.insert("heat_capacity", heat);
286 values
287 })
288 .collect();
289
290 let mut stderr = undefined(&full);
292 for (&name, rows) in stderr.iter_mut() {
293 for (t, row) in rows.iter_mut().enumerate() {
294 for (k, error) in row.iter_mut().enumerate() {
295 let estimates = leave_one_out.iter().map(|values| values[name][t][k]);
296 let mean = estimates.clone().sum::<f64>() / n as f64;
297 let spread: f64 = estimates.map(|value| (value - mean).powi(2)).sum();
298 *error = (left_out / n as f64 * spread).sqrt();
299 }
300 }
301 }
302 stderr
303 }
304}
305
306fn second_moment_lengths(s0: f64, sk_min: &[f64], shape: &[usize]) -> (Vec<f64>, Vec<f64>) {
309 shape
310 .iter()
311 .zip(sk_min)
312 .map(|(&extent, &sk)| {
313 let radicand = s0 / sk - 1.0;
314 let length = if sk > 0.0 && radicand >= 0.0 {
315 radicand.sqrt() / (2.0 * (PI / extent as f64).sin())
316 } else {
317 f64::NAN
318 };
319 (length, length / extent as f64)
320 })
321 .unzip()
322}
323
324pub struct PhysicsCollector {
326 fields: Vec<MomentField>,
327 phases: Vec<Vec<[f64; 2]>>,
329 planes: Vec<Vec<[f64; 2]>>,
331 angles: Vec<f64>,
333 displaced: Vec<Vec<usize>>,
334 options: PhysicsOptions,
335 components: usize,
336 sums: Vec<Vec<f64>>,
337 block_sums: Vec<Vec<f64>>,
338 block_temperatures: Vec<bool>,
340 block_count: u64,
341 block_sweeps: usize,
342 blocks: Vec<MomentBlock>,
343 count: u64,
344 scratch: Vec<f64>,
345 overlap_offset: Option<usize>,
347 overlap_sites: Vec<i8>,
349}
350
351impl PhysicsCollector {
352 pub fn new(
353 lattice: &Lattice,
354 n_temps: usize,
355 options: &PhysicsOptions,
356 components: usize,
357 ) -> Result<Self, String> {
358 options.validate(lattice, components)?;
359 let mut fields = Vec::new();
360 let mut width = 0;
361 let mut add = |name, n| {
362 fields.push(MomentField {
363 name,
364 width: n,
365 offset: width,
366 });
367 width += n;
368 };
369 for name in ["energies", "energies2", "mags", "mags2", "mags4"] {
370 add(name, 1);
371 }
372 if components == 2 {
373 for name in ["helicity_d", "helicity_i", "helicity_i2"] {
374 add(name, lattice.n_dims);
375 }
376 }
377 add("structure_factor_min", lattice.n_dims);
378 if !options.displacements.is_empty() {
379 add("correlations", options.displacements.len());
380 }
381 if options.vortices {
382 add("angle_vortex_density", 1);
383 add("intact_plaquette_fraction", 1);
384 }
385 let phases = lattice
386 .shape
387 .iter()
388 .map(|&extent| {
389 (0..extent)
390 .map(|x| {
391 let (s, c) = (TAU * x as f64 / extent as f64).sin_cos();
392 [c, s]
393 })
394 .collect()
395 })
396 .collect();
397 let planes = lattice
398 .shape
399 .iter()
400 .map(|&extent| vec![[0.0; 2]; extent])
401 .collect();
402 let displaced = options
403 .displacements
404 .iter()
405 .map(|r| {
406 (0..lattice.n_spins)
407 .map(|i| {
408 r.iter()
409 .enumerate()
410 .map(|(d, &delta)| {
411 let extent = lattice.shape[d] as isize;
412 let coordinate =
413 ((i / lattice.strides[d]) % lattice.shape[d]) as isize;
414 ((coordinate + delta.rem_euclid(extent)).rem_euclid(extent)
415 as usize)
416 * lattice.strides[d]
417 })
418 .sum()
419 })
420 .collect()
421 })
422 .collect();
423 Ok(Self {
424 fields,
425 phases,
426 planes,
427 angles: if options.vortices {
428 vec![0.0; lattice.n_spins]
429 } else {
430 vec![]
431 },
432 displaced,
433 options: options.clone(),
434 components,
435 sums: vec![vec![0.0; width]; n_temps],
436 block_sums: if options.block_size.is_some() {
437 vec![vec![0.0; width]; n_temps]
438 } else {
439 vec![]
440 },
441 block_temperatures: vec![],
442 block_count: 0,
443 block_sweeps: 0,
444 blocks: vec![],
445 count: 0,
446 scratch: vec![0.0; width],
447 overlap_offset: None,
448 overlap_sites: vec![],
449 })
450 }
451
452 pub fn with_overlap(mut self, lattice: &Lattice) -> Self {
457 let offset = self.scratch.len();
458 let mut width = offset;
459 for (name, n) in [
460 ("overlap2", 1),
461 ("overlap4", 1),
462 ("overlap_structure_factor_min", lattice.n_dims),
463 ("overlap2_all_pairs", 1),
464 ] {
465 self.fields.push(MomentField {
466 name,
467 width: n,
468 offset: width,
469 });
470 width += n;
471 }
472 for row in self.sums.iter_mut().chain(&mut self.block_sums) {
473 row.resize(width, 0.0);
474 }
475 self.scratch.resize(width, 0.0);
476 self.overlap_offset = Some(offset);
477 self.overlap_sites = vec![0; lattice.n_spins];
478 self
479 }
480
481 pub fn measure_overlap(
486 &mut self,
487 lattice: &Lattice,
488 a: &[i8],
489 b: &[i8],
490 occupied: &[bool],
491 temperature: usize,
492 weight: f64,
493 ) {
494 let Some(offset) = self.overlap_offset else {
495 return;
496 };
497 let mut sites = std::mem::take(&mut self.overlap_sites);
498 for ((q, &x), &y) in sites.iter_mut().zip(a).zip(b) {
499 *q = x * y;
500 }
501 let n = lattice.n_spins as f64;
502 let q = sites.iter().map(|&v| i64::from(v)).sum::<i64>() as f64 / n;
503 self.accumulate_planes(lattice, &sites, occupied);
504 self.overlap_sites = sites;
505 self.scratch[offset] = q * q;
506 self.scratch[offset + 1] = q.powi(4);
507 for d in 0..lattice.n_dims {
508 self.scratch[offset + 2 + d] = self.plane_structure_factor(d, n);
509 }
510 let end = offset + 2 + lattice.n_dims;
511 let values = &self.scratch[offset..end];
512 let rows = std::iter::once(&mut self.sums[temperature]);
513 let block_rows = self.block_sums.get_mut(temperature);
514 for row in rows.chain(block_rows) {
515 for (dst, &value) in row[offset..end].iter_mut().zip(values) {
516 *dst += weight * value;
517 }
518 }
519 }
520
521 pub fn measure_overlap_all_pairs(
526 &mut self,
527 replicas: &[&[i8]],
528 temperature: usize,
529 weight: f64,
530 ) {
531 let Some(offset) = self.overlap_offset else {
532 return;
533 };
534 let (sum, pairs) = super::overlap::pair_q2_sum(replicas.len(), |r| replicas[r]);
535 if pairs == 0 {
536 return;
537 }
538 let column = offset + 2 + self.planes.len();
539 let value = weight * sum / pairs as f64;
540 self.sums[temperature][column] += value;
541 if let Some(row) = self.block_sums.get_mut(temperature) {
542 row[column] += value;
543 }
544 }
545
546 fn plane_structure_factor(&self, d: usize, n: f64) -> f64 {
549 let mut fourier = [[0.0; 2]; 2];
550 for (plane, phase) in self.planes[d].iter().zip(&self.phases[d]) {
551 for (row, &value) in fourier.iter_mut().zip(plane) {
552 for (entry, &phase) in row.iter_mut().zip(phase) {
553 *entry += value * phase;
554 }
555 }
556 }
557 fourier.iter().flatten().map(|v| v * v).sum::<f64>() / n
558 }
559
560 pub fn measure<S: Spin>(
562 &mut self,
563 lattice: &Lattice,
564 spins: &[S],
565 couplings: &[S::Value],
566 occupied: &[bool],
567 temperature: usize,
568 ) -> [f64; 2] {
569 self.measure_impl(lattice, spins, couplings, occupied, temperature, None)
570 }
571
572 pub fn measure_cached<S: Spin>(
576 &mut self,
577 lattice: &Lattice,
578 spins: &[S],
579 couplings: &[S::Value],
580 occupied: &[bool],
581 temperature: usize,
582 cached: CachedTotals,
583 ) -> [f64; 2] {
584 debug_assert_eq!(self.components, 1);
585 self.measure_impl(
586 lattice,
587 spins,
588 couplings,
589 occupied,
590 temperature,
591 Some(cached),
592 )
593 }
594
595 fn measure_impl<S: Spin>(
596 &mut self,
597 lattice: &Lattice,
598 spins: &[S],
599 couplings: &[S::Value],
600 occupied: &[bool],
601 temperature: usize,
602 cached: Option<CachedTotals>,
603 ) -> [f64; 2] {
604 let n = lattice.n_spins as f64;
605 self.scratch.fill(0.0);
606 let i2_offset = 5 + 2 * lattice.n_dims;
607 let sk_offset = if self.components == 2 {
608 5 + 3 * lattice.n_dims
609 } else {
610 5
611 };
612 let (energy, magnetization) = match cached {
613 Some(totals) if self.components == 1 => (totals.energy, totals.magnetization),
614 _ => self.bond_pass(lattice, spins, couplings, occupied),
615 };
616 let m2 = dot(magnetization, magnetization) / (n * n);
617 self.scratch[..5].copy_from_slice(&[energy, energy * energy, m2.sqrt(), m2, m2 * m2]);
618 self.accumulate_planes(lattice, spins, occupied);
619 for d in 0..lattice.n_dims {
620 if self.components == 2 {
621 self.scratch[i2_offset + d] = self.scratch[5 + lattice.n_dims + d].powi(2);
622 }
623 self.scratch[sk_offset + d] = self.plane_structure_factor(d, n);
624 }
625 let correlation_offset = sk_offset + lattice.n_dims;
626 for (r, neighbors) in self.displaced.iter().enumerate() {
627 self.scratch[correlation_offset + r] = neighbors
628 .iter()
629 .enumerate()
630 .filter(|&(i, &j)| occupied[i] && occupied[j])
631 .map(|(i, &j)| dot(spins[i].components(), spins[j].components()))
632 .sum::<f64>()
633 / n;
634 }
635 if self.options.vortices {
636 let offset = correlation_offset + self.displaced.len();
637 let [density, intact] = self.vortices(lattice, spins, couplings, occupied);
638 self.scratch[offset] = density;
639 self.scratch[offset + 1] = intact;
640 }
641 for (dst, &value) in self.sums[temperature].iter_mut().zip(&self.scratch) {
642 *dst += value;
643 }
644 if self.options.block_size.is_some() {
645 for (dst, &value) in self.block_sums[temperature].iter_mut().zip(&self.scratch) {
646 *dst += value;
647 }
648 }
649 [energy, m2]
650 }
651
652 fn bond_pass<S: Spin>(
654 &mut self,
655 lattice: &Lattice,
656 spins: &[S],
657 couplings: &[S::Value],
658 occupied: &[bool],
659 ) -> (f64, [f64; 2]) {
660 let d_offset = 5;
661 let i_offset = 5 + lattice.n_dims;
662 let mut magnetization = [0.0; 2];
663 let mut interaction = 0.0;
664 for i in 0..lattice.n_spins {
665 let spin = spins[i].components();
666 if occupied[i] {
667 magnetization[0] += spin[0];
668 magnetization[1] += spin[1];
669 }
670 for d in 0..lattice.n_neighbors {
671 let j = lattice.neighbor_fwd(i, d);
672 let coupling = couplings[i * lattice.n_neighbors + d].to_f64();
673 let neighbor = spins[j].components();
674 let cosine = coupling * dot(spin, neighbor);
675 interaction += cosine;
676 if self.components == 2 {
677 self.scratch[d_offset + d] += cosine;
678 self.scratch[i_offset + d] += coupling * cross(spin, neighbor);
679 }
680 }
681 }
682 (-interaction / lattice.n_spins as f64, magnetization)
683 }
684
685 fn accumulate_planes<S: Spin>(&mut self, lattice: &Lattice, spins: &[S], occupied: &[bool]) {
688 for (d, plane) in self.planes.iter_mut().enumerate() {
689 plane.fill([0.0; 2]);
690 let stride = lattice.strides[d];
691 let period = stride * lattice.shape[d];
692 for (spin_period, mask_period) in spins
693 .chunks_exact(period)
694 .zip(occupied.chunks_exact(period))
695 {
696 if stride == 1 {
697 for (sum, (spin, &occ)) in
698 plane.iter_mut().zip(spin_period.iter().zip(mask_period))
699 {
700 let [a, b] = masked(*spin, occ);
701 sum[0] += a;
702 sum[1] += b;
703 }
704 continue;
705 }
706 let slabs = spin_period
707 .chunks_exact(stride)
708 .zip(mask_period.chunks_exact(stride));
709 for (sum, (slab, mask)) in plane.iter_mut().zip(slabs) {
710 let [a, b] = masked_sum(slab, mask);
711 sum[0] += a;
712 sum[1] += b;
713 }
714 }
715 }
716 }
717
718 fn vortices<S: Spin>(
720 &mut self,
721 lattice: &Lattice,
722 spins: &[S],
723 couplings: &[S::Value],
724 occupied: &[bool],
725 ) -> [f64; 2] {
726 for (angle, spin) in self.angles.iter_mut().zip(spins) {
727 let [c, s] = spin.components();
728 *angle = s.atan2(c);
729 }
730 let angles = &self.angles;
731 let edge = |a: usize, b: usize| {
733 let delta = angles[b] - angles[a];
734 delta - TAU * (delta / TAU).round()
735 };
736 let mut intact = 0;
737 let mut winding = 0.0;
738 for i in 0..lattice.n_spins {
739 let x = lattice.neighbor_fwd(i, 0);
740 let y = lattice.neighbor_fwd(i, 1);
741 let xy = lattice.neighbor_fwd(x, 1);
742 if ![i, x, xy, y].iter().all(|&j| occupied[j])
743 || [2 * i, 2 * i + 1, 2 * x + 1, 2 * y]
744 .iter()
745 .any(|&b| couplings[b].to_f64() == 0.0)
746 {
747 continue;
748 }
749 intact += 1;
750 let circulation = edge(i, x) + edge(x, xy) + edge(xy, y) + edge(y, i);
751 winding += (circulation / TAU).round().abs();
752 }
753 let density = if intact > 0 {
754 winding / intact as f64
755 } else {
756 f64::NAN
757 };
758 [density, intact as f64 / lattice.n_spins as f64]
759 }
760
761 pub fn end_sweep(&mut self, n_replicas: usize) {
762 self.count += n_replicas as u64;
763 if self.options.block_size.is_none() {
764 return;
765 }
766 self.block_count += n_replicas as u64;
767 self.block_sweeps += 1;
768 if self.options.block_size == Some(self.block_sweeps) {
769 self.flush_block();
770 }
771 }
772 pub(crate) fn own_block_temperatures(&mut self, owned: impl Fn(usize) -> bool) {
776 self.block_temperatures = (0..self.sums.len()).map(owned).collect();
777 }
778
779 fn flush_block(&mut self) {
780 if self.block_count == 0 {
781 return;
782 }
783 let sums = self
784 .block_sums
785 .iter()
786 .enumerate()
787 .map(|(t, row)| {
788 let stored = self.block_temperatures.get(t).copied().unwrap_or(true);
789 if stored {
790 row.clone()
791 } else {
792 Vec::new()
793 }
794 })
795 .collect();
796 self.blocks.push(MomentBlock {
797 sums,
798 count: self.block_count,
799 sweeps: self.block_sweeps,
800 });
801 for row in &mut self.block_sums {
802 row.fill(0.0);
803 }
804 self.block_count = 0;
805 self.block_sweeps = 0;
806 }
807 pub fn finish(mut self) -> PhysicsResult {
809 self.flush_block();
810 for value in self.sums.iter_mut().flatten() {
811 *value /= self.count as f64;
812 }
813 PhysicsResult {
814 fields: self.fields,
815 moments: self.sums,
816 count: self.count,
817 blocks: self.blocks,
818 }
819 }
820}
821
822#[inline]
823fn masked<S: Spin>(spin: S, occupied: bool) -> [f64; 2] {
824 let weight = f64::from(u8::from(occupied));
825 let [a, b] = spin.components();
826 [weight * a, weight * b]
827}
828
829fn masked_sum<S: Spin>(slab: &[S], mask: &[bool]) -> [f64; 2] {
832 let mut lanes = [[0.0; 2]; 4];
833 let (spins4, spin_tail) = slab.as_chunks::<4>();
834 let (masks4, mask_tail) = mask.as_chunks::<4>();
835 let mut tail = [0.0; 2];
836 for (&spin, &occ) in spin_tail.iter().zip(mask_tail) {
837 let [a, b] = masked(spin, occ);
838 tail[0] += a;
839 tail[1] += b;
840 }
841 for (spins, masks) in spins4.iter().zip(masks4) {
842 for ((lane, &spin), &occ) in lanes.iter_mut().zip(spins).zip(masks) {
843 let [a, b] = masked(spin, occ);
844 lane[0] += a;
845 lane[1] += b;
846 }
847 }
848 let [l0, l1, l2, l3] = lanes;
849 [
850 (l0[0] + l1[0]) + (l2[0] + l3[0]) + tail[0],
851 (l0[1] + l1[1]) + (l2[1] + l3[1]) + tail[1],
852 ]
853}
854
855#[cfg(test)]
856mod tests {
857 use super::*;
858 use rand::{Rng, SeedableRng};
859 use rand_xoshiro::Xoshiro256StarStar;
860 #[test]
861 fn signed_winding_mask_coverage_and_rotation_invariance() {
862 let lattice = Lattice::new(vec![3, 3]);
863 let mut spins = vec![[1.0, 0.0]; 9];
864 spins[3] = [0.0, 1.0];
865 spins[4] = [-1.0, 0.0];
866 spins[1] = [0.0, -1.0];
867 let mut j = vec![0.0; 18];
868 for (bond, value) in [(0, 1.0), (7, -1.0), (2, 1.0), (1, 1.0)] {
869 j[bond] = value;
870 }
871 let mut occupied = vec![false; 9];
872 for i in [0, 3, 4, 1, 8] {
873 occupied[i] = true;
874 }
875 let options = PhysicsOptions {
876 vortices: true,
877 displacements: vec![vec![1, 0], vec![0, 0]],
878 block_size: Some(2),
879 };
880 let measure = |s: &[[f64; 2]], j: &[f64]| {
881 let mut collector = PhysicsCollector::new(&lattice, 1, &options, 2).unwrap();
882 collector.measure(&lattice, s, j, &occupied, 0);
883 collector.end_sweep(1);
884 collector.finish().values(&lattice.shape, &[1.0], 2)
885 };
886 let original = measure(&spins, &j);
887 assert_eq!(original["angle_vortex_density"][0][0], 1.0);
888 assert_eq!(original["intact_plaquette_fraction"][0][0], 1.0 / 9.0);
889 assert_eq!(original["correlations"][0][1], 5.0 / 9.0);
890 for s in &mut spins {
891 *s = [0.6 * s[0] - 0.8 * s[1], 0.8 * s[0] + 0.6 * s[1]];
892 }
893 let rotated = measure(&spins, &j);
894 for (name, rows) in &original {
895 for (a, b) in rows.iter().flatten().zip(rotated[name].iter().flatten()) {
896 assert!(
897 (a - b).abs() < 1e-12 || (a.is_nan() && b.is_nan()),
898 "{name}: {a} vs {b}"
899 );
900 }
901 }
902 j[0] = 0.0;
903 let broken = measure(&spins, &j);
904 assert!(broken["angle_vortex_density"][0][0].is_nan());
905 assert_eq!(broken["intact_plaquette_fraction"][0][0], 0.0);
906 }
907 #[test]
908 fn disorder_variance_is_excluded_from_heat_capacity() {
909 let lattice = Lattice::new(vec![3, 3]);
910 let spins = vec![[1.0, 0.0]; 9];
911 let occupied = vec![true; 9];
912 let results: Vec<_> = [1.0, 2.0]
913 .iter()
914 .map(|&j| {
915 let mut c =
916 PhysicsCollector::new(&lattice, 1, &PhysicsOptions::default(), 2).unwrap();
917 for _ in 0..3 {
918 c.measure(&lattice, &spins, &[j; 18], &occupied, 0);
919 c.end_sweep(1);
920 }
921 c.finish()
922 })
923 .collect();
924 let aggregate = PhysicsResult::aggregate(&results, &[3, 3], &[1.0], 2);
925 assert_eq!(aggregate["energies"][0][0], -3.0);
926 assert_eq!(aggregate["energies2"][0][0], 10.0);
927 assert_eq!(aggregate["heat_capacity"][0][0], 0.0);
928 assert_eq!(aggregate["binder_cumulant"][0][0], 0.5);
929 }
930 fn random_xy(n: usize, seed: u64) -> Vec<[f64; 2]> {
931 let mut rng = Xoshiro256StarStar::seed_from_u64(seed);
932 (0..n).map(|_| <[f64; 2]>::random(&mut rng)).collect()
933 }
934
935 fn direct_structure_factor(
937 lattice: &Lattice,
938 spins: &[[f64; 2]],
939 occupied: &[bool],
940 ) -> Vec<f64> {
941 (0..lattice.n_dims)
942 .map(|d| {
943 let mut f = [[0.0; 2]; 2];
944 for (i, spin) in spins.iter().enumerate().filter(|&(i, _)| occupied[i]) {
945 let x = (i / lattice.strides[d]) % lattice.shape[d];
946 let (sin, cos) = (TAU * x as f64 / lattice.shape[d] as f64).sin_cos();
947 for k in 0..2 {
948 f[k][0] += spin[k] * cos;
949 f[k][1] += spin[k] * sin;
950 }
951 }
952 f.iter().flatten().map(|v| v * v).sum::<f64>() / lattice.n_spins as f64
953 })
954 .collect()
955 }
956
957 #[test]
958 fn plane_sums_match_direct_fourier_sums() {
959 let lattice = Lattice::new(vec![3, 4, 5]);
960 let spins = random_xy(lattice.n_spins, 3);
961 let occupied: Vec<bool> = (0..lattice.n_spins).map(|i| i % 7 != 3).collect();
962 let mut c = PhysicsCollector::new(&lattice, 1, &PhysicsOptions::default(), 2).unwrap();
963 c.measure(
964 &lattice,
965 &spins,
966 &vec![0.0; 3 * lattice.n_spins],
967 &occupied,
968 0,
969 );
970 c.end_sweep(1);
971 let got = &c.finish().values(&lattice.shape, &[1.0], 2)["structure_factor_min"][0];
972 for (a, b) in got
973 .iter()
974 .zip(direct_structure_factor(&lattice, &spins, &occupied))
975 {
976 assert!((a - b).abs() < 1e-12, "{a} vs {b}");
977 }
978 }
979
980 #[test]
981 fn angle_differences_match_per_edge_atan2_winding() {
982 let lattice = Lattice::new(vec![6, 5]);
983 let couplings = vec![1.0; 2 * lattice.n_spins];
984 let occupied = vec![true; lattice.n_spins];
985 let options = PhysicsOptions {
986 vortices: true,
987 ..Default::default()
988 };
989 for seed in 0..8 {
990 let spins = random_xy(lattice.n_spins, seed);
991 let mut c = PhysicsCollector::new(&lattice, 1, &options, 2).unwrap();
992 c.measure(&lattice, &spins, &couplings, &occupied, 0);
993 c.end_sweep(1);
994 let got = c.finish().values(&lattice.shape, &[1.0], 2)["angle_vortex_density"][0][0];
995 let vortices: f64 = (0..lattice.n_spins)
996 .map(|i| {
997 let x = lattice.neighbor_fwd(i, 0);
998 let y = lattice.neighbor_fwd(i, 1);
999 let xy = lattice.neighbor_fwd(x, 1);
1000 let circulation: f64 = [i, x, xy, y, i]
1001 .windows(2)
1002 .map(|e| {
1003 let (a, b) = (spins[e[0]], spins[e[1]]);
1004 cross(a, b).atan2(dot(a, b))
1005 })
1006 .sum();
1007 (circulation / TAU).round().abs()
1008 })
1009 .sum();
1010 assert_eq!(got, vortices / lattice.n_spins as f64, "seed {seed}");
1011 }
1012 }
1013
1014 #[test]
1015 fn cached_totals_reproduce_the_bond_pass() {
1016 let lattice = Lattice::new(vec![4, 8]);
1018 let mut rng = Xoshiro256StarStar::seed_from_u64(17);
1019 let spins: Vec<i8> = (0..lattice.n_spins).map(|_| i8::random(&mut rng)).collect();
1020 let couplings: Vec<f32> = (0..2 * lattice.n_spins)
1021 .map(|_| if rng.gen::<bool>() { 1.0 } else { -1.0 })
1022 .collect();
1023 let occupied = vec![true; lattice.n_spins];
1024 let options = PhysicsOptions {
1025 displacements: vec![vec![1, 2]],
1026 ..Default::default()
1027 };
1028 let mut energies = [0.0f32];
1029 let mut magnetization = [0i64];
1030 crate::spins::energy::compute_energies_and_magnetizations_into(
1031 &lattice,
1032 &spins,
1033 &couplings,
1034 &mut energies,
1035 &mut magnetization,
1036 );
1037 let mut full = PhysicsCollector::new(&lattice, 1, &options, 1).unwrap();
1038 let mut cached = PhysicsCollector::new(&lattice, 1, &options, 1).unwrap();
1039 let a = full.measure(&lattice, &spins, &couplings, &occupied, 0);
1040 let b = cached.measure_cached(
1041 &lattice,
1042 &spins,
1043 &couplings,
1044 &occupied,
1045 0,
1046 CachedTotals {
1047 energy: -(energies[0] as f64),
1048 magnetization: [magnetization[0] as f64, 0.0],
1049 },
1050 );
1051 assert_eq!(a, b);
1052 assert_eq!(full.sums, cached.sums);
1053 }
1054
1055 #[test]
1056 fn overlap_moments_match_direct_sums_per_pair() {
1057 let lattice = Lattice::new(vec![3, 4, 5]);
1058 let mut rng = Xoshiro256StarStar::seed_from_u64(5);
1059 let mut ising = || -> Vec<i8> {
1060 (0..lattice.n_spins)
1061 .map(|_| if rng.gen::<bool>() { 1 } else { -1 })
1062 .collect()
1063 };
1064 let (a, b) = (ising(), ising());
1065 let occupied = vec![true; lattice.n_spins];
1066 let mut c = PhysicsCollector::new(&lattice, 1, &PhysicsOptions::default(), 1)
1067 .unwrap()
1068 .with_overlap(&lattice);
1069 c.measure_overlap(&lattice, &a, &b, &occupied, 0, 2.0);
1071 c.end_sweep(2);
1072 let values = c.finish().values(&lattice.shape, &[1.0], 1);
1073
1074 let q: Vec<[f64; 2]> = a
1075 .iter()
1076 .zip(&b)
1077 .map(|(&x, &y)| [f64::from(x * y), 0.0])
1078 .collect();
1079 let mean = q.iter().map(|v| v[0]).sum::<f64>() / lattice.n_spins as f64;
1080 assert!((values["overlap2"][0][0] - mean.powi(2)).abs() < 1e-12);
1081 assert!((values["overlap4"][0][0] - mean.powi(4)).abs() < 1e-12);
1082 for (got, want) in values["overlap_structure_factor_min"][0]
1083 .iter()
1084 .zip(direct_structure_factor(&lattice, &q, &occupied))
1085 {
1086 assert!((got - want).abs() < 1e-12, "{got} vs {want}");
1087 }
1088 }
1089
1090 #[test]
1091 fn overlap2_all_pairs_averages_every_replica_pair() {
1092 let lattice = Lattice::new(vec![4, 5]);
1093 let mut rng = Xoshiro256StarStar::seed_from_u64(8);
1094 let replicas: Vec<Vec<i8>> = (0..3)
1095 .map(|_| {
1096 (0..lattice.n_spins)
1097 .map(|_| if rng.gen::<bool>() { 1 } else { -1 })
1098 .collect()
1099 })
1100 .collect();
1101 let q2 = |a: &[i8], b: &[i8]| {
1102 let dot: i64 = a.iter().zip(b).map(|(&x, &y)| i64::from(x * y)).sum();
1103 (dot as f64 / lattice.n_spins as f64).powi(2)
1104 };
1105 let expected = (q2(&replicas[0], &replicas[1])
1106 + q2(&replicas[0], &replicas[2])
1107 + q2(&replicas[1], &replicas[2]))
1108 / 3.0;
1109 let views: Vec<&[i8]> = replicas.iter().map(Vec::as_slice).collect();
1110 let mut c = PhysicsCollector::new(&lattice, 1, &PhysicsOptions::default(), 1)
1111 .unwrap()
1112 .with_overlap(&lattice);
1113 c.measure_overlap_all_pairs(&views, 0, 3.0);
1114 c.end_sweep(3);
1115 let values = c.finish().values(&lattice.shape, &[1.0], 1);
1116 assert!((values["overlap2_all_pairs"][0][0] - expected).abs() < 1e-12);
1117 assert_eq!(values["overlap2"][0][0], 0.0);
1119 }
1120
1121 #[test]
1125 fn jackknife_of_a_mean_is_the_standard_error() {
1126 let lattice = Lattice::new(vec![3, 3]);
1127 let sample = |scale: f64| {
1128 let collector =
1129 PhysicsCollector::new(&lattice, 1, &PhysicsOptions::default(), 1).unwrap();
1130 let mut result = collector.finish();
1131 result.count = 1;
1132 for (k, moment) in result.moments[0].iter_mut().enumerate() {
1133 *moment = scale * (1.0 + k as f64);
1134 }
1135 result
1136 };
1137 let samples = [sample(1.0), sample(2.0), sample(4.0)];
1138 let stderr = PhysicsResult::jackknife_stderr(&samples, &[3, 3], &[1.5], 1);
1139 let values: Vec<f64> = samples
1141 .iter()
1142 .map(|s| s.values(&[3, 3], &[1.5], 1)["mags2"][0][0])
1143 .collect();
1144 let mean = values.iter().sum::<f64>() / 3.0;
1145 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / 2.0;
1146 assert!((stderr["mags2"][0][0] - (variance / 3.0).sqrt()).abs() < 1e-12);
1147 assert!(stderr["binder_cumulant"][0][0].is_finite());
1148
1149 let single = PhysicsResult::jackknife_stderr(&samples[..1], &[3, 3], &[1.5], 1);
1150 assert!(single["mags2"][0][0].is_nan());
1151 }
1152
1153 #[test]
1154 fn finish_without_measurements_is_nan() {
1155 let lattice = Lattice::new(vec![3, 3]);
1156 let c = PhysicsCollector::new(&lattice, 2, &PhysicsOptions::default(), 1).unwrap();
1157 let result = c.finish();
1158 assert_eq!(result.count, 0);
1159 assert!(result.moments.iter().flatten().all(|v| v.is_nan()));
1160 }
1161
1162 #[test]
1163 fn antiferromagnetic_uniform_length_is_undefined_without_clamping() {
1164 let lattice = Lattice::new(vec![4, 4]);
1165 let spins: Vec<_> = (0..16)
1166 .map(|i| {
1167 if (i / 4 + i % 4) % 2 == 0 {
1168 [1.0, 0.0]
1169 } else {
1170 [-1.0, 0.0]
1171 }
1172 })
1173 .collect();
1174 let mut c = PhysicsCollector::new(
1175 &lattice,
1176 1,
1177 &PhysicsOptions {
1178 displacements: vec![vec![1, 0]],
1179 ..Default::default()
1180 },
1181 2,
1182 )
1183 .unwrap();
1184 c.measure(&lattice, &spins, &vec![-1.0; 32], &[true; 16], 0);
1185 c.end_sweep(1);
1186 let result = c.finish().values(&[4, 4], &[1.0], 2);
1187 assert_eq!(result["correlations"][0][0], -1.0);
1188 assert!(result["correlation_length"][0].iter().all(|v| v.is_nan()));
1189 }
1190}