plesty.lib.experiment.base_experiment
Asynchronous Experiment ABC with plan execution and checkpoint/resume.
An Experiment orchestrates one or more Devices to execute a
measurement routine (issue plesty-lib#3). Its public lifecycle is
setup() → run() → teardown(), all asynchronous; run() is a
template method that executes the experiment’s Plan of
atomic steps with crash-safe status logging and resume — analogous to
checkpointing in deep learning: the plan is the training schedule, the
journal is the training log, and the completed-step set plus persisted step
results form the checkpoint.
Every run owns a directory (the convention is spelled out once, in
plesty.lib.experiment.runs, which also reads it back):
<run_root>/<run_id>/ # run_id = <name>_<YYYYmmdd-HHMMSS>
├── plan.json # frozen schedule + config + content hash
├── journal.jsonl # append-only event log (status, resume source)
├── records.jsonl # one line per completed step: its result document
└── data/ # blobs the records reference (arrays, images)
└── step_0003.npy
Blobs written by devices themselves (frames, images) never cross the
network: they go to a shared disk under <data_dir>/<run_id>/raw, in the
acquiring host’s spelling of the share (Experiment.raw_dir). The
share is named once, by environment — PLESTY_DATA_DIR as the host sees
it, PLESTY_DATA_MOUNT as this machine sees it — and both spellings are
journaled with run_started so any machine can read the run back. With
the mount set, run_root defaults to it: runs land on the share too.
Minimal runnable example (mock, no hardware):
import asyncio
from plesty.lib.data import PlestyArray
from plesty.lib.experiment import Experiment, Plan, Step
class LineScan(Experiment):
def build_plan(self) -> Plan:
steps = [
Step(id=f"scan[x={x}]", op="scan_point", params={"x": x})
for x in range(5)
]
return Plan(steps, config={"points": 5})
def scan_point(self, x: int) -> PlestyArray:
return PlestyArray([float(x)], name="signal", unit="a.u.")
run_id = asyncio.run(LineScan(run_root="runs").run())
# ... interrupted? Resume, skipping completed steps:
asyncio.run(LineScan(run_root="runs").run(resume=run_id))
Attributes
Exceptions
Raised when resuming a run whose stored plan differs from the current one. |
|
Raised when a plan step references an invalid experiment operation. |
Classes
Base class for PLESTY experiments with atomic scheduling and resume. |
Module Contents
- exception plesty.lib.experiment.base_experiment.PlanMismatchError
Bases:
RuntimeErrorRaised when resuming a run whose stored plan differs from the current one.
Initialize self. See help(type(self)) for accurate signature.
- exception plesty.lib.experiment.base_experiment.InvalidOperationError
Bases:
RuntimeErrorRaised when a plan step references an invalid experiment operation.
Initialize self. See help(type(self)) for accurate signature.
- plesty.lib.experiment.base_experiment._RESERVED_OPS
- class plesty.lib.experiment.base_experiment.Experiment(devices: plesty.lib.device.composite_device.CompositeDevice | None = None, run_root: str | pathlib.Path | None = None, name: str | None = None, max_retries: int = 3, retry_sleep: float = 3.0, data_dir: str | None = None, data_mount: str | None = None)
Bases:
abc.ABCBase class for PLESTY experiments with atomic scheduling and resume.
Subclasses override exactly three lifecycle hooks:
build_plan()— required: the atomic measurement schedule.setup()/teardown()— optional: extend device connection/release (callawait super().setup()/...teardown()).
plus one public method per step operation. Step operations are validated before a run starts — every
Step.opmust name an existing public callable on the experiment and must not be one of the reserved lifecycle methods above. Device access goes through theCompositeDevicepassed at construction — an Experiment receives Device instances, never raw hardware.Initialize the experiment.
- Parameters:
devices (Optional[plesty.lib.device.composite_device.CompositeDevice]) – Composite device holding all instruments this experiment orchestrates; connected in
setup()and disconnected inteardown().run_root (str | pathlib.Path | None) – Directory under which each run creates its own checkpoint directory;
NonetakesPLESTY_DATA_MOUNTfrom the environment (runs land on the share) and falls back toruns.name (Optional[str]) – Experiment name used in run ids; defaults to the subclass name in lowercase.
max_retries (int) – Attempts per step before the run aborts.
retry_sleep (float) – Seconds to wait between step retries.
data_dir (Optional[str]) – The shared data root as the acquiring host sees it;
NonetakesPLESTY_DATA_DIR. Device blobs go toraw_dirbeneath it.data_mount (Optional[str]) – The same root as this machine sees it;
NonetakesPLESTY_DATA_MOUNT. Journaled for readers.
- devices = None
- data_dir
- data_mount
- run_root
- name = ''
- max_retries = 3
- retry_sleep = 3.0
- run_id: str | None = None
- run_dir: pathlib.Path | None = None
- logger
- _plan_length = 0
- property raw_dir: str | None
Where this run’s device-written blobs go, in the host’s spelling.
<data_dir>/<run_id>/raw— one folder per run, so a directory the device lists on every acquisition never grows across runs, and a resumed run keeps writing where it started.Nonewithout a data root or before a run has started.- Return type:
Optional[str]
- abstractmethod build_plan() plesty.lib.experiment.schedule.Plan
Build the atomic measurement schedule for this experiment.
Must be deterministic for a given configuration: resume relies on the regenerated plan matching the persisted one by content hash.
- Return type:
- async setup() None
Prepare the experiment; the default connects and preflights all devices.
- Raises:
RigNotReadyError – If a sub-device does not answer or is not the device its configuration requires (see
CompositeDevice.preflight()). Nothing is measured on a rig that is not what the plan assumes.- Return type:
None
- async teardown() None
Release resources; the default disconnects all devices.
Always awaited by
run(), whether the run completes, fails, or is canceled.- Return type:
None
- async run(resume: str | None = None) str
Execute the plan, journaling every step; optionally resume a run.
- Parameters:
resume (Optional[str]) – Run id of an earlier stopped/canceled/aborted run to continue. Steps journaled as completed are skipped; the stored plan must match the current one by content hash.
- Returns:
The run id (directory name under
run_root) — pass it back as resume to continue after an interruption.- Raises:
InvalidOperationError – If a plan step references a missing, private, or reserved operation (checked before anything runs).
PlanMismatchError – If resuming and the stored plan differs from the plan the experiment generates now.
Exception – The final step error is re-raised after
run_abortedis journaled andteardown()has completed.
- Return type:
str
- _validate_plan(plan: plesty.lib.experiment.schedule.Plan) None
Check that every step operation is a valid public method.
Runs before any device is touched or any journal written, so a misconfigured plan fails fast instead of aborting mid-measurement.
- Raises:
InvalidOperationError – Listing every offending step.
- Parameters:
- Return type:
None
- _prepare_run(plan: plesty.lib.experiment.schedule.Plan, resume: str | None) tuple[str, set[str]]
Create a new run directory or validate and load a resumed one.
- Returns:
The run id and the set of step ids already completed — those the journal marks completed plus those with a committed record.
- Parameters:
resume (Optional[str])
- Return type:
tuple[str, set[str]]
- async _run_step(step: plesty.lib.experiment.schedule.Step, index: int, run_dir: pathlib.Path, journal: plesty.lib.experiment.journal.Journal) None
Execute one atomic step with retries and persist its result.
- Parameters:
index (int)
run_dir (pathlib.Path)
journal (plesty.lib.experiment.journal.Journal)
- Return type:
None