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}