Skip to main content

pounce_minima/
options.rs

1//! Options for the global search: which strategy to run and its knobs.
2//!
3//! Moved here from `pounce-cli`'s `cli.rs` (gh#966), where they were
4//! `MinimaMethod` / `MinimaArgs`; the CLI still parses argv into them and
5//! re-exports them under those names.
6
7/// Global-search strategy for `--minima`. Mirrors the six methods of
8/// `pounce.find_minima` (`python/pounce/_minima.py`).
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Method {
11    /// Random / Sobol' box sampling (restart).
12    Multistart,
13    /// Multi-Level Single Linkage clustering (Rinnooy Kan & Timmer 1987).
14    Mlsl,
15    /// Metropolis chain over minima (Wales & Doye 1997).
16    Basinhopping,
17    /// Repulsive Gaussian bumps (filled-function; Ge 1990).
18    Flooding,
19    /// Softened `1/‖x−x*‖^p` poles (deflation; Farrell et al. 2015).
20    Deflation,
21    /// Equal-height tunnel between descents (Levy & Montalvo 1985).
22    Tunneling,
23}
24
25impl Method {
26    pub fn parse(s: &str) -> Result<Self, String> {
27        Ok(match s {
28            "multistart" => Self::Multistart,
29            "mlsl" => Self::Mlsl,
30            "basinhopping" => Self::Basinhopping,
31            "flooding" => Self::Flooding,
32            "deflation" => Self::Deflation,
33            "tunneling" => Self::Tunneling,
34            other => {
35                return Err(format!(
36                    "unknown --minima method '{other}'; choose from \
37                     multistart, mlsl, basinhopping, flooding, deflation, tunneling"
38                ));
39            }
40        })
41    }
42
43    pub fn as_str(&self) -> &'static str {
44        match self {
45            Self::Multistart => "multistart",
46            Self::Mlsl => "mlsl",
47            Self::Basinhopping => "basinhopping",
48            Self::Flooding => "flooding",
49            Self::Deflation => "deflation",
50            Self::Tunneling => "tunneling",
51        }
52    }
53}
54
55/// Parsed `--minima` configuration. Shared knobs have concrete defaults;
56/// strategy-specific knobs are `Option`s resolved per-method in the driver
57/// (so `"auto"` widths and curvature-based amplitudes match
58/// `pounce.find_minima`). Field semantics mirror `_minima.py` exactly.
59#[derive(Debug, Clone)]
60pub struct MinimaOptions {
61    pub method: Method,
62    /// Target: stop once this many distinct minima are found (default 10).
63    pub n_minima: usize,
64    /// Budget: hard cap on solver calls (default `8 * n_minima`).
65    pub max_solves: Option<usize>,
66    /// Give-up: stop after this many solves in a row that find nothing new.
67    pub patience: usize,
68    /// Two minima within this scaled distance are the same (default 1e-4).
69    pub dedup: f64,
70    /// Smallest Hessian eigenvalue tolerated by saddle rejection (1e-6).
71    pub psd_tol: f64,
72    /// Reject a converged solve whose unscaled dual infeasibility exceeds
73    /// `kkt_tol · max(1, max|∇f(x0)|, dual_scale)` (gh#983; default 1e-4).
74    /// `None` (`--kkt-tol none`) disables the check.
75    pub kkt_tol: Option<f64>,
76    /// Seed for the sampler / Sobol' scramble (default 0; reproducible).
77    pub seed: u64,
78    /// Use a scrambled Sobol' sequence for box sampling (default true).
79    pub sobol: bool,
80    // ---- strategy-specific knobs (None ⇒ per-method default) ----
81    pub sigma: Option<f64>,
82    pub sigma_frac: Option<f64>,
83    pub amplitude: Option<f64>,
84    pub amp_margin: Option<f64>,
85    pub eta: Option<f64>,
86    pub power: Option<f64>,
87    pub soft: Option<f64>,
88    pub length: Option<f64>,
89    pub length_frac: Option<f64>,
90    pub gamma: Option<f64>,
91    pub samples_per_round: Option<usize>,
92    pub step: Option<f64>,
93    pub temperature: Option<f64>,
94    pub restart_jitter: Option<f64>,
95    // ---- library-only knobs (no CLI flag; `pounce.find_minima` passes them) ----
96    /// Per-dimension flooding widths; takes precedence over `sigma`.
97    pub sigma_per_dim: Option<Vec<f64>>,
98    /// Per-dimension deflation / tunneling lengths; takes precedence over
99    /// `length`.
100    pub length_per_dim: Option<Vec<f64>>,
101    /// MLSL sample cap (default `max_solves * samples_per_round`).
102    pub max_samples: Option<usize>,
103    /// Flooding: factor a basin's bump grows by on a failed escape (3).
104    pub amp_bump: Option<f64>,
105    /// Flooding: cap on that growth (1e3).
106    pub amp_bump_cap: Option<f64>,
107    /// Flooding: bump height when no Hessian gives a curvature-based one (2).
108    pub amp_fallback: Option<f64>,
109}
110
111impl MinimaOptions {
112    /// The solve budget: `max_solves`, or `8 * n_minima` when unset.
113    pub fn max_solves_or_default(&self) -> usize {
114        self.max_solves.unwrap_or(8 * self.n_minima)
115    }
116}
117
118impl Default for MinimaOptions {
119    fn default() -> Self {
120        Self {
121            // Matches `find_minima`'s default `method="deflation"`.
122            method: Method::Deflation,
123            n_minima: 10,
124            max_solves: None,
125            patience: 8,
126            dedup: 1e-4,
127            psd_tol: 1e-6,
128            kkt_tol: Some(1e-4),
129            seed: 0,
130            sobol: true,
131            sigma: None,
132            sigma_frac: None,
133            amplitude: None,
134            amp_margin: None,
135            eta: None,
136            power: None,
137            soft: None,
138            length: None,
139            length_frac: None,
140            gamma: None,
141            samples_per_round: None,
142            step: None,
143            temperature: None,
144            restart_jitter: None,
145            sigma_per_dim: None,
146            length_per_dim: None,
147            max_samples: None,
148            amp_bump: None,
149            amp_bump_cap: None,
150            amp_fallback: None,
151        }
152    }
153}