Struct Open
pub struct Open { /* private fields */ }Expand description
One parameter’s declared growth policy: the seed it starts from, which side(s) may move, and the numbers that govern and bound that movement.
Also the ergonomic builder: Open::log, Open::linear,
Open::around, then Open::up / Open::down / Open::limit /
Open::factor / Open::max_expansions. Every setter is infallible
and merely records what it is told – nothing is validated eagerly, and no
growth decision is made by the builder. Call Open::validate to check the
result before passing it to the replay or widening engine.
use atune_core::space::{Open, Spread};
let lr = Open::log(1e-5..=1e-2).up().limit(..=1.0);
assert!(lr.validate().is_ok());
let width = Open::around(256.0, Spread::Times(2.0)).down();
assert!(width.validate().is_ok());
// A degenerate seed cannot grow (§7.3) -- caught by `validate`, not by
// the builder itself.
let degenerate = Open::linear(0.5..=0.5);
assert!(degenerate.validate().is_err());Open is serialized as a self-describing object – seed, sides,
limit (a named {"low": ..., "high": ...} pair, never a bare tuple),
factor and max_expansions – and compares with PartialEq, not
Eq: it holds f64 fields, and every one of them is validated finite
(§7.4), so PartialEq already behaves as a total equality in
practice without claiming the stronger trait for a type that holds a
float (matching crate::space::Assignment’s own reasoning).
Deserialization runs Open::validate, mirroring
Distribution’s own DistributionRepr precedent (space/distribution.rs):
a hand-edited state blob cannot introduce a factor <= 1.0 or a
max_expansions above the cap and have it reload silently.
Implementations§
§impl Open
impl Open
pub fn new(seed: Distribution) -> Self
pub fn new(seed: Distribution) -> Self
The general constructor: an open policy around an already-built
seed, with Sides::Both, no limit on either side, factor = 2.0 and max_expansions = 8.
This is what a caller that already has a Distribution – an
integer range, a stepped float, a log integer – reaches for;
Open::log, Open::linear and Open::around are sugar over
this for the common continuous-float cases. Nothing is validated
eagerly – see Open::validate.
A stepped seed is routed back through its own validated
constructor first, so a hand-built, off-grid high (legal per
Distribution::validate, but never reachable through
Distribution::float_step/Distribution::int_step) is snapped
onto the grid here rather than surviving until the next
deserialization does it instead – see renormalize_seed, below. An
already-invalid seed is left exactly as given, so
Open::validate, not this constructor, is what reports it.
pub fn log(range: RangeInclusive<f64>) -> Self
pub fn log(range: RangeInclusive<f64>) -> Self
An open seed sampled uniformly in the logarithm of the value, over
range – sugar for Open::new with a logarithmic
Distribution::Float. Mirrors Distribution::float_log’s
bounds without its up-front validation; call Open::validate to
check the result.
pub fn linear(range: RangeInclusive<f64>) -> Self
pub fn linear(range: RangeInclusive<f64>) -> Self
An open seed sampled uniformly over range – sugar for
Open::new with a linear Distribution::Float.
pub fn around(center: f64, spread: Spread) -> Self
pub fn around(center: f64, spread: Spread) -> Self
An open seed centered at center, spread by spread – §6.1’s
around, the “no idea at all” declaration. Spread::Times is
multiplicative and samples logarithmically:
[center / f, center * f]. Spread::Plus is additive and samples
linearly: [center - d, center + d].
pub fn around_int(center: i64, spread: Spread) -> Self
pub fn around_int(center: i64, spread: Spread) -> Self
Open::around’s integer twin: an open integer seed centered at
center, spread by spread. Spread::Times samples
logarithmically over [center / f, center * f]; Spread::Plus
linearly over [center - d, center + d]. Fractional bounds round
away from the centre — a spread is a declared uncertainty, and
rounding it inward would quietly shrink what the user asked for.
This constructor is infallible like its siblings, so an
unrepresentable spread — non-finite, or bounds outside f64’s
exact-integer window — is carried rather than guessed at: the seed
comes out inverted (low > high) and Open::validate refuses it
at declaration, exactly where a float around’s non-finite bound is
refused. Saturating instead would manufacture a legal-looking
i64::MAX bound out of an inf the user never meant.
pub fn up(self) -> Self
pub fn up(self) -> Self
Restricts growth to the upper bound only.
pub fn down(self) -> Self
pub fn down(self) -> Self
Restricts growth to the lower bound only.
pub fn limit(self, bound: impl Into<LimitBound>) -> Self
pub fn limit(self, bound: impl Into<LimitBound>) -> Self
Sets a hard bound on one side, merging with whatever the other side
already had. Spelled with ..=/.. on the standard range types –
§6.5 settles ..= as the one spelling for a limit, because a limit
is attainable: ..=1.0 sets the upper bound at 1.0, 1e-8..
sets the lower bound at 1e-8. Calling this twice, once per side,
sets both.
pub fn factor(self, factor: f64) -> Self
pub fn factor(self, factor: f64) -> Self
Overrides the default growth factor (2.0): the span is multiplied
by this on each growth step. Validated by Open::validate, not
here – see §7.4.
pub fn max_expansions(self, max_expansions: u32) -> Self
pub fn max_expansions(self, max_expansions: u32) -> Self
Overrides the default expansion cap (8, counted per side).
Validated by Open::validate, not here – see §7.4.
pub fn validate(&self) -> Result<()>
pub fn validate(&self) -> Result<()>
Checks this policy’s own declaration-time invariants – §7.3, §7.4
and §9.6. This is the single function that owns policy
validation. #[derive(Space)] rejects malformed attribute
combinations at compile time, while its generated policy construction
routes values through this runtime validation. The rules below remain
the canonical declaration-time checks:
- the seed is itself a valid, finite
Distribution–Distribution::validate: both bounds finite (and, for a stepped float, a finite positive step),low <= high, and a strictly positivelowwhen logarithmic. This is also where a non-finiteSpreadis caught:Open::aroundbakesSpread’s value into the seed’s bounds beforeOpenever stores it, so an infinite orNaNSpreadsurfaces here as a non-finite seed bound, not as a separate rule (Error::InvalidSpace, §7.4); - the seed is an ordered distribution –
Distribution::FloatorDistribution::Int(Error::Unsupportedotherwise: a categorical or boolean parameter has no bound to grow); - the seed has non-zero span (§7.3);
factoris finite and strictly greater than1.0(§7.4);max_expansionsis at most64(§7.4);- every bound of
limitis finite, when set (§7.4); - no bound of
limitexcludes the seed’s own range (§9.6); - no bound of
limitsits on a sidesidesdoes not permit to move (§9.6).
§Errors
Error::InvalidSpace naming the first violated rule above, or
Error::Unsupported for a categorical or boolean seed.
use atune_core::error::Error;
use atune_core::space::Open;
// factor == 1.0 moves no bound while still consuming an expansion.
let no_op_factor = Open::linear(0.0..=1.0).factor(1.0);
assert!(matches!(no_op_factor.validate(), Err(Error::InvalidSpace(_))));
// A ceiling below the seed's own high excludes the seed (§9.6).
let bad_limit = Open::log(1e-5..=1e-2).limit(..=1e-3);
assert!(matches!(bad_limit.validate(), Err(Error::InvalidSpace(_))));pub fn seed(&self) -> &Distribution
pub fn seed(&self) -> &Distribution
The seed this policy grows from: what was first declared (or, for
Open::around, what it computed), before any growth. Immutable once
declared (§7.2); study policy registration enforces write-once equality.
pub fn limit_high(&self) -> Option<f64>
pub fn limit_high(&self) -> Option<f64>
The hard upper bound this policy will not grow past, if any.
pub fn factor_value(&self) -> f64
pub fn factor_value(&self) -> f64
The growth factor this policy applies per expansion (§7’s factor).
pub fn max_expansions_value(&self) -> u32
pub fn max_expansions_value(&self) -> u32
The per-side expansion cap (§7’s max_expansions).