Enum Distribution
pub enum Distribution {
Float {
low: f64,
high: f64,
log: bool,
step: Option<f64>,
},
Int {
low: i64,
high: i64,
log: bool,
step: i64,
},
Cat {
choices: CatChoices,
},
Bool,
}Expand description
The support of one parameter.
§Validity
The variants carry public fields so that the shape matches the frozen seam
and pattern matching stays natural. That
means a caller can build a nonsensical distribution by hand. Every method
on this type therefore tolerates an invalid value: transforms return
Error::InvalidSpace, and the predicate-style methods return false /
None. Nothing panics. Prefer the fallible constructors
(Distribution::try_new_with_float, Distribution::try_new_with_int, …), which validate up
front; deserialization validates too.
§Stepped ranges are normalized
The constructors and the deserializer snap a stepped range’s high down
onto the step grid, because a high that is not reachable from low in
whole steps has no consistent meaning: contains, cardinality,
grid_values and from_unit would each have to guess. So
Distribution::try_new_with_stepped_float(0.0, 11.0, 3.0) stores high == 9.0, and the
effective bounds are always the ones the distribution reports. A high
that reconstructs to the nearest grid point within the bounded
float-rounding allowance is kept bit-for-bit:
Distribution::try_new_with_stepped_float(0.0, 0.7, 0.1) keeps 0.7 and has eight grid
points, even though 0.7 / 0.1 is not exact in binary.
A hand-built variant skips that normalization, so its high may sit off
the grid. The methods stay consistent regardless — they derive the grid
from low and step, never from an unreachable high.
Variants§
Float
A floating-point range.
Fields
Int
An integer range.
Fields
high: i64Inclusive upper bound. When step exceeds 1, the constructors
snap this down to the last grid point, exactly as for
Distribution::Float.
Cat
A categorical choice.
Fields
choices: CatChoicesThe stable labels; ParamValue::Cat indexes them.
Bool
A boolean, treated throughout as a two-choice categorical
(false = index 0, true = index 1).
Implementations§
§impl Distribution
impl Distribution
pub fn try_new_with_float(low: f64, high: f64) -> Result<Self>
pub fn try_new_with_float(low: f64, high: f64) -> Result<Self>
A continuous linear float range.
§Errors
Error::InvalidSpace if a bound is not finite or low > high.
pub fn try_new_with_log_float(low: f64, high: f64) -> Result<Self>
pub fn try_new_with_log_float(low: f64, high: f64) -> Result<Self>
A continuous logarithmic float range.
§Errors
As Distribution::try_new_with_float, plus low must be strictly positive.
pub fn try_new_with_stepped_float(
low: f64,
high: f64,
step: f64,
) -> Result<Self>
pub fn try_new_with_stepped_float( low: f64, high: f64, step: f64, ) -> Result<Self>
A linear float range discretized to a step grid anchored at low.
⚠️ high is snapped down onto the grid when it is not reachable
from low in whole steps — see Distribution::try_new_with_float_options.
use atune_core::space::Distribution;
// 11.0 is not 0.0 + k * 3.0 for any whole k: the bound moves to 9.0.
let d = Distribution::try_new_with_stepped_float(0.0, 11.0, 3.0).unwrap();
assert!(matches!(d, Distribution::Float { high, .. } if high == 9.0));
assert_eq!(d.cardinality(), Some(4)); // 0, 3, 6, 9
// 0.7 reconstructs to the nearest grid point, so it is kept.
let e = Distribution::try_new_with_stepped_float(0.0, 0.7, 0.1).unwrap();
assert!(matches!(e, Distribution::Float { high, .. } if high == 0.7));
assert_eq!(e.cardinality(), Some(8));§Errors
As Distribution::try_new_with_float, plus step must be finite and positive.
pub fn try_new_with_float_options(
low: f64,
high: f64,
scale: Scale,
step: Option<f64>,
) -> Result<Self>
pub fn try_new_with_float_options( low: f64, high: f64, scale: Scale, step: Option<f64>, ) -> Result<Self>
The general float constructor.
§Normalization
With a step, the stored high is the last grid point rather than
the declared bound. (high - low) / step is computed; if the declared
high matches the reconstructed nearest grid point within the bounded
float-rounding allowance used by contains,
high is kept verbatim. Otherwise the cell count is floored and high
is rewritten to low + n * step. This matches Optuna, which likewise
adjusts high for stepped distributions, and it is what makes
contains,
cardinality,
grid_values and
from_unit agree: the support has exactly
n + 1 points, the last of which is the reported high.
§Errors
Error::InvalidSpace if a bound or the step is not finite,
low > high, step <= 0, Scale::Log with low <= 0, or logarithmic scale combined
with a step (a logarithmic and uniformly stepped grid is
ill-defined; Optuna rejects it too).
pub fn try_new_with_int(low: i64, high: i64) -> Result<Self>
pub fn try_new_with_int(low: i64, high: i64) -> Result<Self>
pub fn try_new_with_log_int(low: i64, high: i64) -> Result<Self>
pub fn try_new_with_log_int(low: i64, high: i64) -> Result<Self>
A logarithmic integer range (implies unit step).
§Errors
As Distribution::try_new_with_int, plus low must be strictly positive.
pub fn try_new_with_stepped_int(low: i64, high: i64, step: i64) -> Result<Self>
pub fn try_new_with_stepped_int(low: i64, high: i64, step: i64) -> Result<Self>
A linear integer range discretized to a step grid anchored at low.
⚠️ high is snapped down onto the grid when it is not reachable
from low in whole steps — see Distribution::try_new_with_int_options.
use atune_core::space::Distribution;
let d = Distribution::try_new_with_stepped_int(0, 10, 3).unwrap();
assert!(matches!(d, Distribution::Int { high, .. } if high == 9));
assert_eq!(d.cardinality(), Some(4)); // 0, 3, 6, 9§Errors
As Distribution::try_new_with_int, plus step must be at least 1.
pub fn try_new_with_int_options(
low: i64,
high: i64,
scale: Scale,
step: i64,
) -> Result<Self>
pub fn try_new_with_int_options( low: i64, high: i64, scale: Scale, step: i64, ) -> Result<Self>
The general integer constructor.
§Normalization
As Distribution::try_new_with_float_options: with step > 1 the stored high
becomes low + n * step where n = (high - low) / step (integer
division), so the reported upper bound is always attainable. Integer
arithmetic is exact, so no tolerance is involved.
§Errors
Error::InvalidSpace if low > high, step < 1, logarithmic scale with
low <= 0, or logarithmic scale with step != 1.
pub const fn new_with_categorical_choices(choices: CatChoices) -> Self
pub const fn new_with_categorical_choices(choices: CatChoices) -> Self
A categorical distribution over an already-validated choice set.
pub fn try_new_with_categorical_labels<I, S>(labels: I) -> Result<Self>
pub fn try_new_with_categorical_labels<I, S>(labels: I) -> Result<Self>
A categorical distribution over labels.
§Errors
Error::InvalidSpace if the labels are empty or contain duplicates.
pub const fn new_with_boolean() -> Self
pub const fn new_with_boolean() -> Self
A boolean distribution.
pub const fn value_kind(&self) -> ValueKind
pub const fn value_kind(&self) -> ValueKind
The kind of ParamValue this distribution produces.
pub fn is_expansion_of(&self, seed: &Self) -> bool
pub fn is_expansion_of(&self, seed: &Self) -> bool
true when self could have been produced by growing seed —
same kind, same log flag, same step/choice structure, bounds a
superset, and (for a stepped support) a grid still anchored so that
every point seed supported remains supported.
A predicate, deliberately kept off the suggest hot path: the growth
tests are its consumers, the monotonicity property test
growth_keeps_every_seed_value_supported (space/growth/tests.rs,
N65) among them.
pub fn is_single_valued(&self) -> bool
pub fn is_single_valued(&self) -> bool
true if the support holds exactly one value.
Single-valued parameters skip sampling entirely: the study loop assigns the only legal value — the enqueued-fixed → single-valued → relative → independent priority.
pub fn validate(&self) -> Result<()>
pub fn validate(&self) -> Result<()>
Checks the distribution’s own invariants.
§Errors
Error::InvalidSpace describing the first violated invariant. See
the constructors for the full list.
pub fn contains(&self, value: &ParamValue) -> bool
pub fn contains(&self, value: &ParamValue) -> bool
true if value lies in this distribution’s support.
For a stepped distribution the value must also sit on the grid (within the bounded reconstructed float-rounding allowance). An invalid distribution contains nothing.
One float grid cannot be indexed at all: when (v - low) / step
overflows to infinity — a subnormal step — the grid is finer than
f64 can address, and every in-range value counts as on it. That is
the same reading cardinality (which
saturates) and from_unit (which skips the
snap) already take, so the four views keep describing one grid.
pub fn cardinality(&self) -> Option<u64>
pub fn cardinality(&self) -> Option<u64>
The number of distinct values, or None for a continuous float.
None means “unbounded resolution”, nothing else: every other
distribution — including a stepped float — is finite and reports its
count. An invalid distribution reports None. Counts saturate at
u64::MAX: a grid so fine that its cell count overflows is reported as
the largest representable count, never as the smallest.
pub fn grid_values(&self) -> Option<Vec<ParamValue>>
pub fn grid_values(&self) -> Option<Vec<ParamValue>>
Every value of a finite distribution, in ascending order.
None when cardinality is None (a
continuous float, or an invalid distribution), or when the grid
exceeds MAX_GRID_POINTS. This is the primitive the Grid sampler
enumerates.
The last element is bit-for-bit the reported high (for a stepped
range that is the normalized, attainable bound — see the type-level
Stepped ranges are normalized), so a caller may compare against the
bound exactly rather than within a tolerance.
§Allocation
The returned vector has cardinality
elements, which is why the MAX_GRID_POINTS ceiling exists: an
unbounded range such as Distribution::try_new_with_int(0, i64::MAX) returns None
rather than attempting — and failing — to allocate its grid. Nothing
here can panic or exhaust memory.
pub fn is_compatible_with(&self, other: &Self) -> bool
pub fn is_compatible_with(&self, other: &Self) -> bool
Whether two distributions may describe the same parameter of one study.
This is Optuna’s compatibility gate, made explicit (see when a space is wrong):
- the kind must match — a parameter never changes from float to categorical;
- the log flag must match — the meaning of a stored unit coordinate would otherwise change;
- categorical choice sets must be identical, because
ParamValue::Catstores an index and reordering the labels would silently rewrite history; - numeric bounds (and steps) may drift, because a define-by-run objective is allowed to widen or narrow a range between trials. Old values stay valid; they simply may fall outside the new support.
use atune_core::space::Distribution;
let a = Distribution::try_new_with_float(0.0, 1.0).unwrap();
let b = Distribution::try_new_with_float(0.0, 2.0).unwrap();
assert!(a.is_compatible_with(&b)); // bounds may drift
assert!(!a.is_compatible_with(&Distribution::try_new_with_log_float(1.0, 2.0).unwrap()));
let x = Distribution::try_new_with_categorical_labels(["adam", "sgd"]).unwrap();
let y = Distribution::try_new_with_categorical_labels(["adam", "sgd", "rmsprop"]).unwrap();
assert!(!x.is_compatible_with(&y)); // choice sets may notpub fn to_unit(&self, value: &ParamValue) -> Result<f64>
pub fn to_unit(&self, value: &ParamValue) -> Result<f64>
Maps a value to its unit coordinate in [0, 1].
| Kind | Formula |
|---|---|
| linear float / int | (v - low) / (high - low) |
| logarithmic float / int | (ln v - ln low) / (ln high - ln low) |
categorical (n choices) | (i + 0.5) / n — the cell centre |
| boolean | as a two-choice categorical: 0.25 / 0.75 |
A degenerate numeric support (low == high) maps to 0.0 instead of
dividing by zero. Categoricals have no such special case: a one-choice
categorical (and likewise a one-choice Cat-shaped support) maps to its
cell centre, 0.5. Step grids do not affect this direction: an in-range
value that is off the grid still gets its exact coordinate.
§Errors
Error::InvalidSpaceif the distribution is malformed;Error::OutOfRangeif the value has the wrong kind or lies outside the bounds.
pub fn from_unit(&self, u: f64) -> Result<ParamValue>
pub fn from_unit(&self, u: f64) -> Result<ParamValue>
Maps a unit coordinate back to a value in the support.
The inverse of to_unit, and total by
construction: u is clamped to [0, 1], the result is snapped to the
nearest step-grid cell with the cell index clamped to the grid, and
integers are rounded to nearest. It never panics, and
d.contains(&d.from_unit(u).unwrap()) holds for every u — the result
is always a point of the support, not merely a value inside the bounds.
Numeric bounds come back exactly: u = 0 gives low and u = 1 gives
high (or the last grid point below it), on a logarithmic scale too,
where exp(ln x) is not the identity in f64. Interior round trips,
from_unit(to_unit(v)), are exact up to f64 rounding, which for a
logarithmic integer is about v · ln v · 2^-52. Once that passes half
a step, from about 2^46 for unit steps, the round trip can return a
neighbouring integer; the result is still a point of the support.
Categorical coordinates use floor(u * n), clamped to n - 1, which
is the exact inverse of the cell-centre mapping used by to_unit.
§Errors
Error::InvalidSpaceif the distribution is malformed;Error::OutOfRangeifuis NaN (there is nothing to clamp it to).
Trait Implementations§
§impl Clone for Distribution
impl Clone for Distribution
§fn clone(&self) -> Distribution
fn clone(&self) -> Distribution
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more§impl Debug for Distribution
impl Debug for Distribution
§impl<'de> Deserialize<'de> for Distribution
impl<'de> Deserialize<'de> for Distribution
§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
§impl Display for Distribution
impl Display for Distribution
§fn fmt(&self, f: &mut Formatter<'_>) -> Result
fn fmt(&self, f: &mut Formatter<'_>) -> Result
Renders a human-readable representation in the string DSL.
For distributions produced by parse, the output re-parses to an
equal distribution (see the module documentation). This is not
lossless serialization for arbitrary hand-built distributions: choice
syntax cannot represent empty labels, commas, or leading or trailing
whitespace, and omits typed choice payloads. Quotes and backslashes are
ordinary label characters, not escape syntax. Use serialization that
preserves payloads when those must be retained.