Steps are the unit of atomicity
A Step is the smallest unit of work that either completes — its result is persisted and journaled — or is re-run on resume:
from plesty.lib.experiment import Plan, Step
Step(
id="scan[x=3,y=5]", # stable, deterministic, unique within the plan
op="measure_point", # name of the experiment method to call
params={"x": 3, "y": 5}, # keyword arguments passed to the method
)
Choose the step granularity by asking: if the run dies here, what is safe to redo? In plesty-pl-image-scan, one step is one marker cell — a partially captured cell is retaken as a whole so its images stay spatially aligned, while completed cells are never re-imaged.
Step ids must not depend on run time or randomness: resume matches completed steps by id.
The plan is frozen
build_plan() returns a Plan — the ordered steps plus the measurement config:
def build_plan(self) -> Plan:
steps = [
Step(id=f"scan[x={x}]", op="measure_point", params={"x": x})
for x in range(self.points)
]
return Plan(steps, config={"points": self.points, "noise": self.noise})
The plan is written to plan.json at run start and never mutated. Its content hash lets a resumed run verify it is continuing the same schedule — which is why build_plan() must be deterministic for a given configuration, and why the tuning config belongs inside the plan.
Writing step operations
A step operation is a public method — sync or async — that performs one measurement and returns the result:
async def measure_point(self, x: int) -> PlestyArray:
"""Measure one scan position."""
self.devices.stage.write("POSITION", x)
power = self.devices.powermeter.query("POWER")
return PlestyArray([float(power)], name="signal", unit="watt",
description=f"intensity at x={x}")
The return value is persisted automatically as raw blob + JSON metadata (see Runs, data & resume) — a PlestyArray, encoded bytes (e.g. an image), or any JSON-serializable value.
Design principles
Return structured, typed data. Prefer PlestyArray (carries name, unit, description) or a typed structure over raw strings — downstream analyzers and mypy both need known types. Gate 4 requires explicit return type annotations on all public methods.
Set the state you need — don't assume it. A step may execute right after a resume, hours after the previous step. Always write the device state the step depends on before reading.
Make settle times explicit parameters. Physical instruments settle after setpoint changes; hardcoded sleeps can't be adjusted per hardware.
Keep steps independent. A step should not rely on results held in memory from a previous step — after a crash, that memory is gone. Anything a later step needs must come from the plan parameters or from persisted data.