mixle.doe.sequential module¶
Sequential design loop – the one orchestration primitive both adaptive-design experiments proved was missing, extracted as a small, composable driver instead of hand-rolled per demo.
Every adaptive-design pipeline built end-to-end (adaptive groundwater monitoring, adaptive gravity survey design) hand-wrote the same stateful loop: fit a model to the data so far -> summarize its uncertainty -> ask a controller whether the uncertainty is tight enough to stop or another sample is needed -> if continuing, use a design criterion to propose the next sample -> acquire it, append, repeat. This is that loop, parameterized. It is deliberately NOT a general workflow engine (branching DAGs, autonomous pipeline composition from a natural-language goal): that broader orchestration layer is a real but separate design question whose scope depends on intended usage, left open on purpose. This is only the part that is unambiguously right regardless of that answer, because a sequential experimental-design loop is a real thing people run either way, and both demos proved its absence forces hand-rolling.
It composes the rest of this codebase’s decision machinery rather than reinventing it:
should_continueis any(history) -> {"keep_going": bool, "reason": str}callable – e.g. wrapmixle.analysis.real_options.voi_stopping_decision()(stop when the value of the next sample drops below its cost) or an LLM controller viamixle_mlops.core.decisions.structured_decision(a forced, un-self-contradictable STOP/CONTINUE).
proposeis any(state, history) -> action– e.g. wrapmixle.doe.active.expected_information_gain_linear()/ a monitoring-network design.a calibration guard (
mixle.inference.calibration_gate) can be dropped intosummarizeso a round whose posterior is miscalibrated is flagged in the record the controller sees.
The audit trail (SequentialDesignResult) is meant to be trustworthy even when a round goes
wrong: summary/decision records are detached portable JSON values, and every callback,
validation, and history-copy failure is recorded as an explicit failed round before the selected
on_error policy returns or raises – never a silent, traceless abort.
- class DesignRound(index, state, summary, decision, proposed_action=None, failed=False, failed_step=None, error=None)[source]
Bases:
objectOne round of a sequential design: the fitted
state, its uncertaintysummary, the controllerdecisionthat round, and theproposed_actionchosen next (Noneon the final round, where the loop stopped and proposed nothing – or on a round that failed before a proposal was made, see below).summaryanddecisionare the small, JSON-friendly audit fields: whatsequential_design()stores is always a deep copy of what the callback returned, and what later callbacks see viahistoryis always a deep copy of what is stored, so no callback (nor any code holding a reference to a value it once returned) can retroactively rewrite a past round’s record.stateandproposed_actionare shared by reference – they may be large or otherwise-uncopyable fit artifacts (a GP posterior, a torch module) that callbacks should treat as read-only by convention.If any callback, record validation, or history-copy stage fails during this round, that failure is recorded rather than left implicit:
failedisTrue,failed_stepnames the exact stage, anderroris a shortf"{type}: {message}"description of the exception. A failed round’sstate/summary/decisionreflect whatever completed before the failure.
- exception SequentialDesignError(message, *, result)[source]
Bases:
RuntimeErrorRaised when a callback, validation, or history-copy stage fails under
on_error="raise".resultis the partial audit trail up to and including the failed round (seeDesignRound.failed), so a caller that wants both fail-fast propagation and the ability to inspect what happened can do:try: sequential_design(..., on_error="raise") except SequentialDesignError as exc: log(exc.result.rounds) # the audit trail survives the raise raise
The original callback exception is available as
__cause__(standard exception chaining).- Parameters:
message (str)
result (SequentialDesignResult)
- Return type:
None
- class SequentialDesignResult(rounds=<factory>, stopped_reason='')[source]
Bases:
objectThe full audit trail of a sequential design: every round in order, plus why it stopped.
- sequential_design(initial_data, *, fit, summarize, should_continue, propose, acquire, combine, max_rounds, on_error='raise')[source]
Run a stateful sequential experimental-design loop and return its full audit trail.
Each round:
fit(data) -> state;summarize(state, round_index) -> uq_summary;should_continue(history) -> {"keep_going": bool, "reason": str, ...}. If the controller stops (or the round budget is hit), the loop ends. Otherwisepropose(state, history) -> actionpicks the next sample,acquire(action) -> new_dataobtains it (a real simulation/survey/measurement), andcombine(data, new_data) -> datafolds it in for the next round.- Parameters:
initial_data (Any) – the starting dataset (whatever
fitconsumes).fit (Callable[[Any], Any]) – data -> state (e.g. a posterior). Called once per round on the accumulated data.
summarize (Callable[[Any, int], dict[str, Any]]) – (state, round_index) -> a JSON-friendly uncertainty summary dict. This is what the controller sees, so put the decision-relevant numbers (and any calibration flag) here.
should_continue (Callable[[list[DesignRound]], dict[str, Any]]) – (history-so-far, including the current round’s state/summary) -> a dict with an actual Boolean
"keep_going"and a nonempty string"reason". Everything else in the dict is recorded as the round’s decision.historyis a copy-safe view (seeDesignRound) – mutating it does not affect the returned audit trail.propose (Callable[[Any, list[DesignRound]], Any]) – (state, history) -> the next action/design point. Return
Noneto stop the loop even though the controller wanted to continue (no admissible next sample) –stopped_reasonis then"no_proposal".historyis the same copy-safe view.acquire (Callable[[Any], Any]) – action -> the new observation(s) from actually taking that sample.
combine (Callable[[Any, Any], Any]) – (data, new_data) -> the updated dataset for the next round.
max_rounds (int) – maximum number of adaptive acquisitions after the initial fit. The audit trail therefore contains at most max_rounds + 1 fitted states: index 0 for the initial data, followed by one state per acquired sample.
on_error (str) –
what to do when any callback, audit-record validation, or history-copy stage raises. In every case the failure is first recorded as an explicit failed round (see
DesignRound.failed) so the audit trail always shows what happened – the policy only controls what happens after that:"raise"(default): re-raise asSequentialDesignError(chained to the original exception via__cause__), carrying the partial result as.result. Matches the historical behavior of letting the exception propagate, but the caller can now still recover the audit trail up to the failure by catching it."record_and_stop": swallow the exception and return the partial result normally, withstopped_reason == "callback_error"and the failed round’sfailed/failed_step/errordescribing what happened.
There is deliberately no “continue past a failed round” option: a round that fails in
propose/acquire/combinenever obtained new data, so the next round would just re-fit on the samedata– not meaningful forward progress. Retry that yourself (e.g. by callingsequential_design()again) if that is what you want.
- Returns:
A
SequentialDesignResult– everyDesignRoundin order plusstopped_reason.- Return type:
SequentialDesignResult