1#[derive(Debug, Clone, Copy, PartialEq)]
2pub enum SweepMode {
3 None,
4 Metropolis,
5 Gibbs,
6}
7
8impl TryFrom<&str> for SweepMode {
9 type Error = String;
10 fn try_from(s: &str) -> Result<Self, Self::Error> {
11 match s {
12 "none" => Ok(Self::None),
13 "metropolis" => Ok(Self::Metropolis),
14 "gibbs" => Ok(Self::Gibbs),
15 _ => Err(format!(
16 "unknown sweep_mode '{s}', expected 'metropolis', 'gibbs' or 'none'"
17 )),
18 }
19 }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum ClusterMode {
24 Wolff,
25 Sw,
26}
27
28impl TryFrom<&str> for ClusterMode {
29 type Error = String;
30 fn try_from(s: &str) -> Result<Self, Self::Error> {
31 match s {
32 "wolff" => Ok(Self::Wolff),
33 "sw" => Ok(Self::Sw),
34 _ => Err(format!(
35 "unknown cluster_mode '{s}', expected 'wolff' or 'sw'"
36 )),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ClusterAction {
43 Update,
44 Observe,
45}
46
47impl TryFrom<&str> for ClusterAction {
48 type Error = String;
49
50 fn try_from(s: &str) -> Result<Self, Self::Error> {
51 match s {
52 "update" => Ok(Self::Update),
53 "observe" => Ok(Self::Observe),
54 _ => Err(format!(
55 "unknown cluster action '{s}', expected 'update' or 'observe'"
56 )),
57 }
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum PtSchedule {
63 SingleRandomEdge,
64 FullLadder,
65}
66
67impl TryFrom<&str> for PtSchedule {
68 type Error = String;
69
70 fn try_from(s: &str) -> Result<Self, Self::Error> {
71 match s {
72 "single_random_edge" => Ok(Self::SingleRandomEdge),
73 "full_ladder" => Ok(Self::FullLadder),
74 _ => Err(format!(
75 "unknown pt_schedule '{s}', expected 'single_random_edge' or 'full_ladder'"
76 )),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum AutocorrelationBackend {
83 Ring,
84 Fft,
85}
86
87impl TryFrom<&str> for AutocorrelationBackend {
88 type Error = String;
89
90 fn try_from(s: &str) -> Result<Self, Self::Error> {
91 match s {
92 "ring" => Ok(Self::Ring),
93 "fft" => Ok(Self::Fft),
94 _ => Err(format!(
95 "unknown autocorrelation_backend '{s}', expected 'ring' or 'fft'"
96 )),
97 }
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq)]
102pub enum OverlapClusterBuildMode {
103 Houdayer(usize),
104 Pair(usize),
107 Jorg(usize),
110 Cmr,
111 Rmc,
113 Perm(usize),
118 GaugedPerm(usize),
123}
124
125impl OverlapClusterBuildMode {
126 pub fn group_size(&self) -> usize {
128 match self {
129 Self::Houdayer(n)
130 | Self::Pair(n)
131 | Self::Jorg(n)
132 | Self::Perm(n)
133 | Self::GaugedPerm(n) => *n,
134 Self::Cmr => 2,
135 Self::Rmc => 1,
136 }
137 }
138}
139
140fn parse_group_size(s: &str, prefix: &str, label: &str) -> Result<usize, String> {
141 let n: usize = s[prefix.len()..].parse().map_err(|_| {
142 format!(
143 "invalid {label} group size in '{s}', expected '{prefix}N' with even integer N >= 2"
144 )
145 })?;
146 if n < 2 || !n.is_multiple_of(2) {
147 return Err(format!("{label} group size must be even and >= 2, got {n}"));
148 }
149 Ok(n)
150}
151
152pub const MAX_PERM_GROUP: usize = 8;
154
155fn parse_perm_size(s: &str, prefix: &str, max: usize) -> Result<usize, String> {
156 let n: usize = s[prefix.len()..].parse().map_err(|_| {
157 format!("invalid group size in '{s}', expected '{prefix}N' with integer N >= 2")
158 })?;
159 if !(2..=max).contains(&n) {
160 return Err(format!("{prefix}N needs 2 <= N <= {max}, got {n}"));
161 }
162 Ok(n)
163}
164
165impl TryFrom<&str> for OverlapClusterBuildMode {
166 type Error = String;
167 fn try_from(s: &str) -> Result<Self, Self::Error> {
168 match s {
169 "houdayer" | "houd2" => Ok(Self::Houdayer(2)),
170 "jorg" => Ok(Self::Jorg(2)),
171 "cmr" | "cmr2" => Ok(Self::Cmr),
172 "rmc" => Ok(Self::Rmc),
173 _ if s.starts_with("gperm") => {
174 parse_perm_size(s, "gperm", MAX_PERM_GROUP).map(Self::GaugedPerm)
175 }
176 _ if s.starts_with("perm") => parse_perm_size(s, "perm", MAX_PERM_GROUP).map(Self::Perm),
177 _ if s.starts_with("pair") => parse_group_size(s, "pair", "pair").map(Self::Pair),
178 _ if s.starts_with("jorg") => parse_group_size(s, "jorg", "Jörg").map(Self::Jorg),
179 _ if s.starts_with("houd") => {
180 let n = parse_group_size(s, "houd", "Houdayer")?;
181 if n > 2 {
182 eprintln!(
183 "WARNING: houd{n} (group_size > 2) is experimental and does not satisfy \
184 detailed balance: flipping a balanced site can change the total energy"
185 );
186 }
187 Ok(Self::Houdayer(n))
188 }
189 _ => Err(format!(
190 "unknown overlap_cluster_build_mode '{s}', expected 'houdayer', 'houdN', 'pairN', 'jorg', 'jorgN', 'cmr', 'rmc', 'permN' or 'gpermN'"
191 )),
192 }
193 }
194}
195
196#[derive(Clone, Debug)]
197pub struct ClusterConfig {
198 pub interval: usize,
199 pub mode: ClusterMode,
200 pub action: ClusterAction,
201 pub collect_stats: bool,
202}
203
204#[derive(Clone, Debug)]
205pub struct OverlapClusterConfig {
206 pub interval: usize,
207 pub modes: Vec<OverlapClusterBuildMode>,
208 pub cluster_mode: ClusterMode,
209 pub action: ClusterAction,
210 pub collect_stats: bool,
211 pub snapshot_interval: Option<usize>,
212 pub max_temperature: Option<f32>,
216}
217
218impl OverlapClusterConfig {
219 pub fn max_group_size(&self) -> usize {
220 self.modes.iter().map(|m| m.group_size()).max().unwrap_or(2)
221 }
222}
223
224pub fn parse_overlap_modes(s: &str) -> Result<Vec<OverlapClusterBuildMode>, String> {
225 s.split('+')
226 .map(|part| OverlapClusterBuildMode::try_from(part.trim()))
227 .collect()
228}
229
230fn validate_sim_config(cfg: &SimConfig) -> Result<(), &'static str> {
231 if cfg.n_sweeps < 1 {
232 return Err("n_sweeps must be >= 1");
233 }
234 if cfg.warmup_sweeps > cfg.n_sweeps {
235 return Err("warmup_sweeps must be <= n_sweeps");
236 }
237 if let Some(ref c) = cfg.cluster_update {
238 if c.interval < 1 {
239 return Err("cluster_update interval must be >= 1");
240 }
241 if c.action == ClusterAction::Observe && c.mode == ClusterMode::Wolff {
242 return Err("cluster_action='observe' requires cluster_mode='sw'");
243 }
244 }
245 if cfg.pt_interval == Some(0) {
246 return Err("pt_interval must be >= 1");
247 }
248 if cfg.autocorrelation_backend == AutocorrelationBackend::Fft
249 && cfg.autocorrelation_max_lag.is_none()
250 {
251 return Err("autocorrelation_backend='fft' requires autocorrelation_max_lag");
252 }
253 if let Some(ref h) = cfg.overlap_cluster {
254 if h.interval < 1 {
255 return Err("overlap_cluster interval must be >= 1");
256 }
257 if let Some(si) = h.snapshot_interval {
258 if si < 1 || si % h.interval != 0 {
259 return Err(
260 "snapshot_interval must be a positive multiple of overlap_cluster interval",
261 );
262 }
263 }
264 if h.modes.is_empty() {
265 return Err("overlap_cluster modes must not be empty");
266 }
267 if h.max_temperature
268 .is_some_and(|t| !(t.is_finite() && t > 0.0))
269 {
270 return Err("overlap_cluster max_temperature must be positive and finite");
271 }
272 if h.modes.iter().any(|mode| {
275 matches!(
276 mode,
277 OverlapClusterBuildMode::Perm(n) | OverlapClusterBuildMode::GaugedPerm(n)
278 if !(2..=MAX_PERM_GROUP).contains(n)
279 )
280 }) {
281 return Err("permN and gpermN group sizes must be between 2 and 8");
282 }
283 if h.modes.iter().any(
284 |mode| matches!(mode, OverlapClusterBuildMode::Houdayer(n) if *n < 2 || n % 2 != 0),
285 ) {
286 return Err("Houdayer group size must be even and >= 2");
287 }
288 if h.modes.iter().any(|mode| {
289 matches!(mode, OverlapClusterBuildMode::Pair(n) | OverlapClusterBuildMode::Jorg(n) if *n < 2 || n % 2 != 0)
290 }) {
291 return Err("pairN and jorgN group sizes must be even and >= 2");
292 }
293 if h.action == ClusterAction::Observe {
294 if h.cluster_mode == ClusterMode::Wolff {
295 return Err("overlap_cluster_action='observe' requires overlap_cluster_mode='sw'");
296 }
297 if h.snapshot_interval.is_some() {
298 return Err(
299 "snapshot_interval is not supported with overlap_cluster_action='observe'",
300 );
301 }
302 if h.modes
303 .iter()
304 .any(|mode| matches!(mode, OverlapClusterBuildMode::Houdayer(n) if *n > 2))
305 {
306 return Err("overlap_cluster_action='observe' does not support experimental houdN with N > 2");
307 }
308 if h.modes.iter().any(|mode| {
310 matches!(
311 mode,
312 OverlapClusterBuildMode::Pair(_)
313 | OverlapClusterBuildMode::Rmc
314 | OverlapClusterBuildMode::Perm(_)
315 | OverlapClusterBuildMode::GaugedPerm(_)
316 ) || matches!(mode, OverlapClusterBuildMode::Jorg(n) if *n > 2)
317 }) {
318 return Err(
319 "overlap_cluster_action='observe' supports only houdayer, jorg and cmr",
320 );
321 }
322 }
323 }
324 Ok(())
325}
326
327#[derive(Clone, Debug)]
328pub struct SimConfig {
329 pub n_sweeps: usize,
330 pub warmup_sweeps: usize,
331 pub sweep_mode: SweepMode,
332 pub cluster_update: Option<ClusterConfig>,
333 pub pt_interval: Option<usize>,
334 pub pt_schedule: PtSchedule,
335 pub overlap_cluster: Option<OverlapClusterConfig>,
336 pub autocorrelation_max_lag: Option<usize>,
337 pub autocorrelation_backend: AutocorrelationBackend,
338 pub sequential: bool,
339 pub equilibration_diagnostic: bool,
340 pub conditional_link_overlap: bool,
345}
346
347impl SimConfig {
348 pub fn validate(&self) -> Result<(), String> {
350 validate_sim_config(self).map_err(Into::into)
351 }
352
353 pub fn n_measurement_sweeps(&self) -> usize {
355 self.n_sweeps.saturating_sub(self.warmup_sweeps)
356 }
357
358 pub fn autocorrelation_lag(&self) -> Option<usize> {
361 self.autocorrelation_max_lag
362 .map(|lag| lag.min(self.n_measurement_sweeps() / 4).max(1))
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 fn config() -> SimConfig {
371 SimConfig {
372 n_sweeps: 1,
373 warmup_sweeps: 0,
374 sweep_mode: SweepMode::Metropolis,
375 cluster_update: None,
376 pt_interval: None,
377 pt_schedule: PtSchedule::SingleRandomEdge,
378 overlap_cluster: None,
379 autocorrelation_max_lag: None,
380 autocorrelation_backend: AutocorrelationBackend::Ring,
381 sequential: true,
382 equilibration_diagnostic: false,
383 conditional_link_overlap: true,
384 }
385 }
386
387 #[test]
388 fn rejects_zero_parallel_tempering_interval() {
389 let mut config = config();
390 config.pt_interval = Some(0);
391 assert!(config.validate().is_err());
392 }
393
394 #[test]
395 fn rejects_empty_overlap_mode_list() {
396 let mut config = config();
397 config.overlap_cluster = Some(OverlapClusterConfig {
398 interval: 1,
399 modes: vec![],
400 cluster_mode: ClusterMode::Sw,
401 action: ClusterAction::Update,
402 collect_stats: false,
403 snapshot_interval: None,
404 max_temperature: None,
405 });
406 assert!(config.validate().is_err());
407 }
408
409 #[test]
410 fn rejects_unsupported_observe_modes() {
411 let mut config = config();
412 config.cluster_update = Some(ClusterConfig {
413 interval: 1,
414 mode: ClusterMode::Wolff,
415 action: ClusterAction::Observe,
416 collect_stats: true,
417 });
418 assert!(config.validate().is_err());
419
420 config.cluster_update = None;
421 config.overlap_cluster = Some(OverlapClusterConfig {
422 interval: 1,
423 modes: vec![OverlapClusterBuildMode::Houdayer(4)],
424 cluster_mode: ClusterMode::Sw,
425 action: ClusterAction::Observe,
426 collect_stats: true,
427 snapshot_interval: None,
428 max_temperature: None,
429 });
430 assert!(config.validate().is_err());
431 }
432
433 #[test]
434 fn rejects_invalid_houdayer_group_sizes() {
435 for group_size in [0, 1, 3] {
436 let mut config = config();
437 config.overlap_cluster = Some(OverlapClusterConfig {
438 interval: 1,
439 modes: vec![OverlapClusterBuildMode::Houdayer(group_size)],
440 cluster_mode: ClusterMode::Sw,
441 action: ClusterAction::Update,
442 collect_stats: false,
443 snapshot_interval: None,
444 max_temperature: None,
445 });
446 assert!(config.validate().is_err(), "houd{group_size} accepted");
447 }
448 }
449
450 #[test]
452 fn rejects_invalid_permutation_group_sizes() {
453 for mode in [
454 OverlapClusterBuildMode::Perm(0),
455 OverlapClusterBuildMode::Perm(1),
456 OverlapClusterBuildMode::GaugedPerm(1),
457 OverlapClusterBuildMode::GaugedPerm(MAX_PERM_GROUP + 1),
458 ] {
459 let mut config = config();
460 config.overlap_cluster = Some(OverlapClusterConfig {
461 interval: 1,
462 modes: vec![mode],
463 cluster_mode: ClusterMode::Wolff,
464 action: ClusterAction::Update,
465 collect_stats: false,
466 snapshot_interval: None,
467 max_temperature: None,
468 });
469 assert!(config.validate().is_err(), "{mode:?} accepted");
470 }
471 }
472
473 #[test]
474 fn parses_multi_pair_and_replica_monte_carlo_modes() {
475 use OverlapClusterBuildMode::*;
476 assert_eq!(
477 parse_overlap_modes("jorg+jorg2+jorg4+pair2+pair6+rmc+houd2+perm3+gperm4").unwrap(),
478 vec![
479 Jorg(2),
480 Jorg(2),
481 Jorg(4),
482 Pair(2),
483 Pair(6),
484 Rmc,
485 Houdayer(2),
486 Perm(3),
487 GaugedPerm(4)
488 ]
489 );
490 for bad in [
491 "pair", "pair3", "pair0", "jorg5", "jorgx", "perm1", "perm9", "gperm1", "permx",
492 ] {
493 assert!(
494 OverlapClusterBuildMode::try_from(bad).is_err(),
495 "{bad} accepted"
496 );
497 }
498 }
499
500 #[test]
501 fn rejects_observing_multi_pair_and_replica_monte_carlo_modes() {
502 for mode in [
503 OverlapClusterBuildMode::Pair(2),
504 OverlapClusterBuildMode::Jorg(4),
505 OverlapClusterBuildMode::Rmc,
506 ] {
507 let mut config = config();
508 config.overlap_cluster = Some(OverlapClusterConfig {
509 interval: 1,
510 modes: vec![mode],
511 cluster_mode: ClusterMode::Sw,
512 action: ClusterAction::Observe,
513 collect_stats: true,
514 snapshot_interval: None,
515 max_temperature: None,
516 });
517 assert!(config.validate().is_err(), "{mode:?} observe accepted");
518 }
519 }
520
521 #[test]
522 fn rejects_fft_without_autocorrelation_lag() {
523 let mut config = config();
524 config.autocorrelation_backend = AutocorrelationBackend::Fft;
525 assert!(config.validate().is_err());
526
527 config.autocorrelation_max_lag = Some(8);
528 assert!(config.validate().is_ok());
529 }
530}