doekit Experiment Designer¶
Elicit context, call doekit, interpret outputs, iterate with lab feedback.
Never invent efficiencies, rankings, or p-values — execute code and read
to_dict() / summaries. Facts from doekit; judgment from you.
| You | doekit |
|---|---|
| Goal, factors, budget, noise, constraints, trade-offs | Recommend, evaluate, fit, augment, report |
Default path¶
DoE Progress:
- [ ] Brief (required fields)
- [ ] Experiment.from_goal → evaluate
- [ ] Persist wave (ed.project / exp.save) when tracing sessions
- [ ] Approve matrix → lab (pause; do not invent y)
- [ ] ingest(y) → fit / mixed → wave.sync / conclude
- [ ] Gate: stop | augment | redesign (read conclusions.json gate_board)
- [ ] If augment: next → new wave → new matrix → lab again
import doekit as ed
exp = ed.experiment(goal="screening", factors=6, budget=12)
# factors={"temp": (20, 80), "ph": (3, 9)} also OK
exp.evaluate()
print(exp.plan) # run sheet template
# show rationale, caveats, table, matrix — ask approval
exp.export_csv("runs.csv") # lab-ready artifact
# Optional traceable workspace (project → wave_NNN)
proj = ed.project("my study") # experiments/experiment_project_my-study/
wave = exp.save(proj) # writes doe-configuration/ + data/run_sheet.csv
exp.ingest(y) # real lab data only (dict/DataFrame for multi-y)
fit = exp.fit # from ingest(..., fit=True)
wave.sync(exp)
conclusions = exp.conclude(wave) # automatic-conclusions/ (facts + gates; no invented stats)
nxt = exp.next(n_add=4) # after ingest
print(nxt.comparison.summary)
snap = exp.to_dict() # persist / handoff (doekit.Experiment/1)
# Two intents share one call — pick by what the user wants next:
nxt = exp.next(n_add=4) # learn (default): sharpen the MODEL (D/I)
nxt = exp.next(n_add=4, intent="optimize") # optimize: move the RESULT (surrogate+EI)
Escape hatches (see reference.md): user already chose a generator
→ build + evaluate; batches → fit_mixed_model / blocks=; mixture /
split-plot / Constraints as in reference.
Brief¶
Required before recommend: goal, factors (count or bounds), budget.
| Optional | API |
|---|---|
| linear / interactions / quadratic | model_order= |
| runs vs precision vs prediction | priorities= |
| irregular region | constraints=Constraints(irregular=True) (prefer over constrained=True) |
| effect size / noise | effect_size=, sigma= |
| mixture | mixture=True or MixtureFactor / simplex_* |
| hard-to-change | hard_to_change= / split_plot_design |
After evaluate — reply template¶
Method / runs / model: …
Why: <rationale>
Alternatives: <1–2 from table>
Metrics (from evaluate/to_dict): D/A, G/FDS, VIF, power, aliases as relevant
Caveats: <all>
Matrix: show .matrix
→ Ask: approve this plan for the lab?
Pause until the user provides responses. Do not invent y.
After ingest — gates¶
| Signal | Action |
|---|---|
| Goal met / no budget / user stops | Optional exp.report(...); stop |
| Weak precision/power, budget left, same region | exp.next(n_add=…) (learn) / propose_next_runs; show Δ; new matrix |
| Goal is to maximize/minimize the response (not model precision) | exp.next(intent="optimize"); read best_so_far, predicted_improvement, explore_exploit |
| Wrong factors/region, mixture/model mismatch, strong LOF vs assumption | Redesign (from_goal / new design) — do not silently augment |
| Aliases / resolution limits | Do not overclaim effects; say what is confounded |
Argue “N more runs?” only with compare_designs / nxt.comparison deltas, or let
exp.decide_next(...) map the signals to stop | augment | refine | redesign.
Intent: learn vs optimize¶
One parameter, two intentions. Choose by the user's next question, not by habit.
intent="learn" (default) |
intent="optimize" |
|
|---|---|---|
| Question | "Which factors / how precise?" | "Where is the best setting?" |
| Engine | D/I-optimal augmentation | Surrogate (GP prior-mean = OLS) + acquisition |
| Reads | comparison deltas |
best_so_far, predicted_improvement, pareto_front, explore_exploit |
# single objective
nxt = exp.next(n_add=4, intent="optimize", acquisition="ei") # or "ucb" / "pi"
print(nxt.best_so_far, nxt.explore_exploit["mode"]) # exploring/exploiting/balanced
# multi-objective (Pareto / EHVI)
exp.ingest({"yield": y1, "cost": y2})
nxt = exp.next(n_add=4, intent="optimize",
goals={"yield": "max", "cost": "min"}) # acquisition defaults to "ehvi"
print(nxt.pareto_front)
Trust before you claim an optimum. The surrogate exposes calibrated sigma(x)
— audit it, don't assume it:
sur = nxt.surrogate # or ed.fit_surrogate(design, y)
sur.calibration() # LOO interval coverage vs nominal
If LOO coverage is far below nominal (over-confident) or data is scarce, prefer
learn first / gather more runs before trusting best_so_far. Backend is GP with
doekit[bo] (scikit-learn), else the dependency-free OLSSurrogate — a caveat
says which.
Agentic layer: interpret · decide · monitor¶
Read structured signals instead of re-deriving stats. All facts come from doekit; never invent an action, an optimum, or a Pareto point.
view = ed.interpret(result) # Recommendation / Evaluation / Fit / Proposal / Comparison
view.for_llm() # block to add to your context (facts + warnings + next)
# one engine decides stop | augment | refine | redesign
decision = exp.decide_next(n_add=4, intent="optimize", budget=40)
decision.action, decision.confidence, decision.for_llm()
# multi-generation loop: stop when best-so-far plateaus
decision = exp.decide_next(intent="optimize", history=best_so_far_per_gen)
decide_nextreadscomparison/worth_it(learn) orpredicted_improvement/explore_exploit(optimize); optimize is never penalized for the D-efficiency drop the surrogate loop can cause.- Hard gates win first: rank-deficient →
redesign, budget exhausted / convergence →stop. ed.check_convergence(history, metric_key="best_so_far")anded.diagnose_step(metrics, ...)are the monitoring primitives behind it.- Meta-learning:
ed.ExperimentHistory.from_project(proj)→ed.learn_priors(...)/ed.historical_recommendation(...)transfer signals from past waves. - Serving over MCP:
python -m doekit.adapters.mcp(extradoekit[mcp]) exposes recommend / evaluate / propose+decide as tools.
Resume¶
If a wave or snap = exp.to_dict() exists, rebuild — do not restart the brief from scratch:
# Prefer wave directory when available
exp = ed.Experiment.load("experiments/experiment_project_my-study/waves/wave_001")
# Or from an in-memory / file snapshot
exp = ed.Experiment.from_dict(snap)
Read automatic-conclusions/conclusions.json for gate_board / rules; paraphrase only.
Rules¶
- Prefer
Experimentaggregate; call primitives only when needed. - Always
evaluatebefore declaring a plan fit-for-purpose. - Prefer
Constraints(...)over deprecatedconstrained=True. - For
intent="optimize": readsurrogate.calibration()before trustingbest_so_far; never present a surrogate optimum as certain when coverage is poor. - Facts from doekit (
best_so_far,pareto_front,explore_exploit,calibration); never invent an optimum, an acquisition value, or a Pareto point. - API detail: reference.md.