hyperopt_samplers/grid.rs
1use hyperopt_core::{Distribution, Sampler, StudyState, Trial, Value};
2use rand::rngs::StdRng;
3use rand::SeedableRng;
4
5use crate::random::sample_value;
6
7/// Exhaustive search over a caller-provided discrete grid.
8///
9/// Unlike [`crate::RandomSampler`], which can sample continuous distributions
10/// directly, `GridSampler` only makes sense for parameters with a **finite,
11/// pre-specified set of values** — you must register each parameter's grid up
12/// front with [`GridSampler::add_grid`] (or the typed helpers). The trials are
13/// enumerated as a mixed-radix odometer over the registered grids: trial `n`
14/// maps deterministically to one point in the Cartesian product, in the order
15/// the grids were added.
16///
17/// Notes / limitations:
18/// - Because this is define-by-run, a given trial may not request every
19/// registered parameter (conditional spaces). Enumeration still ranges over
20/// the full product; unused coordinates simply don't get read that trial.
21/// - If more trials are run than there are grid points, the odometer wraps
22/// (trial `n` uses combination `n % total`).
23/// - A parameter that is suggested but was never registered has no grid to draw
24/// from, so it falls back to a random draw (seeded, for reproducibility) and
25/// is documented as such rather than silently returning a constant.
26pub struct GridSampler {
27 grids: Vec<(String, Vec<Value>)>,
28 fallback_rng: StdRng,
29}
30
31impl GridSampler {
32 /// A grid sampler with no parameters registered yet.
33 pub fn new() -> Self {
34 GridSampler {
35 grids: Vec::new(),
36 fallback_rng: StdRng::seed_from_u64(0),
37 }
38 }
39
40 /// Register a parameter's grid of explicit values. Order of registration
41 /// defines the odometer axis order.
42 pub fn add_grid(mut self, name: &str, values: Vec<Value>) -> Self {
43 self.grids.push((name.to_string(), values));
44 self
45 }
46
47 /// Register a float-valued grid.
48 pub fn add_float_grid(self, name: &str, values: &[f64]) -> Self {
49 self.add_grid(name, values.iter().map(|v| Value::Float(*v)).collect())
50 }
51
52 /// Register an integer-valued grid.
53 pub fn add_int_grid(self, name: &str, values: &[i64]) -> Self {
54 self.add_grid(name, values.iter().map(|v| Value::Int(*v)).collect())
55 }
56
57 /// Register a categorical grid.
58 pub fn add_categorical_grid(self, name: &str, values: &[&str]) -> Self {
59 self.add_grid(
60 name,
61 values.iter().map(|v| Value::Categorical(v.to_string())).collect(),
62 )
63 }
64
65 /// Total number of grid points (the product of all registered grid sizes).
66 /// Running this many trials covers the whole grid exactly once.
67 pub fn grid_size(&self) -> usize {
68 self.grids
69 .iter()
70 .map(|(_, v)| v.len().max(1))
71 .product::<usize>()
72 .max(1)
73 }
74
75 /// The index into `name`'s grid for the given trial number, via mixed-radix
76 /// decomposition over the registered grids.
77 fn index_for(&self, name: &str, trial_number: usize) -> Option<usize> {
78 // Product of the radices of the axes *after* the target axis.
79 let mut suffix_product = 1usize;
80 let mut target_len = None;
81 let mut divisor = 1usize;
82 for (grid_name, values) in self.grids.iter().rev() {
83 let radix = values.len().max(1);
84 if grid_name == name {
85 target_len = Some(values.len());
86 divisor = suffix_product;
87 }
88 suffix_product = suffix_product.saturating_mul(radix);
89 }
90 let len = target_len?;
91 if len == 0 {
92 return None;
93 }
94 let total = self.grid_size();
95 let combo = trial_number % total;
96 Some((combo / divisor) % len)
97 }
98}
99
100impl Default for GridSampler {
101 fn default() -> Self {
102 Self::new()
103 }
104}
105
106impl Sampler for GridSampler {
107 fn suggest(
108 &mut self,
109 _study_state: &StudyState,
110 trial: &Trial,
111 param_name: &str,
112 distribution: &Distribution,
113 ) -> Value {
114 if let Some(idx) = self.index_for(param_name, trial.number) {
115 if let Some((_, values)) = self.grids.iter().find(|(n, _)| n == param_name) {
116 if let Some(v) = values.get(idx) {
117 return v.clone();
118 }
119 }
120 }
121 // Unregistered parameter — fall back to a (seeded) random draw.
122 sample_value(&mut self.fallback_rng, distribution)
123 }
124}