Skip to main content

StudyBuilder

Struct StudyBuilder 

pub struct StudyBuilder { /* private fields */ }
Expand description

Builds a Study, injecting the handles it drives.

Every seam is injected and every default is stated: a study never constructs a clock, reaches for the system time, or picks a storage backend on the caller’s behalf.

KnobDefault
storageMemStorage — in memory, nothing survives the process
samplerRandom
schedulerNopScheduler — nothing is ever pruned
clockSystemClock with the system feature, otherwise a ManualClock frozen at 0
observernone (structured tracing events are still emitted)
workerWorkerId(0)
budgetBudget::new
parallelism1
retry policyRetryPolicy::new
use atune_core::clock::ManualClock;
use atune_core::sampler::Grid;
use atune_core::space::{Distribution, ParamSpec, SpaceSchema};
use atune_core::study::{Study, StudyConfig};
use std::sync::Arc;

let space = SpaceSchema::try_new([
    ParamSpec::try_new("opt", Distribution::try_new_with_categorical_labels(["adam", "sgd"])?)?,
])?;
let study = Study::builder()
    .sampler(Arc::new(Grid::try_new(space.clone())?))
    .clock(Arc::new(ManualClock::new(0)))
    .create(StudyConfig::new("grid").with_space(space))?;

assert_eq!(study.trial_count()?, 0);

Implementations§

§

impl StudyBuilder

pub const fn new() -> Self

Starts a builder with every seam at its default.

pub const fn target_value(self, value: f64) -> Self

Stops this handle’s search once the study’s best trial reaches value.

Before each ask the handle reads the canonical constraint-aware best_trial; when that trial is feasible (or declares no constraints) and its value is at least as good as value in the study’s direction, ask returns Ok(None) and optimize ends normally. A resumed study whose best already meets the target therefore starts no trial. The check costs one extra snapshot read per ask, and only when a target is set.

The target belongs to this handle and is not persisted. It needs a single-objective study and a finite value; create and load refuse anything else.

pub fn fan_sync_steps(self, steps: impl IntoIterator<Item = u64>) -> Self

Makes a multi-seed fan rung-synchronous at steps.

A fan normally runs each replicate to completion before the scheduler sees any aggregate, so a pruner can only cut a fan after all of its replicates have finished. With sync steps, the study runs every replicate up to the first step, hands the scheduler the aggregate of each aligned step reported so far, and only then runs the replicates on to the next step. A Decision::Prune or Decision::Pause at a sync step therefore stops every replicate there. Pass a scheduler’s rung steps, such as RungLadder::rungs.

A replicate yields on its first report at or past a sync step: that call returns Error::TrialPaused, and should_stop does too. The objective must be resumable: record a checkpoint with record_checkpoint before reporting, return the error, and on its next run continue from parent_checkpoint and parent_step, which hold that replicate’s own latest reference. An objective that keeps no checkpoint restarts and reaches the same reports, at the cost of repeated work.

The steps belong to this handle and are not persisted. Duplicates are ignored and order does not matter. create and load refuse a zero step, and any step on a study whose fan is not wider than one seed.

pub fn storage(self, storage: Arc<dyn Storage>) -> Self

Uses storage as the study’s backend.

In-memory by default: a study built without this call runs against a fresh MemStorage, which is right for a first program, a test and a study whose results are read before the process exits — and wrong for anything durable, resumable or shared between processes. Call this for those.

A backend used for Study-driven trial execution must expose TrialLifecycleStorage through Storage::trial_lifecycle. The base Storage trait alone supports direct CRUD and observation, but Study::ask and Study::optimize return Error::Unsupported when the atomic lifecycle capability is absent.

pub fn share_queue_from(self, source: &Study) -> Self

Shares template-queue ownership with source for a compatible load.

The queue is the only source state retained. A subsequent load must name the same study and use the exact same storage Arc; otherwise it fails before reading or recovering the backend. This method is intentionally incompatible with create, which rejects it before creating anything.

pub fn share_finalization_runtime_from(self, source: &Study) -> Self

Shares finalization admission with source for a compatible load.

This is an explicit companion to share_queue_from for adapters that rebuild a handle while reusing the same underlying mutable sampler and scheduler state. Transparent wrappers may delegate to those seams; this method does not select or verify the seam objects. It shares only the local finalization runtime, and queue ownership remains independent unless share_queue_from is also used. A subsequent load must name the same study and use the exact same storage Arc; otherwise it fails before reading or recovering the backend. This method is intentionally incompatible with create, which rejects it before creating anything.

pub fn sampler(self, sampler: Arc<dyn Sampler>) -> Self

