1use super::{driver, realization::XyRealization};
3use crate::{
4 clusters::fk::embedded_update,
5 config::{
6 AutocorrelationBackend, ClusterAction, ClusterConfig, ClusterMode, PtSchedule, SimConfig,
7 SweepMode,
8 },
9 geometry::Lattice,
10 spins::xy::{interaction, local_sweep, LocalMove, XyEmbedding},
11 statistics::{
12 autocorrelation::PrecisionAutocorr,
13 physics::{MomentBlock, PhysicsCollector, PhysicsOptions, PhysicsResult, PhysicsValues},
14 },
15};
16use rayon::prelude::*;
17use std::sync::atomic::AtomicBool;
18
19#[derive(Debug)]
20pub struct XyConfig {
21 pub simulation: SimConfig,
22 pub cluster_updates: usize,
24 pub overrelaxation_sweeps: usize,
25 pub physics: PhysicsOptions,
26 pub measure_interval: usize,
31}
32impl Default for XyConfig {
33 fn default() -> Self {
34 Self {
35 simulation: SimConfig {
36 n_sweeps: 1000,
37 warmup_sweeps: 250,
38 sweep_mode: SweepMode::Metropolis,
39 cluster_update: Some(ClusterConfig {
40 interval: 1,
41 mode: ClusterMode::Sw,
42 action: ClusterAction::Update,
43 collect_stats: false,
44 }),
45 pt_interval: None,
46 pt_schedule: PtSchedule::SingleRandomEdge,
47 overlap_cluster: None,
48 autocorrelation_max_lag: None,
49 autocorrelation_backend: AutocorrelationBackend::Ring,
50 sequential: false,
51 equilibration_diagnostic: false,
52 conditional_link_overlap: true,
53 },
54 cluster_updates: 1,
55 overrelaxation_sweeps: 0,
56 physics: PhysicsOptions::default(),
57 measure_interval: 1,
58 }
59 }
60}
61impl XyConfig {
62 fn validate(&self) -> Result<(), String> {
63 let cfg = &self.simulation;
64 cfg.validate()?;
65 if cfg.overlap_cluster.is_some() || cfg.equilibration_diagnostic {
66 return Err(
67 "overlap updates and Ising equilibration diagnostics are not supported for XY"
68 .into(),
69 );
70 }
71 if self.cluster_updates == 0 {
72 return Err("cluster_updates must be positive".into());
73 }
74 if self.measure_interval == 0 {
75 return Err("measure_interval must be positive".into());
76 }
77 if cfg
78 .cluster_update
79 .as_ref()
80 .is_some_and(|c| c.action != ClusterAction::Update || c.collect_stats)
81 {
82 return Err("XY clusters support updates and visited-spin counts; Ising graph collectors are unsupported".into());
83 }
84 if cfg.sweep_mode == SweepMode::None && cfg.cluster_update.is_none() {
85 return Err("XY sampling requires Metropolis, Gibbs or cluster updates".into());
86 }
87 Ok(())
88 }
89}
90
91pub struct XyDisorderResult {
92 pub physics: PhysicsResult,
93 pub energy_tau: Vec<f64>,
95 pub mags2_tau: Vec<f64>,
96 pub energy_tau_converged: Vec<bool>,
98 pub mags2_tau_converged: Vec<bool>,
99 pub cluster_updates: Vec<u64>,
101 pub visited_spins: Vec<u64>,
103 pub metropolis_accepted: Vec<u64>,
107 pub metropolis_attempts: Vec<u64>,
108}
109pub struct XyResult {
110 pub values: PhysicsValues,
111 pub per_disorder: Vec<XyDisorderResult>,
112}
113
114pub struct XySimulation {
126 pub lattice: Lattice,
127 pub realizations: Vec<XyRealization>,
128 pub occupation: Vec<Vec<bool>>,
129 pub n_replicas: usize,
130 pub n_temps: usize,
131 constructor_seed: u64,
132}
133impl XySimulation {
134 pub fn new(
135 shape: Vec<usize>,
136 couplings: Vec<Vec<f64>>,
137 temperatures: &[f64],
138 n_replicas: usize,
139 occupation: Option<Vec<Vec<bool>>>,
140 seed: u64,
141 ) -> Result<Self, String> {
142 if shape.is_empty() || shape.iter().any(|&l| l < 3) {
143 return Err("XY hypercubic extents must be at least three".into());
144 }
145 let n = shape
146 .iter()
147 .try_fold(1usize, |n, &l| n.checked_mul(l))
148 .ok_or("lattice size overflow")?;
149 if n > u32::MAX as usize || n.checked_mul(shape.len()).is_none() {
150 return Err("lattice is too large".into());
151 }
152 if n_replicas == 0
153 || temperatures.is_empty()
154 || temperatures.iter().any(|t| !t.is_finite() || *t <= 0.0)
155 {
156 return Err(
157 "temperatures must be nonempty, positive and finite; n_replicas must be positive"
158 .into(),
159 );
160 }
161 n_replicas
162 .checked_mul(temperatures.len())
163 .and_then(|systems| systems.checked_mul(n))
164 .ok_or("spin count overflow")?;
165 if couplings.is_empty()
166 || couplings
167 .iter()
168 .any(|j| j.len() != n * shape.len() || j.iter().any(|x| !x.is_finite()))
169 {
170 return Err("couplings must have the lattice shape and be finite".into());
171 }
172 let occupation = occupation.unwrap_or_else(|| vec![vec![true; n]; couplings.len()]);
173 if occupation.len() != couplings.len() || occupation.iter().any(|m| m.len() != n) {
174 return Err("occupation must match the disorder and lattice dimensions".into());
175 }
176 let lattice = Lattice::new(shape);
177 let realizations = couplings
178 .into_iter()
179 .enumerate()
180 .map(|(d, mut j)| {
181 for i in 0..n {
182 for axis in 0..lattice.n_dims {
183 if !occupation[d][i] || !occupation[d][lattice.neighbor_fwd(i, axis)] {
184 j[i * lattice.n_dims + axis] = 0.0;
185 }
186 }
187 }
188 XyRealization::new(
189 &lattice,
190 j,
191 temperatures,
192 n_replicas,
193 super::realization::realization_seed(seed, d),
194 )
195 })
196 .collect();
197 Ok(Self {
198 lattice,
199 realizations,
200 occupation,
201 n_replicas,
202 n_temps: temperatures.len(),
203 constructor_seed: seed,
204 })
205 }
206 pub fn reset(&mut self, seed: Option<u64>) {
207 for (d, real) in self.realizations.iter_mut().enumerate() {
208 real.reset(
209 &self.lattice,
210 self.n_replicas,
211 self.n_temps,
212 super::realization::realization_seed(seed.unwrap_or(self.constructor_seed), d),
213 );
214 }
215 }
216 pub fn sample(
217 &mut self,
218 config: &XyConfig,
219 interrupted: &AtomicBool,
220 on_sweep: &(dyn Fn() + Sync),
221 ) -> Result<XyResult, String> {
222 config.validate()?;
223 driver::validate_batch(
224 &self.lattice,
225 &self.realizations,
226 self.n_replicas,
227 self.n_temps,
228 &config.simulation,
229 )?;
230 config.physics.validate(&self.lattice, 2)?;
232 if self.occupation.len() != self.realizations.len()
233 || self
234 .occupation
235 .iter()
236 .any(|m| m.len() != self.lattice.n_spins)
237 {
238 return Err("occupation dimensions changed".into());
239 }
240 for (real, mask) in self.realizations.iter().zip(&self.occupation) {
241 if real.spins.iter().any(|s| {
243 let excess = (s[0] * s[0] + s[1] * s[1] - 1.0).abs();
244 excess.is_nan() || excess > 2e-8
245 }) {
246 return Err("XY spins must be finite unit vectors".into());
247 }
248 for i in 0..self.lattice.n_spins {
249 for d in 0..self.lattice.n_dims {
250 if (!mask[i] || !mask[self.lattice.neighbor_fwd(i, d)])
251 && real.couplings[i * self.lattice.n_dims + d] != 0.0
252 {
253 return Err("vacant sites must have zero incident couplings".into());
254 }
255 }
256 }
257 }
258 let sequential = driver::effective_sequential(&config.simulation, self.realizations.len());
259 let measurement_groups = if sequential {
261 1
262 } else {
263 (rayon::current_num_threads() / self.realizations.len()).max(1)
264 };
265 let per_disorder = driver::map_realizations(&mut self.realizations, |d, real| {
266 run_xy(
267 &self.lattice,
268 real,
269 &self.occupation[d],
270 self.n_replicas,
271 self.n_temps,
272 config,
273 sequential,
274 measurement_groups,
275 interrupted,
276 on_sweep,
277 )
278 })?;
279 let values = PhysicsResult::aggregate(
280 per_disorder.iter().map(|r| &r.physics),
281 &self.lattice.shape,
282 &self.realizations[0].temperatures[..self.n_temps],
283 2,
284 );
285 Ok(XyResult {
286 values,
287 per_disorder,
288 })
289 }
290}
291
292struct Measurement {
297 groups: Vec<PhysicsCollector>,
298}
299impl Measurement {
300 fn new(
301 lattice: &Lattice,
302 n_temps: usize,
303 options: &PhysicsOptions,
304 max_groups: usize,
305 ) -> Result<Self, String> {
306 let n_groups = n_temps.min(max_groups).max(1);
307 let groups = (0..n_groups)
308 .map(|group| {
309 let mut collector = PhysicsCollector::new(lattice, n_temps, options, 2)?;
310 if n_groups > 1 {
311 collector.own_block_temperatures(|t| t % n_groups == group);
312 }
313 Ok(collector)
314 })
315 .collect::<Result<_, String>>()?;
316 Ok(Self { groups })
317 }
318
319 fn measure(
321 &mut self,
322 lattice: &Lattice,
323 real: &XyRealization,
324 occupation: &[bool],
325 n_temps: usize,
326 out: &mut [[f64; 2]],
327 ) {
328 let n_groups = self.groups.len();
329 let n = lattice.n_spins;
330 let measure_group = |(group, collector): (usize, &mut PhysicsCollector)| {
331 let mut measured = Vec::new();
332 for t in (group..n_temps).step_by(n_groups) {
333 for slot in (t..real.system_ids.len()).step_by(n_temps) {
334 let id = real.system_ids[slot];
335 let spins = &real.spins[id * n..(id + 1) * n];
336 let values = collector.measure(lattice, spins, &real.couplings, occupation, t);
337 measured.push((slot, values));
338 }
339 }
340 measured
341 };
342 let measured: Vec<_> = if n_groups == 1 {
343 vec![measure_group((0, &mut self.groups[0]))]
344 } else {
345 self.groups
346 .par_iter_mut()
347 .enumerate()
348 .map(measure_group)
349 .collect()
350 };
351 for (slot, values) in measured.into_iter().flatten() {
352 out[slot] = values;
353 }
354 }
355
356 fn end_sweep(&mut self, n_replicas: usize) {
357 for collector in &mut self.groups {
358 collector.end_sweep(n_replicas);
359 }
360 }
361
362 fn finish(self, n_temps: usize) -> PhysicsResult {
363 let mut groups: Vec<_> = self.groups.into_iter().map(|c| c.finish()).collect();
364 if groups.len() == 1 {
365 return groups.pop().unwrap();
366 }
367 let n_groups = groups.len();
368 let owner = |t: usize| &groups[t % n_groups];
369 let blocks = (0..groups[0].blocks.len())
370 .map(|b| MomentBlock {
371 sums: (0..n_temps)
372 .map(|t| owner(t).blocks[b].sums[t].clone())
373 .collect(),
374 count: groups[0].blocks[b].count,
375 sweeps: groups[0].blocks[b].sweeps,
376 })
377 .collect();
378 PhysicsResult {
379 fields: groups[0].fields.clone(),
380 moments: (0..n_temps).map(|t| owner(t).moments[t].clone()).collect(),
381 count: groups[0].count,
382 blocks,
383 }
384 }
385}
386
387#[allow(clippy::too_many_arguments)]
388fn run_xy(
389 lattice: &Lattice,
390 real: &mut XyRealization,
391 occupation: &[bool],
392 n_replicas: usize,
393 n_temps: usize,
394 config: &XyConfig,
395 sequential: bool,
396 measurement_groups: usize,
397 interrupted: &AtomicBool,
398 on_sweep: &(dyn Fn() + Sync),
399) -> Result<XyDisorderResult, String> {
400 let cfg = &config.simulation;
401 let mut physics = Measurement::new(lattice, n_temps, &config.physics, measurement_groups)?;
402 let n_measures = cfg.n_measurement_sweeps().div_ceil(config.measure_interval);
403 let lag = cfg
404 .autocorrelation_max_lag
405 .map(|lag| lag.min(n_measures / 4).max(1));
406 let mut energy_ac = lag.map(|k| {
407 PrecisionAutocorr::<f64>::with_backend(k, n_temps, cfg.autocorrelation_backend, n_measures)
408 });
409 let mut m2_ac = lag.map(|k| {
410 PrecisionAutocorr::<f64>::with_backend(k, n_temps, cfg.autocorrelation_backend, n_measures)
411 });
412 let mut energy_buf = vec![0.0; n_temps];
413 let mut m2_buf = vec![0.0; n_temps];
414 let mut measured = vec![[0.0; 2]; real.system_ids.len()];
415 let mut visits = vec![0; n_temps * n_replicas];
416 let mut updates = vec![0; n_temps];
417 let mut metropolis_accepted = vec![0u64; n_temps * n_replicas];
418 let mut metropolis_passes = 0u64;
419 let occupied = (!occupation.iter().all(|&o| o)).then_some(occupation);
420 let local_move = match cfg.sweep_mode {
421 SweepMode::Metropolis => Some(LocalMove::Metropolis),
422 SweepMode::Gibbs => Some(LocalMove::HeatBath),
423 SweepMode::None => None,
424 };
425 driver::run_sweeps(cfg, interrupted, on_sweep, real.schedule.sweeps, |step| {
426 let moves = local_move.into_iter().chain(std::iter::repeat_n(
427 LocalMove::Overrelaxation,
428 config.overrelaxation_sweeps,
429 ));
430 for kind in moves {
431 let count = step.record && kind == LocalMove::Metropolis;
432 metropolis_passes += u64::from(count);
433 local_sweep(
434 lattice,
435 &mut real.spins,
436 &real.couplings,
437 &real.temperatures,
438 &real.system_ids,
439 &mut real.rngs,
440 occupied,
441 sequential,
442 kind,
443 count.then_some(metropolis_accepted.as_mut_slice()),
444 );
445 }
446 if step.cluster {
447 let cluster = cfg.cluster_update.as_ref().unwrap();
448 for _ in 0..config.cluster_updates {
449 embedded_update::<XyEmbedding>(
450 lattice,
451 &mut real.spins,
452 &real.couplings,
453 &real.temperatures,
454 &real.system_ids,
455 &mut real.rngs,
456 cluster.mode == ClusterMode::Wolff,
457 ClusterAction::Update,
458 None,
459 None,
460 sequential,
461 Some(&mut visits),
462 );
463 for count in &mut updates {
464 *count += n_replicas as u64;
465 }
466 }
467 }
468 let measure =
469 step.record && (step.index - cfg.warmup_sweeps).is_multiple_of(config.measure_interval);
470 if measure {
471 physics.measure(lattice, real, occupation, n_temps, &mut measured);
472 energy_buf.fill(0.0);
473 m2_buf.fill(0.0);
474 for (slot, &id) in real.system_ids.iter().enumerate() {
475 let t = slot % n_temps;
476 real.energies[id] = -measured[slot][0];
477 energy_buf[t] += measured[slot][0] / n_replicas as f64;
478 m2_buf[t] += measured[slot][1] / n_replicas as f64;
479 }
480 physics.end_sweep(n_replicas);
481 if let Some(ac) = &mut energy_ac {
482 ac.push(&energy_buf);
483 }
484 if let Some(ac) = &mut m2_ac {
485 ac.push(&m2_buf);
486 }
487 } else if step.temper {
488 let n = lattice.n_spins;
489 let refresh = |(energy, spins): (&mut f64, &[[f64; 2]])| {
490 *energy = interaction(lattice, spins, &real.couplings) / n as f64;
491 };
492 if sequential {
493 real.energies
494 .iter_mut()
495 .zip(real.spins.chunks_exact(n))
496 .for_each(refresh);
497 } else {
498 real.energies
499 .par_iter_mut()
500 .zip(real.spins.par_chunks_exact(n))
501 .for_each(refresh);
502 }
503 }
504 if step.temper {
505 real.temper(lattice.n_spins, n_replicas, n_temps, cfg.pt_schedule);
506 }
507 real.schedule.sweeps += 1;
508 })?;
509 let (energy_tau, energy_tau_converged) = driver::tau_estimates(energy_ac.as_ref());
510 let (mags2_tau, mags2_tau_converged) = driver::tau_estimates(m2_ac.as_ref());
511 let per_temperature = |slots: Vec<u64>| {
512 let mut totals = vec![0; n_temps];
513 for (slot, count) in slots.into_iter().enumerate() {
514 totals[slot % n_temps] += count;
515 }
516 totals
517 };
518 let n_occupied = occupation.iter().filter(|&&o| o).count() as u64;
519 Ok(XyDisorderResult {
520 physics: physics.finish(n_temps),
521 energy_tau,
522 mags2_tau,
523 energy_tau_converged,
524 mags2_tau_converged,
525 cluster_updates: updates,
526 visited_spins: per_temperature(visits),
527 metropolis_accepted: per_temperature(metropolis_accepted),
528 metropolis_attempts: vec![metropolis_passes * n_replicas as u64 * n_occupied; n_temps],
529 })
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535
536 fn sample_with_threads(sequential: bool, threads: usize) -> (XyResult, Vec<u64>) {
537 let couplings = (0..40).map(|i| ((i * 7) % 11) as f64 / 5.0 - 0.6).collect();
538 let occupation = (0..20).map(|i| i != 6).collect();
539 let temperatures = [0.5, 0.8, 1.1, 1.6, 2.4];
540 let mut simulation = XySimulation::new(
541 vec![5, 4],
542 vec![couplings],
543 &temperatures,
544 2,
545 Some(vec![occupation]),
546 21,
547 )
548 .unwrap();
549 let mut config = XyConfig::default();
550 let cfg = &mut config.simulation;
551 (cfg.n_sweeps, cfg.warmup_sweeps, cfg.sequential) = (64, 21, sequential);
552 (cfg.pt_interval, cfg.autocorrelation_max_lag) = (Some(1), Some(4));
553 config.overrelaxation_sweeps = 1;
554 config.physics = PhysicsOptions {
555 displacements: vec![vec![1, 1]],
556 vortices: true,
557 block_size: Some(7),
558 };
559 let pool = rayon::ThreadPoolBuilder::new()
560 .num_threads(threads)
561 .build()
562 .unwrap();
563 let result = pool
564 .install(|| simulation.sample(&config, &AtomicBool::new(false), &|| {}))
565 .unwrap();
566 let spins = simulation.realizations[0]
567 .spins
568 .iter()
569 .flatten()
570 .map(|v| v.to_bits())
571 .collect();
572 (result, spins)
573 }
574
575 #[test]
578 fn measurement_stride_and_metropolis_counts() {
579 let mut xy =
580 XySimulation::new(vec![4, 4], vec![vec![1.0; 32]], &[0.4, 2.0], 2, None, 7).unwrap();
581 let mut config = XyConfig::default();
582 config.simulation.n_sweeps = 25;
583 config.simulation.warmup_sweeps = 5;
584 config.simulation.cluster_update = None;
585 config.measure_interval = 3;
586 let result = xy.sample(&config, &AtomicBool::new(false), &|| {}).unwrap();
587 let disorder = &result.per_disorder[0];
588 assert_eq!(disorder.physics.count, 14);
590 assert_eq!(disorder.metropolis_attempts, vec![20 * 2 * 16; 2]);
591 let rate: Vec<f64> = disorder
592 .metropolis_accepted
593 .iter()
594 .zip(&disorder.metropolis_attempts)
595 .map(|(&accepted, &attempts)| accepted as f64 / attempts as f64)
596 .collect();
597 assert!(
598 rate.iter().all(|&rate| rate > 0.0 && rate <= 1.0),
599 "{rate:?}"
600 );
601 assert!(
602 rate[0] < rate[1],
603 "colder temperatures accept fewer proposals: {rate:?}"
604 );
605
606 config.measure_interval = 0;
607 assert!(xy.sample(&config, &AtomicBool::new(false), &|| {}).is_err());
608 }
609
610 #[test]
611 fn parallel_measurement_matches_sequential_for_any_thread_count() {
612 let bits = |rows: &[Vec<f64>]| -> Vec<Vec<u64>> {
613 rows.iter()
614 .map(|row| row.iter().map(|v| v.to_bits()).collect())
615 .collect()
616 };
617 let (reference, reference_spins) = sample_with_threads(true, 1);
618 let expected = &reference.per_disorder[0];
619 for threads in [2, 3, 8] {
621 let (result, spins) = sample_with_threads(false, threads);
622 assert_eq!(spins, reference_spins);
623 for (name, rows) in &reference.values {
624 assert_eq!(bits(rows), bits(&result.values[name]), "{name}");
625 }
626 let actual = &result.per_disorder[0];
627 assert_eq!(
628 bits(&actual.physics.moments),
629 bits(&expected.physics.moments)
630 );
631 assert_eq!(actual.physics.blocks.len(), expected.physics.blocks.len());
632 for (a, b) in actual.physics.blocks.iter().zip(&expected.physics.blocks) {
633 assert_eq!((a.count, a.sweeps), (b.count, b.sweeps));
634 assert_eq!(bits(&a.sums), bits(&b.sums));
635 }
636 assert_eq!(
637 bits(&[actual.energy_tau.clone(), actual.mags2_tau.clone()]),
638 bits(&[expected.energy_tau.clone(), expected.mags2_tau.clone()])
639 );
640 assert_eq!(actual.visited_spins, expected.visited_spins);
641 assert_eq!(actual.metropolis_accepted, expected.metropolis_accepted);
642 }
643 }
644}