Trait Space
pub trait Space: Sized {
// Required methods
fn schema() -> Result<SpaceSchema>;
fn decode(assignment: &Assignment) -> Result<Self>;
fn encode(&self) -> Assignment;
// Provided method
fn policy() -> Result<SpacePolicy> { ... }
}Expand description
A typed configuration that doubles as a search space.
Implementing Space means “this Rust type is the thing being tuned”:
schema states which parameters exist and over what
support, decode turns one sampled point (an
Assignment) into a typed value, and encode turns a
typed value back into a point (for warm-starting, enqueuing a known-good
configuration, or recording one). The guide covers the two-layer model
under two ways to write a space.
You rarely write this by hand: #[derive(Space)] generates it for a flat
config struct. The manual implementation below is what the derive produces
in spirit, and is the specification a hand-written impl (or the derive)
must satisfy.
§The round-trip contract
For any Assignment that satisfies schema (every
declared parameter present and in support):
decodesucceeds, andencode(decode(a)?)reproducesarestricted to the declared parameters — parameters the schema does not declare (a fixed field under the derive) do not appear in either the schema or an encoded point.
A fixed field is therefore present on the decoded value but never on the
encoded assignment; where its value comes from on decode is the
implementation’s business (the derive documents its own rule).
Precision caveat. The schema stores every numeric bound as f64, so
encode(decode(a)?) reproduces a exactly only when the field type can
hold the sampled value losslessly. An f32 field is the exception: decode
narrows f64 → f32 and encode widens it back, so for an a carrying an
f64 that is not representable in f32 (most sampled coordinates, 0.1
included) the reproduced value matches a only to f32 precision. The
converse identity that the framework actually relies on — decode(encode(x)) == x for a typed value x — is always exact, because f32 → f64 → f32
is lossless. Integer fields never drift: a value that does not fit the field
type is a clean Error::OutOfRange on
decode, not a silent truncation.
§Why schema is fallible
It returns Result rather than a bare SpaceSchema because a schema is
assembled from the fallible Distribution and
SpaceSchema constructors, and a library path must not panic. A
malformed support (low > high, a logarithmic range with a non-positive
lower bound, …) surfaces as a clean Err, never an unwrap. This is the
one shape adjustment from the design’s sketch of the trait, taken for
exactly that reason.
§Errors
schema and decode return
Error::InvalidSpace /
Error::OutOfRange as their
implementations dictate; encode is total.
use atune_core::error::Result;
use atune_core::space::{Assignment, Distribution, ParamValue, Space, SpaceSchema};
struct Toy {
lr: f64,
epochs: u32,
}
impl Space for Toy {
fn schema() -> Result<SpaceSchema> {
let mut schema = SpaceSchema::empty();
schema.declare("lr", Distribution::float_log(1e-5, 1e-2)?)?;
schema.declare("epochs", Distribution::int(1, 16)?)?;
Ok(schema)
}
fn decode(assignment: &Assignment) -> Result<Self> {
let lr = match assignment.get("lr") {
Some(ParamValue::F64(v)) => *v,
_ => return Err(atune_core::error::Error::OutOfRange {
param: "lr".into(),
detail: "missing or not a float".into(),
}),
};
let epochs = match assignment.get("epochs") {
Some(ParamValue::I64(v)) => u32::try_from(*v).map_err(|_| {
atune_core::error::Error::OutOfRange {
param: "epochs".into(),
detail: "does not fit u32".into(),
}
})?,
_ => return Err(atune_core::error::Error::OutOfRange {
param: "epochs".into(),
detail: "missing or not an integer".into(),
}),
};
Ok(Toy { lr, epochs })
}
fn encode(&self) -> Assignment {
let mut assignment = Assignment::new();
assignment.insert("lr", ParamValue::F64(self.lr));
assignment.insert("epochs", ParamValue::I64(i64::from(self.epochs)));
assignment
}
}
let schema = Toy::schema().unwrap();
assert_eq!(schema.names().collect::<Vec<_>>(), ["lr", "epochs"]); // declaration order
let toy = Toy { lr: 3e-4, epochs: 8 };
let point = toy.encode();
let back = Toy::decode(&point).unwrap();
assert_eq!(back.epochs, 8);
assert!((back.lr - 3e-4).abs() < f64::EPSILON);Required Methods§
fn schema() -> Result<SpaceSchema>
fn schema() -> Result<SpaceSchema>
The declared search space: parameter names to supports, in declaration order.
§Errors
Whatever the underlying Distribution /
SpaceSchema constructors reject — typically
Error::InvalidSpace.
fn decode(assignment: &Assignment) -> Result<Self>
fn decode(assignment: &Assignment) -> Result<Self>
Reconstructs the typed configuration from one sampled point.
A hostile or malformed assignment (a missing parameter, a value of the
wrong kind, a number outside the field type’s range) yields a clean
Err, never a panic.
§Errors
Error::OutOfRange naming the first
parameter that is missing or cannot be converted to its field type.
fn encode(&self) -> Assignment
fn encode(&self) -> Assignment
Renders the typed configuration back into a point.
Total: it maps every tuned field to a ParamValue.
Fields excluded from the schema (fixed fields, under the derive) are
omitted, so the result round-trips through decode.
Provided Methods§
fn policy() -> Result<SpacePolicy>
fn policy() -> Result<SpacePolicy>
The growth policy this space declares — which parameters are open, and how (§6.2 of the open-search-spaces plan).
Defaulted to the empty policy, so a hand-written implementation and
every derive without an open/around field change nothing. The
derive overrides it when a field opts in; hand it to
StudyBuilder::policy alongside
schema.
§Errors
Whatever SpacePolicy::insert’s
validation rejects — typically
Error::InvalidSpace.
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".