Uses sampler instead of Random.

pub fn scheduler(self, scheduler: Arc<dyn Scheduler>) -> Self

Uses scheduler instead of NopScheduler.

pub fn clock(self, clock: Arc<dyn Clock>) -> Self

Uses clock as the study’s only source of time.

pub fn observer(self, observer: Arc<dyn StudyObserver>) -> Self

Sends typed lifecycle events to observer.

Delivery is synchronous with each emitting operation; parallel trials may invoke the observer concurrently. Observer failures never become study errors. Implementations should enqueue lightweight notifications and perform blocking work elsewhere.

pub const fn worker(self, worker: WorkerId) -> Self

Names this handle among the study’s workers.

Two workers sharing a study must not share an id: it is what a lease names as its owner, what FrozenTrial::worker records, and therefore what tells a fail-over sweep — and a human reading the study — whose trial a stale heartbeat belongs to. The identity is readable back through Study::worker, and every lease this handle claims stamps it onto the trial record.

It names the handle, not the Storage behind it. A backend that carries its own worker option (JournalOptions::worker, SqliteOptions::worker) stamps that one on the writes it performs itself — see FrozenTrial::worker for the full rule.

pub const fn budget(self, budget: Budget) -> Self

Limits what the study may spend.

pub const fn parallelism(self, n: usize) -> Self

Evaluates n trials at once.

n <= 1 is sequential and spawns nothing, which is the only form available on wasm32-unknown-unknown; n > 1 needs the system feature and is rejected by Study::optimize without it.

pub const fn retry_policy(self, policy: RetryPolicy) -> Self

Re-enqueues failed configurations according to policy.

pub const fn seed_protocol(self, protocol: SeedProtocol) -> Self

Evaluates each trial as a multi-seed fan under protocol.

The default is SeedProtocol::new (single-seed, no fan), which takes the ordinary one-evaluation-per-trial path. A protocol with a fan width above one turns a trial into k replicates of the same configuration under k seeds, aggregated into the sampler-visible objective (multi-seed protocol). See Study::reevaluate for the final held-out-seed stage.

§This applies only when create-ing

The protocol is a persisted property of the study (StudyConfig::seed_protocol): create writes what you set here into the config it stores, and the study evaluates trials under it. On load this setting is ignored — a resumed study takes the protocol from its stored config, so it can never silently mix a fanned study’s aggregate objective with fan-free raw draws. Passing a protocol here and then load-ing is therefore a no-op, not an override.

pub fn create(self, cfg: StudyConfig) -> Result<Study>

Creates a new study in storage.

§Errors

Error::InvalidSpace if the configuration does not validate; Error::Conflict if the backend enforces unique names and the name is taken; Error::Storage on a backend fault.

pub fn load(self, id: StudyId) -> Result<Study>

Loads an existing study — the resume path.

The configuration (and therefore the seed and the SeedProtocol) comes from storage, so a second handle reproduces the first one’s sampling and its per-trial fan: a study created with a k-seed fan resumes as a k-seed fan even from a plain builder that set no protocol. Any protocol passed to this builder via seed_protocol is ignored. Trial numbering continues where storage left it, and the trial budget counts the trials already in the study.

Writable load also recovers durable finalization outboxes before it returns. If another owner holds the study-wide finalization claim, a system build reclaims it only after the complete claim observation stays unchanged for three seconds of local monotonic time, using nominal 100 ms polling. Without system, held ownership fails immediately with backpressure. This recovery grace is independent of the Journal’s append-lock policy.

§Errors

Error::NotFound if the study does not exist; Error::Backpressure for same-handle admission or an active durable finalization owner; Error::Unsupported if the backend lacks finalization ownership or bounded outbox paging; Error::ResourceLimit if recovery cannot traverse its outbox within the backend’s supported bounds; Error::Conflict or Error::Incompatible for invalid persisted state; Error::LeaseLost if the exact acquisition observation loses a race; or Error::Storage on a backend fault.

§

impl StudyBuilder

pub fn policy(self, policy: SpacePolicy) -> Self

Declares this study’s growth policy — which parameters are open, and how.

Create-time plumbing, exactly as sampler is: create persists one Scope::Named slot per named parameter, and load ignores this field — a resumed handle reads the stored slots on demand, so the storage is the single source of truth. StudyConfig is untouched.

Every Open is validated at create, and a policy naming a declared parameter must seed exactly the declared distribution — the range has one home, not two.

Trait Implementations§

§

impl Debug for StudyBuilder

§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

The knobs; the seam handles are opaque by design.

§

impl Default for StudyBuilder

§

fn default() -> Self

As new.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more