Struct Random
pub struct Random;Expand description
Samples every parameter independently and uniformly over its support.
The baseline sampler, and the one every other sampler is compared against. It is stateless: it holds no fields, persists nothing, and learns nothing from history.
§Determinism
Each value is a pure function of (trial.sampler_seed, parameter name, distribution) — the determinism
contract.
Concretely, one draw is
rng = ChaCha8Rng::seed_from_u64(seed_for_name(trial.sampler_seed, name))
value = distribution.from_unit(<one uniform coordinate from rng>)and trial.sampler_seed is itself seed_for(study_seed, trial_number, Stream::Sampler). Three properties follow, and all three are asserted by
this module’s tests:
- The assignment of trial number
ndepends only on(study seed, n, space)— not on wall clock, thread interleaving, completion order or worker count. A 16-thread study evaluates exactly the configuration set a single-threaded one does. - A parameter’s value is stable under space edits. Because nothing is drawn sequentially, adding, removing or reordering a parameter leaves every other parameter of every trial untouched.
sample_relativeandsample_independentagree. They run the identical per-name derivation, so a parameter that migrates between the relative space and the independent fallback keeps its value.
§Uniformity
Sampling happens in unit space and goes through
Distribution::from_unit, so log scaling, step snapping and categorical
mapping have exactly one implementation.
What “uniform” means depends on how from_unit reads the coordinate, so
there are two draws, chosen by whether that map is affine in u:
| Distribution | Draw | Uniform over |
|---|---|---|
Cat, Bool | cell index k, coordinate (k + 0.5) / n | the choices |
Int { log: false, .. } | cell index | the integers of the grid |
Float { step: Some(_), log: false } | cell index | the grid points |
Int { log: true, .. } | raw coordinate in [0, 1) | ln(value), not the integers |
Float { step: None, .. } (linear or log) | raw coordinate in [0, 1) | the value / ln(value) |
The cell-index draw is the better choice wherever it is correct: a raw
coordinate would give the two endpoints half the weight of every interior
point, because from_unit rounds to the nearest cell. It is only correct
when the coordinate maps affinely onto the grid, which is exactly the
non-logarithmic case. A logarithmic Int reports a
cardinality — it counts the linear step
grid — but from_unit maps the coordinate exponentially, so cell centres
do not land on value cells: the draw would be neither uniform in the
integers nor uniform in the log, and the top of the range would be
unreachable. It therefore takes the raw coordinate, which is what a
log-scaled distribution means and what reaches both declared bounds.
Consequently a log-scaled draw is not uniform over its integers: small values are deliberately favoured, which is the entire point of asking for a log scale. Both declared bounds are reachable in every case.
Either way the result satisfies Distribution::contains, which the tests
sweep over every distribution kind.
use atune_core::id::{TrialId, TrialNumber};
use atune_core::sampler::{Random, Sampler};
use atune_core::space::{Distribution, ParamSpec, SpaceSchema};
use atune_core::study::StudyView;
use atune_core::trial::TrialMeta;
let space = SpaceSchema::new([
ParamSpec::new("lr", Distribution::float_log(1e-5, 1e-1).unwrap()).unwrap(),
ParamSpec::new("layers", Distribution::int(1, 8).unwrap()).unwrap(),
])
.unwrap();
let sampler = Random::new();
let view = StudyView::new(atune_core::id::StudyId::new(0), vec![], Some(space.clone()));
let trial = TrialMeta::derive(TrialId::new(1), TrialNumber::new(7), 42);
let params = sampler.sample_relative(&view, &trial, &space).unwrap();
let lr = params.get("lr").unwrap();
// The independent fallback reproduces the joint draw exactly.
let alone = sampler
.sample_independent(&view, &trial, "lr", space.distribution("lr").unwrap())
.unwrap();
assert_eq!(&alone, lr);Implementations§
§impl Random
impl Random
pub const fn new() -> Self
pub const fn new() -> Self
Creates the sampler.
pub fn draw(
&self,
base: u64,
name: &str,
dist: &Distribution,
) -> Result<ParamValue>
pub fn draw( &self, base: u64, name: &str, dist: &Distribution, ) -> Result<ParamValue>
Draws one parameter from base — the whole sampler in one function.
base is the trial’s
sampler_seed. Exposed because
it is the contract: any sampler that wants “the uniform draw this
parameter would have got” (a fallback, a restart, a comparison) must
produce the same bits, and the only way to guarantee that is to call
this.
§Errors
Error::InvalidSpace if the
distribution is malformed — propagated verbatim from
Distribution::from_unit.
Trait Implementations§
impl Copy for Random
impl Eq for Random
§impl Sampler for Random
impl Sampler for Random
§fn infer_relative_space(&self, study: &StudyView) -> Result<SpaceSchema>
fn infer_relative_space(&self, study: &StudyView) -> Result<SpaceSchema>
The study’s declared space when it has one; otherwise the intersection of the visible trials’ parameter sets.
§Define-by-run semantics
This is Optuna’s IntersectionSearchSpace, made explicit:
- only
visibletrials count (CompleteandPruned) — a crashed trial must not teach the sampler anything, not even which parameters exist; - a name survives only if every visible trial recorded it and
every recording is
compatible with the first one.
Numeric bounds may therefore drift between trials; a changed
categorical choice set, log flag or kind drops the parameter from the
joint space, and it is then sampled through
sample_independentinstead; - the surviving distribution is the first visible trial’s;
- with no visible trials the result is the empty schema, which means “sample everything independently”.
The result is ordered by name: an inferred space has no declaration order to preserve, and a name order is the only order that does not depend on which trial happened to complete first.
§Errors
Error::InvalidSpace if a
recorded parameter name is empty, which no writer of this crate
produces.
§fn sample_relative(
&self,
_study: &StudyView,
trial: &TrialMeta,
space: &SpaceSchema,
) -> Result<Assignment>
fn sample_relative( &self, _study: &StudyView, trial: &TrialMeta, space: &SpaceSchema, ) -> Result<Assignment>
Draws every parameter of space, each independently of the others.
The returned assignment binds exactly the names space declares.
§Errors
Error::InvalidSpace if a declared
distribution 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>
Draws exactly one parameter.
Returns bit-for-bit what sample_relative
would have bound to name, because both call
Random::draw.
§Errors
Error::InvalidSpace if dist is
malformed.
§fn reseed(&self, _seed: u64)
fn reseed(&self, _seed: u64)
A no-op — and deliberately so.
Sampler::reseed exists for samplers that
carry auxiliary randomness which two parallel workers must not draw in
lockstep. Random has none: every value it produces is a pure function
of (trial.sampler_seed, name, distribution), and sampler_seed is
derived by the framework from the study seed and the trial number.
What this call therefore does not affect:
- any value sampled for any trial, before or after the call;
- the reproducibility of a study — which is the point. If reseeding
moved the draws, a worker calling it would silently break promise 2
and two workers would disagree about what trial
nis.
To change what a study samples, change the study seed
(StudyConfig::with_seed); it
is configuration, not runtime state.