1use super::swendsen_wang::{Geometry, check_spins, magnetization, validate_graph, validate_square};
32use crate::error::{SeqError, SeqResult};
33use crate::handle::LcgRng;
34
35#[derive(Debug, Clone)]
37pub struct WolffConfig {
38 pub coupling: f64,
40 pub(crate) geometry: Geometry,
42}
43
44impl WolffConfig {
45 pub fn square(rows: usize, cols: usize, coupling: f64, periodic: bool) -> SeqResult<Self> {
47 validate_square(rows, cols, coupling)?;
48 Ok(Self {
49 coupling,
50 geometry: Geometry::Square {
51 rows,
52 cols,
53 periodic,
54 },
55 })
56 }
57
58 pub fn graph(adjacency: Vec<Vec<usize>>, coupling: f64) -> SeqResult<Self> {
60 validate_graph(&adjacency, coupling)?;
61 Ok(Self {
62 coupling,
63 geometry: Geometry::Graph { adjacency },
64 })
65 }
66
67 pub fn n_sites(&self) -> usize {
69 self.geometry.n_sites()
70 }
71}
72
73#[derive(Debug, Clone)]
78pub struct Wolff {
79 coupling: f64,
80 neighbours: Vec<Vec<usize>>,
82 n_sites: usize,
83 last_cluster_size: usize,
85}
86
87fn build_neighbours(geometry: &Geometry) -> Vec<Vec<usize>> {
89 match geometry {
90 Geometry::Square {
91 rows,
92 cols,
93 periodic,
94 } => {
95 let rows = *rows;
96 let cols = *cols;
97 let periodic = *periodic;
98 let mut nbrs = vec![Vec::new(); rows * cols];
99 for r in 0..rows {
100 for c in 0..cols {
101 let i = r * cols + c;
102 if c + 1 < cols {
104 nbrs[i].push(r * cols + (c + 1));
105 nbrs[r * cols + (c + 1)].push(i);
106 } else if periodic && cols > 1 {
107 let j = r * cols; nbrs[i].push(j);
109 nbrs[j].push(i);
110 }
111 if r + 1 < rows {
113 nbrs[i].push((r + 1) * cols + c);
114 nbrs[(r + 1) * cols + c].push(i);
115 } else if periodic && rows > 1 {
116 let j = c; nbrs[i].push(j);
118 nbrs[j].push(i);
119 }
120 }
121 }
122 nbrs
123 }
124 Geometry::Graph { adjacency } => adjacency.clone(),
125 }
126}
127
128impl Wolff {
129 pub fn new(cfg: WolffConfig) -> Self {
131 let neighbours = build_neighbours(&cfg.geometry);
132 let n_sites = cfg.geometry.n_sites();
133 Self {
134 coupling: cfg.coupling,
135 neighbours,
136 n_sites,
137 last_cluster_size: 0,
138 }
139 }
140
141 pub fn square(rows: usize, cols: usize, coupling: f64, periodic: bool) -> SeqResult<Self> {
143 Ok(Self::new(WolffConfig::square(
144 rows, cols, coupling, periodic,
145 )?))
146 }
147
148 pub fn from_graph(adjacency: Vec<Vec<usize>>, coupling: f64) -> SeqResult<Self> {
150 Ok(Self::new(WolffConfig::graph(adjacency, coupling)?))
151 }
152
153 pub fn n_sites(&self) -> usize {
155 self.n_sites
156 }
157
158 pub fn last_cluster_size(&self) -> usize {
160 self.last_cluster_size
161 }
162
163 #[inline]
167 pub fn bond_probability(&self, beta: f64) -> f64 {
168 let two_beta_j = 2.0 * beta * self.coupling;
169 if two_beta_j <= 0.0 {
170 0.0
171 } else {
172 (1.0 - (-two_beta_j).exp()).clamp(0.0, 1.0)
173 }
174 }
175
176 pub fn step(&mut self, spins: &mut [i8], beta: f64, rng: &mut LcgRng) -> SeqResult<()> {
186 check_spins(spins, self.n_sites, beta)?;
187 if self.n_sites == 0 {
188 return Err(SeqError::EmptyInput);
189 }
190
191 let p = self.bond_probability(beta);
192 let seed = rng.next_usize(self.n_sites);
193 let s_seed = spins[seed];
194
195 let mut in_cluster = vec![false; self.n_sites];
198 let mut stack: Vec<usize> = Vec::new();
199 in_cluster[seed] = true;
200 stack.push(seed);
201 let mut cluster: Vec<usize> = vec![seed];
202
203 while let Some(site) = stack.pop() {
204 for &nbr in &self.neighbours[site] {
205 if !in_cluster[nbr] && spins[nbr] == s_seed && rng.next_f64() < p {
206 in_cluster[nbr] = true;
207 stack.push(nbr);
208 cluster.push(nbr);
209 }
210 }
211 }
212
213 for &site in &cluster {
215 spins[site] = -spins[site];
216 }
217 self.last_cluster_size = cluster.len();
218 Ok(())
219 }
220
221 pub fn magnetization(&self, spins: &[i8]) -> SeqResult<f64> {
226 magnetization(spins, self.n_sites)
227 }
228
229 pub fn energy(&self, spins: &[i8]) -> SeqResult<f64> {
235 if spins.len() != self.n_sites {
236 return Err(SeqError::ShapeMismatch {
237 expected: self.n_sites,
238 got: spins.len(),
239 });
240 }
241 let mut e = 0.0;
242 for (i, nbrs) in self.neighbours.iter().enumerate() {
244 for &j in nbrs {
245 if i < j {
246 e -= self.coupling * (spins[i] as f64) * (spins[j] as f64);
247 }
248 }
249 }
250 Ok(e)
251 }
252
253 #[cfg(test)]
255 pub(crate) fn neighbours(&self) -> &[Vec<usize>] {
256 &self.neighbours
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 const BETA_ORDERED: f64 = 0.70;
265 const BETA_DISORDERED: f64 = 0.20;
266
267 fn run_chain(
268 w: &mut Wolff,
269 spins: &mut [i8],
270 beta: f64,
271 seed: u64,
272 burn: usize,
273 samples: usize,
274 ) -> f64 {
275 let mut rng = LcgRng::new(seed);
276 for _ in 0..burn {
277 w.step(spins, beta, &mut rng).expect("step");
278 }
279 let mut acc = 0.0;
280 for _ in 0..samples {
281 w.step(spins, beta, &mut rng).expect("step");
282 acc += w.magnetization(spins).expect("mag").abs();
283 }
284 acc / samples as f64
285 }
286
287 #[test]
288 fn neighbour_table_square_periodic_degree_four() {
289 let w = Wolff::square(5, 5, 1.0, true).expect("ok");
290 for nbrs in w.neighbours() {
292 assert_eq!(nbrs.len(), 4);
293 }
294 assert_eq!(w.n_sites(), 25);
295 }
296
297 #[test]
298 fn neighbour_table_square_open_corner_and_centre() {
299 let w = Wolff::square(3, 3, 1.0, false).expect("ok");
300 assert_eq!(w.neighbours()[0].len(), 2);
302 assert_eq!(w.neighbours()[4].len(), 4);
303 }
304
305 #[test]
306 fn bond_probability_formula() {
307 let w = Wolff::square(2, 2, 1.0, false).expect("ok");
308 let beta = 0.33_f64;
309 let expected = 1.0 - (-2.0 * beta).exp();
310 assert!((w.bond_probability(beta) - expected).abs() < 1e-12);
311 assert_eq!(w.bond_probability(0.0), 0.0);
312 }
313
314 #[test]
315 fn cluster_contains_only_aligned_spins() {
316 let w_dims = (6usize, 6usize);
320 let mut w = Wolff::square(w_dims.0, w_dims.1, 1.0, true).expect("ok");
321 let mut spins: Vec<i8> = (0..36)
322 .map(|i| if (i / 6 + i % 6) % 2 == 0 { 1 } else { -1 })
323 .collect();
324 let mut rng = LcgRng::new(1);
325 w.step(&mut spins, 5.0, &mut rng).expect("step");
326 assert_eq!(
329 w.last_cluster_size(),
330 1,
331 "checkerboard cluster is singleton"
332 );
333 }
334
335 #[test]
336 fn high_beta_uniform_flips_whole_lattice() {
337 let mut w = Wolff::square(8, 8, 1.0, true).expect("ok");
340 let mut spins = vec![1i8; 64];
341 let mut rng = LcgRng::new(42);
342 w.step(&mut spins, 5.0, &mut rng).expect("step");
343 assert_eq!(w.last_cluster_size(), 64, "should engulf whole lattice");
344 assert!(spins.iter().all(|&s| s == -1), "uniform −1 after flip");
345 }
346
347 #[test]
348 fn phase_transition_ordered_vs_disordered() {
349 let mut w = Wolff::square(16, 16, 1.0, true).expect("ok");
350 let mut spins = vec![1i8; 256];
351 let m_ordered = run_chain(&mut w, &mut spins, BETA_ORDERED, 17, 60, 120);
352
353 let mut spins2 = vec![1i8; 256];
354 let m_dis = run_chain(&mut w, &mut spins2, BETA_DISORDERED, 17, 60, 120);
355
356 assert!(
357 m_ordered > 0.8,
358 "ordered |M|={m_ordered} should be high (>0.8)"
359 );
360 assert!(m_dis < 0.4, "disordered |M|={m_dis} should be low (<0.4)");
361 assert!(
362 m_ordered - m_dis > 0.4,
363 "ordered ({m_ordered}) vs disordered ({m_dis}) must separate"
364 );
365 }
366
367 #[test]
368 fn energy_change_matches_boundary_sum() {
369 let mut w = Wolff::square(7, 7, 1.0, true).expect("ok");
372 let mut spins: Vec<i8> = (0..49)
373 .map(|i| if (i * 7 + 3) % 5 < 2 { 1 } else { -1 })
374 .collect();
375
376 let beta = 0.45;
379 let p = w.bond_probability(beta);
380 let mut rng = LcgRng::new(123);
381 let seed = rng.next_usize(w.n_sites());
382 let s_seed = spins[seed];
383 let mut in_cluster = vec![false; w.n_sites()];
384 let mut stack = vec![seed];
385 in_cluster[seed] = true;
386 while let Some(site) = stack.pop() {
387 for &nbr in &w.neighbours()[site] {
388 if !in_cluster[nbr] && spins[nbr] == s_seed && rng.next_f64() < p {
389 in_cluster[nbr] = true;
390 stack.push(nbr);
391 }
392 }
393 }
394 let mut delta = 0.0;
396 for (i, nbrs) in w.neighbours().iter().enumerate() {
397 for &j in nbrs {
398 if i < j && in_cluster[i] != in_cluster[j] {
399 delta += 2.0 * 1.0 * (spins[i] as f64) * (spins[j] as f64);
400 }
401 }
402 }
403
404 let e_before = w.energy(&spins).expect("e");
405 let mut rng2 = LcgRng::new(123);
408 w.step(&mut spins, beta, &mut rng2).expect("step");
409 let e_after = w.energy(&spins).expect("e");
410 assert!(
411 (e_after - e_before - delta).abs() < 1e-9,
412 "ΔE={} should equal boundary sum {}",
413 e_after - e_before,
414 delta
415 );
416 }
417
418 #[test]
419 fn determinism_fixed_seed() {
420 let mut a = Wolff::square(10, 10, 1.0, true).expect("ok");
421 let mut b = Wolff::square(10, 10, 1.0, true).expect("ok");
422 let mut sa = vec![1i8; 100];
423 let mut sb = vec![1i8; 100];
424 let mut ra = LcgRng::new(7777);
425 let mut rb = LcgRng::new(7777);
426 for _ in 0..50 {
427 a.step(&mut sa, 0.45, &mut ra).expect("step");
428 b.step(&mut sb, 0.45, &mut rb).expect("step");
429 }
430 assert_eq!(sa, sb);
431 assert_eq!(a.last_cluster_size(), b.last_cluster_size());
432 }
433
434 #[test]
435 fn ergodic_global_sign_flips() {
436 let mut w = Wolff::square(12, 12, 1.0, true).expect("ok");
439 let mut spins = vec![1i8; 144];
440 let mut rng = LcgRng::new(2025);
441 let mut saw_positive = false;
442 let mut saw_negative = false;
443 for _ in 0..150 {
444 w.step(&mut spins, 0.60, &mut rng).expect("step");
445 let m = w.magnetization(&spins).expect("m");
446 if m > 0.5 {
447 saw_positive = true;
448 }
449 if m < -0.5 {
450 saw_negative = true;
451 }
452 }
453 assert!(
454 saw_positive && saw_negative,
455 "must explore both ±M states (+ = {saw_positive}, − = {saw_negative})"
456 );
457 }
458
459 #[test]
460 fn graph_variant_path_three() {
461 let adjacency = vec![vec![1], vec![0, 2], vec![1]];
463 let mut w = Wolff::from_graph(adjacency, 1.0).expect("ok");
464 assert_eq!(w.n_sites(), 3);
465 let mut spins = vec![1i8; 3];
466 let mut rng = LcgRng::new(9);
467 let mut saw_multi = false;
469 for _ in 0..30 {
470 w.step(&mut spins, 4.0, &mut rng).expect("step");
471 if w.last_cluster_size() > 1 {
472 saw_multi = true;
473 }
474 }
475 assert!(saw_multi, "high-β path should grow a multi-site cluster");
476 }
477
478 #[test]
479 fn err_dimension_mismatch() {
480 let mut w = Wolff::square(4, 4, 1.0, false).expect("ok");
481 let mut spins = vec![1i8; 17]; let mut rng = LcgRng::new(1);
483 match w.step(&mut spins, 0.4, &mut rng) {
484 Err(SeqError::ShapeMismatch { expected, got }) => {
485 assert_eq!(expected, 16);
486 assert_eq!(got, 17);
487 }
488 other => panic!("expected ShapeMismatch, got {other:?}"),
489 }
490 }
491
492 #[test]
493 fn err_bad_lattice_and_invalid_spin() {
494 assert!(matches!(
495 Wolff::square(3, 0, 1.0, false),
496 Err(SeqError::InvalidConfiguration(_))
497 ));
498 let mut w = Wolff::square(2, 2, 1.0, false).expect("ok");
499 let mut spins = vec![1i8, -1, 0, 1]; let mut rng = LcgRng::new(1);
501 assert!(matches!(
502 w.step(&mut spins, 0.4, &mut rng),
503 Err(SeqError::InvalidParameter { .. })
504 ));
505 let mut good = vec![1i8; 4];
507 assert!(matches!(
508 w.step(&mut good, f64::INFINITY, &mut rng),
509 Err(SeqError::InvalidParameter { .. })
510 ));
511 }
512
513 #[test]
514 fn err_graph_invariants() {
515 assert!(matches!(
516 Wolff::from_graph(vec![vec![1], vec![]], 1.0),
517 Err(SeqError::GraphInvariantViolated(_))
518 ));
519 assert!(matches!(
520 Wolff::from_graph(vec![], 1.0),
521 Err(SeqError::EmptyInput)
522 ));
523 assert!(matches!(
524 Wolff::from_graph(vec![vec![5], vec![]], 1.0),
525 Err(SeqError::IndexOutOfBounds { .. })
526 ));
527 }
528}