Plesty Documentation

The Experiment Class

The async Experiment base class — lifecycle hooks, constructor, and a minimal runnable subclass.

plesty.lib.experiment.Experiment is an async abstract base class that turns a measurement into an atomic, journaled, resumable schedule. You describe what to measure; the base class owns how it executes — journaling, retries, checkpointing, and resume.

What a subclass implements

A subclass overrides exactly three lifecycle hooks, plus one public method per step operation:

Member Required Role
build_plan() yes Return the Plan: the atomic measurement schedule plus the frozen config. Must be deterministic — resume verifies the regenerated plan against the persisted one by content hash.
setup() no (async) Prepare the run. The default connects all devices and preflights them — each must answer identity() and expose the operations its requires names, or the run stops before the first step. Extend with await super().setup().
teardown() no (async) Release resources. The default disconnects all devices. Always awaited — whether the run completes, fails, or is canceled.
step operations one per Step.op Public methods (sync or async) executed per step; the return value is persisted. See Plan & step operations.

Step operations are validated before the run starts: every Step.op must name an existing public method on the experiment, and the lifecycle names (build_plan, run, setup, teardown) are reserved.

Constructor

Experiment(
    devices=None,       # CompositeDevice with all instruments; connected in setup()
    run_root=None,      # checkpoint dirs go here; None = PLESTY_DATA_MOUNT, else runs/
    name=None,          # used in run ids; defaults to the subclass name lowercased
    max_retries=3,      # attempts per step before the run aborts
    retry_sleep=3.0,    # seconds between step retries
    data_dir=None,      # shared data root as the acquiring host writes it
    data_mount=None,    # the same root as this machine reads it
)

data_dir/data_mount default to PLESTY_DATA_DIR/PLESTY_DATA_MOUNT. They are how a device that writes its own files — a camera, a spectrometer — is told where to put them: self.raw_dir is <data_dir>/<run_id>/raw, and setup() routes the writing devices to it with self.devices.set_data_path(self.raw_dir, devices=["cam"]). Blobs never travel back over the network; only their paths do.

Device access goes exclusively through the CompositeDevice passed at construction — an experiment receives device clients, never raw hardware. Plan construction is device-free by contract: build_plan() must work without any devices attached.

Running

run_id = asyncio.run(experiment.run())            # new run
asyncio.run(experiment.run(resume=run_id))        # continue an interrupted run

run() executes the plan step by step, journaling every event. On resume, steps journaled as completed are skipped; a run whose regenerated plan no longer matches the stored one is refused with PlanMismatchError. Failed steps are retried up to max_retries times before the run aborts. See Runs, data & resume for what lands on disk.

Minimal runnable subclass

No hardware required:

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))

The demo experiment is the full reference version of this pattern — a simulated Gaussian line scan with async step operations, reproducible pseudo-noise, and a CLI with --resume.