Skip to main content

Space

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):

  • decode succeeds, and
  • encode(decode(a)?) reproduces a restricted 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>

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>

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

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>

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".

Implementors§