Skip to main content

spin_sim/
config.rs

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    /// Houdayer clusters for `N / 2` replica pairs swapped jointly: active sites are
105    /// those where any pair disagrees, and a cluster flip swaps every pair on it.
106    Pair(usize),
107    /// Jörg bonds generalized to `N / 2` replica pairs swapped jointly; `Jorg(2)` is
108    /// the two-replica Jörg move.
109    Jorg(usize),
110    Cmr,
111    /// Swendsen-Wang replica Monte Carlo between adjacent temperature slots.
112    Rmc,
113    /// Replica-permutation clusters for a group of `N` replicas at one temperature:
114    /// active sites are those where the group does not agree, and each connected
115    /// active component gets a uniformly random permutation of the group. `Perm(2)`
116    /// is Houdayer's move.
117    Perm(usize),
118    /// [`Self::Perm`] in the gauge of a uniformly random reference site: sites whose
119    /// replica pattern equals the reference's up to a global sign stay fixed, and the
120    /// other sites' gauged spins are permuted. `GaugedPerm(2)` is Houdayer's move on
121    /// the minority overlap sign of the reference.
122    GaugedPerm(usize),
123}
124
125impl OverlapClusterBuildMode {
126    /// Replicas per temperature consumed by one move.
127    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
152/// Largest replica group for `permN` and `gpermN`.
153pub 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    /// Apply overlap moves only at temperatures <= this value (rmc: both ends of an
213    /// edge). Same-temperature cluster moves pay off only well below T_c (Zhu, Ochoa
214    /// & Katzgraber, PRL 115, 077201 (2015)); `None` applies them everywhere.
215    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        // Parallel group tasks index per-pair RNG and output slots, which is sound
273        // only for group sizes >= 2; the pair rules also need an even size.
274        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            // Observed graphs are reported per two-replica build mode only.
309            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    /// Accumulate the link overlap and its square conditioned on the overlap bin
341    /// (`ql_at_q_sum`, `ql2_at_q_sum`), also per disorder sample. They are two of
342    /// the three histogram-sized overlap arrays, so turning this off saves two
343    /// thirds of that memory.
344    pub conditional_link_overlap: bool,
345}
346
347impl SimConfig {
348    /// Checks the options against each other; the message names the offending one.
349    pub fn validate(&self) -> Result<(), String> {
350        validate_sim_config(self).map_err(Into::into)
351    }
352
353    /// Sweeps left for measurement after warmup.
354    pub fn n_measurement_sweeps(&self) -> usize {
355        self.n_sweeps.saturating_sub(self.warmup_sweeps)
356    }
357
358    /// Largest autocorrelation lag that is estimated: the requested lag, capped at a
359    /// quarter of the measured sweeps and at least 1.
360    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    /// The string parser bounds perm sizes, but the enum can be built directly.
451    #[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}