Struct Grid
pub struct Grid { /* private fields */ }Expand description
Walks every point of a finite space, once each, in a fixed order.
The sampler for “try all of these”: a small, exhaustively enumerable space where a random search would waste evaluations re-drawing points it has already seen. It is stateless — no visited set, no cursor, nothing persisted — because the grid point is computed from the trial number.
§Which point a trial gets
Trial number n gets grid point n, decomposed in mixed radix over the
parameters in schema declaration order, with the last-declared
parameter varying fastest (the usual positional-numeral convention, where
the first parameter is the most significant digit):
index(p_i) = (n / (card(p_{i+1}) * … * card(p_{m-1}))) % card(p_i)So a space [a: 3 values, b: 2 values] is walked
(a0,b0), (a0,b1), (a1,b0), (a1,b1), (a2,b0), (a2,b1).
Nothing about this depends on stored state, wall clock, worker count or the order trials complete in, so the grid is covered exactly once even when 16 workers run concurrently — and a resumed study picks up precisely where the trial numbering left off.
§Exhaustion is the stop condition
Once n reaches the number of grid points, every sampling call returns
Error::SpaceExhausted. The study loop treats that as a normal stop,
not as a failure — it is how a grid search ends. A loop that maps it onto
a failed trial is wrong.
§Construction is fallible
Grid::new refuses, rather than allocating, when the space cannot be
enumerated:
| Space | Result |
|---|---|
contains a continuous parameter (cardinality() == None) | Error::InvalidSpace |
product of cardinalities exceeds MAX_GRID_POINTS | Error::InvalidSpace |
| empty schema | the single empty point |
The ceiling is MAX_GRID_POINTS — the same 2^20 that bounds
Distribution::grid_values, reused rather than reinvented, so “a
distribution can be enumerated” and “a space can be enumerated” mean the
same thing at the same size.
use atune_core::sampler::Grid;
use atune_core::space::{Distribution, ParamSpec, ParamValue, SpaceSchema};
let space = SpaceSchema::new([
ParamSpec::new("opt", Distribution::cat_labels(["adam", "sgd"]).unwrap()).unwrap(),
ParamSpec::new("amp", Distribution::boolean()).unwrap(),
])
.unwrap();
let grid = Grid::new(space).unwrap();
assert_eq!(grid.points(), 4);
// `amp` is declared last, so it varies fastest.
let first = grid.point_at(0).unwrap();
assert_eq!(first.get("opt"), Some(&ParamValue::Cat(0)));
assert_eq!(first.get("amp"), Some(&ParamValue::Bool(false)));
let second = grid.point_at(1).unwrap();
assert_eq!(second.get("opt"), Some(&ParamValue::Cat(0)));
assert_eq!(second.get("amp"), Some(&ParamValue::Bool(true)));
// And the fifth trial has nothing left to evaluate.
assert!(grid.point_at(4).is_err());Implementations§
§impl Grid
impl Grid
pub fn new(space: SpaceSchema) -> Result<Self>
pub fn new(space: SpaceSchema) -> Result<Self>
Builds the grid of space.
§Errors
Error::InvalidSpace if any parameter is continuous (a float without
a step), if any single parameter’s grid exceeds MAX_GRID_POINTS, or
if the product of the cardinalities does. Nothing is allocated before
those checks pass, so an accidentally unbounded space costs an error and
not the process.
pub const fn space(&self) -> &SpaceSchema
pub const fn space(&self) -> &SpaceSchema
The space this grid enumerates.
pub const fn points(&self) -> u64
pub const fn points(&self) -> u64
The number of grid points — the number of trials the grid can serve.
An empty schema has exactly one point (the empty assignment), matching
SpaceSchema::cardinality.
pub fn point_at(&self, index: u64) -> Result<Assignment>
pub fn point_at(&self, index: u64) -> Result<Assignment>
The assignment at grid point index.
See the type documentation for the mixed-radix decomposition. This is a
pure function: the same index always yields the same assignment.
§Errors
Error::SpaceExhausted if index is at or beyond
points.
Trait Implementations§
§impl Sampler for Grid
impl Sampler for Grid
§fn snapshots_space(&self) -> bool
fn snapshots_space(&self) -> bool
true: the grid is an enumeration of the space snapshotted in
Grid::new, so a bound that grows mid-study would change the
grid’s cardinality underneath the trial-number-to-point mapping
(open-search-spaces §12.2, clause 1).
§fn infer_relative_space(&self, _study: &StudyView) -> Result<SpaceSchema>
fn infer_relative_space(&self, _study: &StudyView) -> Result<SpaceSchema>
The space the grid was constructed with.
Deliberately not the study’s declared space: a Grid enumerates
what it was built to enumerate, and silently switching to a space with
different cardinalities would break the trial-number-to-grid-point
mapping mid-study. The two are normally the same schema; when they are
not, parameters the grid does not cover still get values, through the
uniform fallback documented on
sample_independent.
§Errors
Never; the signature is fallible because inference in general is.
§fn sample_relative(
&self,
_study: &StudyView,
trial: &TrialMeta,
space: &SpaceSchema,
) -> Result<Assignment>
fn sample_relative( &self, _study: &StudyView, trial: &TrialMeta, space: &SpaceSchema, ) -> Result<Assignment>
The grid point of this trial’s number.
Binds exactly the parameters of space: those the grid covers take
their grid value, and any others fall back to the uniform per-name draw
(see sample_independent).
§Errors
Error::SpaceExhausted once the trial number reaches
points — the grid search is over, and the study loop
stops rather than returning the error to the caller.
The ask that discovers this has already created its trial record
durably, because creation and the lifecycle reservation are one atomic
step, so the record cannot be withdrawn: it is finished Failed with this
error as its reason. That trailing record is expected, not a fault — it
carries no parameters, is invisible to samplers and to pending repulsion,
and is asserted by space_exhaustion_is_a_normal_stop. It does count
against max_trials, and a caller listing every trial will see it.
Error::InvalidSpace if a
distribution outside the grid is malformed.
§fn sample_independent(
&self,
_study: &StudyView,
trial: &TrialMeta,
name: &str,
dist: &Distribution,
) -> Result<ParamValue>
fn sample_independent( &self, _study: &StudyView, trial: &TrialMeta, name: &str, dist: &Distribution, ) -> Result<ParamValue>
One parameter of this trial’s grid point.
§Parameters outside the grid
A define-by-run objective may suggest a parameter the grid was never
built for, and a caller may ask for a covered parameter with a
distribution whose support no longer holds the grid value. Both cases
fall back to the uniform draw Random would have made —
from_unit of a coordinate drawn from
ChaCha8Rng::seed_from_u64(seed_for_name(trial.sampler_seed, name)).
That keeps three things true: the value is in the caller’s declared
support; it is a pure function of (study seed, trial number, name),
so it is as reproducible as the grid itself; and the grid’s own
enumeration is untouched, because an off-grid parameter never consumes
a grid coordinate.
§Errors
Error::SpaceExhausted once the trial number reaches
points;
Error::InvalidSpace if dist is
malformed.
§fn reseed(&self, _seed: u64)
fn reseed(&self, _seed: u64)
A no-op: the grid is a pure function of the trial number.
Reseeding cannot move a grid point without breaking the
visit-every-point-once guarantee, so it does not. It does not move the
uniform fallback either — that is keyed on the trial’s sampler_seed,
exactly as Random::reseed is. See
Random’s reseed documentation for the full argument.