plesty.lib.experiment
Experiment orchestration: plans, journals, and the async Experiment ABC.
Submodules
Exceptions
Raised when a plan step references an invalid experiment operation. |
|
Raised when resuming a run whose stored plan differs from the current one. |
|
The rig failed its preflight and the operator did not (or could not) recover it. |
Classes
Base class for PLESTY experiments with atomic scheduling and resume. |
|
Crash-safe append-only event log for one experiment run. |
|
Read access to one run directory: plan, journal, records, data paths. |
|
A frozen, hashable schedule of atomic steps plus the run configuration. |
|
One atomic measurement step in an experiment plan. |
Functions
|
Build the rig, run its preflight, and ask the operator on a failure. |
|
Return the most recently started run under run_root. |
|
Return the run directories under run_root, oldest first. |
|
Split a run id into its experiment name and start time. |
|
Return the run id for experiment name started at when (now by default). |
Package Contents
- class plesty.lib.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
- exception plesty.lib.experiment.InvalidOperationError
Bases:
RuntimeErrorRaised when a plan step references an invalid experiment operation.
Initialize self. See help(type(self)) for accurate signature.
- exception plesty.lib.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.
- class plesty.lib.experiment.Journal(path: str | pathlib.Path)
Crash-safe append-only event log for one experiment run.
Attach the journal to path, creating parent directories.
- Parameters:
path (str | pathlib.Path) – Location of the
journal.jsonlfile; appended to if it already exists (the resume case).
- TERMINAL_EVENTS = ('run_completed', 'run_canceled', 'run_aborted')
- path
- append(event: str, **fields: Any) dict[str, Any]
Append one event line and fsync it to disk.
- Parameters:
event (str) – Event name, e.g.
"step_completed".fields (Any) – Additional payload stored on the event (step id, result path, error text, …).
- Returns:
The full event record that was written.
- Return type:
dict[str, Any]
- replay() list[dict[str, Any]]
Read all events back, tolerating a torn final line after a crash.
- Returns:
The recorded events in write order; an empty list if the journal file does not exist yet.
- Return type:
list[dict[str, Any]]
- completed_steps() set[str]
Return the ids of all steps journaled as completed.
Together with the steps that have a committed record line this is the resume set: a resumed run skips these and re-runs everything else.
- Return type:
set[str]
- status() dict[str, Any]
Summarize the run status by replaying the journal.
- Returns:
A dict with
state("not_started","running","completed","canceled"or"aborted"), completed and failed step counts, and the last recorded event.- Return type:
dict[str, Any]
- exception plesty.lib.experiment.RigNotReadyError(problems: dict[str, str], message: str)
Bases:
RuntimeErrorThe rig failed its preflight and the operator did not (or could not) recover it.
- Variables:
problems – Sub-device name → one-line problem.
- Parameters:
problems (dict[str, str])
message (str)
Keep the per-device problems next to the message.
- problems
- plesty.lib.experiment.connect_rig(build: collections.abc.Callable[[frozenset[str]], plesty.lib.device.composite_device.CompositeDevice], *, optional: collections.abc.Iterable[str] = (), ask: collections.abc.Callable[[str], str] | None = None, env_prefix: str | None = None, out: collections.abc.Callable[[str], None] = print) tuple[plesty.lib.device.composite_device.CompositeDevice, frozenset[str]]
Build the rig, run its preflight, and ask the operator on a failure.
- Parameters:
build (collections.abc.Callable[[frozenset[str]], plesty.lib.device.composite_device.CompositeDevice]) – Factory building the composite without the given (optional) sub-devices; called again on every retry, so an edited
.envor a freshly started server is picked up.optional (collections.abc.Iterable[str]) – Sub-devices the experiment can run without; when only these fail, the operator may choose to continue without them.
ask (Optional[collections.abc.Callable[[str], str]]) – Reads the operator’s answer to a prompt. Defaults to
inputwhen stdin is a terminal, else to no prompt at all (the diagnosis is logged andRigNotReadyErrorraised).env_prefix (Optional[str]) – Deployment prefix of the
<NAME>_ADDRESSvariables, for the diagnosis.out (collections.abc.Callable[[str], None]) – Where the diagnosis and the prompt go (
print).
- Returns:
The ready composite and the set of optional sub-devices skipped.
- Raises:
RigNotReadyError – If the operator aborts, or nobody can be asked.
- Return type:
tuple[plesty.lib.device.composite_device.CompositeDevice, frozenset[str]]
- class plesty.lib.experiment.Run(run_dir: str | pathlib.Path, *, mount: str | None = None, data_dir: str | None = None, replay_rows: int = 0)
Read access to one run directory: plan, journal, records, data paths.
Nothing here writes. The plan is read once; the journal is replayed on each
status()because a run under way keeps appending to it.Attach to a run directory.
- Parameters:
run_dir (str | pathlib.Path) – The run directory.
mount (Optional[str]) – This machine’s view of the shared data root;
NonetakesPLESTY_DATA_MOUNTfrom the environment, then the mount journaled by the machine that ran the experiment (only when that directory exists here).data_dir (Optional[str]) – The shared root as the acquiring host sees it — only for runs that did not record it themselves.
replay_rows (int) – How
source()hands records out —0follows the run live,nrations a stored run n records per poll.
- path
- replay_rows = 0
- _mount = None
- _data_dir = None
- _plan: plesty.lib.experiment.schedule.Plan | None = None
- _started: dict[str, Any] | None = None
- sources: list[Any] = []
- property id: str
The run id (the directory name).
- Return type:
str
- property name: str | None
The experiment name the run id carries, if it parses as one.
- Return type:
Optional[str]
- property started: datetime.datetime | None
The start time the run id carries, if it parses as one.
- Return type:
Optional[datetime.datetime]
- property plan: plesty.lib.experiment.schedule.Plan
The frozen plan (loaded once).
- Return type:
- property config: dict[str, Any]
The frozen configuration;
{}when the plan is unreadable.- Return type:
dict[str, Any]
- property journal: plesty.lib.experiment.journal.Journal
The run’s journal.
- Return type:
- status() dict[str, Any]
The run status by journal replay (see
Journal.status()).- Return type:
dict[str, Any]
- records() Iterable[dict[str, Any]]
Iterate the committed step records, oldest first.
- Return type:
Iterable[dict[str, Any]]
- _run_started() dict[str, Any]
- Return type:
dict[str, Any]
- property data_dir: str | None
The shared root as the acquiring host sees it.
Journaled by the run; falls back to the plan’s top-level
data_dirfor runs written before the journal carried it, then to thedata_dirgiven at construction.- Return type:
Optional[str]
- property raw_dir: str | None
Where the run’s device-written blobs went, in the host’s spelling.
- Return type:
Optional[str]
- property data_mount: str | None
The shared root as this machine sees it (see the class docstring).
- Return type:
Optional[str]
- local(host_path: str) pathlib.Path
Return host_path (as a record spells it) as a path on this machine.
Untranslated when the run has no data root or this machine has no mount for it — the case of a viewer on the acquiring host itself.
- Parameters:
host_path (str)
- Return type:
pathlib.Path
- source(mapper: plesty.lib.monitor.sources.FrameMapper | None = None) plesty.lib.monitor.sources.RunSource | plesty.lib.monitor.sources.ReplaySource
Return a source over this run’s records — live or replayed.
A
RunSourcewhenreplay_rowsis 0, else aReplaySourcehanding out that many records per poll. Every source made here is kept insources.Make one source per consumer: a source hands each record out once, so two views polling the same source would each see half the rows. The mapper is what they share.
- Parameters:
mapper (Optional[plesty.lib.monitor.sources.FrameMapper])
- Return type:
Union[plesty.lib.monitor.sources.RunSource, plesty.lib.monitor.sources.ReplaySource]
- property exhausted: bool
Whether every source made here has handed out its last record.
Live sources never are (the next record may still be coming); a run without sources counts as exhausted so a watcher does not wait forever.
- Return type:
bool
- __repr__() str
Return
Run('<path>').- Return type:
str
- plesty.lib.experiment.latest_run(run_root: str | pathlib.Path, name: str | Sequence[str] | None = None) pathlib.Path
Return the most recently started run under run_root.
- Raises:
FileNotFoundError – If there is no such run yet — a viewer opened before its experiment is a mistake worth naming.
- Parameters:
run_root (str | pathlib.Path)
name (Union[str, Sequence[str], None])
- Return type:
pathlib.Path
- plesty.lib.experiment.list_runs(run_root: str | pathlib.Path, name: str | Sequence[str] | None = None) list[pathlib.Path]
Return the run directories under run_root, oldest first.
- Parameters:
run_root (str | pathlib.Path) – The directory runs are created in.
name (Union[str, Sequence[str], None]) – Keep only runs of this experiment name (or any of several — an experiment that was renamed still owns its old runs).
Nonekeeps every run.
- Return type:
list[pathlib.Path]
- plesty.lib.experiment.parse_run_id(text: str) tuple[str, datetime.datetime] | None
Split a run id into its experiment name and start time.
- Parameters:
text (str) – A directory name.
- Returns:
(name, started), orNonewhen text is not a run id — the name may itself contain underscores, so the split is at the last one.- Return type:
Optional[tuple[str, datetime.datetime]]
- plesty.lib.experiment.run_id(name: str, when: datetime.datetime | None = None) str
Return the run id for experiment name started at when (now by default).
- Parameters:
name (str)
when (Optional[datetime.datetime])
- Return type:
str
- class plesty.lib.experiment.Plan(steps: list[Step], config: dict[str, Any] | None = None)
A frozen, hashable schedule of atomic steps plus the run configuration.
Usage:
plan = Plan( steps=[Step(id=f"scan[{i}]", op="scan_point", params={"x": i}) for i in range(10)], config={"exposure_s": 0.1}, ) plan.save("runs/run_001/plan.json")
Initialize the plan with its steps and optional configuration.
- Parameters:
steps (list[Step]) – Ordered atomic steps; step ids must be unique.
config (dict[str, Any] | None) – Experiment configuration recorded alongside the schedule.
- Raises:
ValueError – If two steps share the same id.
- config: dict[str, Any]
- to_dict() dict[str, Any]
Return a JSON-serializable representation of the plan.
- Return type:
dict[str, Any]
- classmethod from_dict(data: dict[str, Any]) Plan
Reconstruct a plan from
to_dict()output.- Parameters:
data (dict[str, Any])
- Return type:
- content_hash() str
Return a stable SHA-256 hash of the schedule and configuration.
Used on resume to verify that the persisted plan matches the plan the experiment would generate now — resuming under changed parameters is refused rather than silently mixing two schedules.
- Return type:
str
- save(path: str | pathlib.Path) pathlib.Path
Write the plan as JSON to path, creating parent directories.
- Parameters:
path (str | pathlib.Path)
- Return type:
pathlib.Path
- classmethod load(path: str | pathlib.Path) Plan
Load a plan previously written by
save().- Parameters:
path (str | pathlib.Path)
- Return type:
- __len__() int
Return the number of steps in the plan.
- Return type:
int
- class plesty.lib.experiment.Step
One atomic measurement step in an experiment plan.
- Variables:
id – Stable, deterministic identifier unique within the plan, e.g.
"scan[x=3,y=5]". Resume matches completed steps by this id, so it must not depend on run time or randomness.op – Name of the experiment method to call for this step.
params – Keyword arguments passed to the method.
- id: str
- op: str
- params: dict[str, Any]
- to_dict() dict[str, Any]
Return a JSON-serializable representation of the step.
- Return type:
dict[str, Any]