Skip to main content

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}