experiment.py holds one class based on plesty-lib's Experiment. It does two
things: builds the plan — every step, decided before the first one runs —
and implements one step method per kind of step.
class Experiment(BaseExperiment):
def build_plan(self) -> Plan:
"""One row per plate angle, then the analysis."""
start, stop, step = (float(self.config[k]) for k in ("start_deg", "stop_deg", "step_deg"))
angles = [start + i * step for i in range(int(round((stop - start) / step)) + 1)]
steps = [Step(id=f"hwp[{a:06.2f}]", op="measure_row", params={"angle": a}) for a in angles]
steps.append(Step(id="analysis", op="analyze", params={}))
return Plan(steps, config=self.config)
def measure_row(self, angle: float) -> dict[str, float | str]:
"""Turn the plate to *angle*, take a spectrum, and read the power beside it."""
reached = self.devices.call("hwp", "move_absolute", angle)
self.devices.call("spec", "acquire")
reported = self.devices.call("spec", "get_recent_file")
power = self.devices.call("pm", "measure_power", averaging=int(self.config["averaging"]))
return {
"hwp_deg": float(reached),
"power_w": float(power["power"]),
"data_file": self.raw_file("spec", reported),
}
async def setup(self) -> None:
"""Connect and preflight the rig, home the stage, route the spectra."""
await super().setup() # connects the rig and runs its preflight
self.devices.call("hwp", "home_stage")
self.route(self.devices, ["spec"]) # the spectrometer saves into this run's folder
A Step names its id, the method it calls (op) and the values it calls it
with. Whatever the method returns is saved as that step's record — here,
one row: the angle reached, the power, and where the spectrum is. The last step
is the analysis (see Analyze). --dry-run prints the plan
without connecting anything:
$ uv run python -m plesty.demo_pow_pl --dry-run
Plan: 92 step(s), content hash 9695c42c55bf…
hwp[000.00]: measure_row({'angle': 0.0})
hwp[002.00]: measure_row({'angle': 2.0})
…
hwp[180.00]: measure_row({'angle': 180.0})
analysis: analyze({})
The spectrum stays on the disk
A spectrum is a file, and it never travels over the network: the spectrometer
saves it and reports only its path. self.route(...) in setup() sends the
spectrometer to this run's raw/spec folder — a path relative to its data
folder, never an absolute one — and keeps the folder it answered. Then
self.raw_file("spec", reported) turns the path it reports into one relative to
the shared disk — the same for every PC that reads the run. A device that was
not routed has no folder to record, and raw_file raises. A row then reads:
{"hwp_deg": 2.000002441894532, "power_w": 4.60247e-06,
"data_file": "runs/experiment_20260925-181144/raw/spec/mock_20260925-181144-371322.spe"}
See Large files never cross the network twice.
Three rules keep a run resumable
A step is atomic: it finishes and its record is saved, or it counts as not done — and a stopped run continues by skipping the steps already saved (see Data Flow). That only works if:
build_plan()is deterministic — the same settings give the same plan. The plan is hashed; continuing a run whose plan no longer matches is refused.- Step ids come from the step's values (
hwp[030.00]), never from the time or a random number, because finished steps are recognised by id. opnames a method of the class. Every step is checked before the first runs, so a typo stops the run before any instrument moves.
Writing a step
- Set the state it needs; do not assume it. A step may run hours after the
one before it, after a resume.
measure_rowmoves the plate itself. - Keep steps independent. Anything a later step needs comes from the plan
or from saved records, never from memory — which is why
analyzereads the rows back instead of collecting them as they pass. - Return typed values — a dict of numbers and paths, a
PlestyArraywith name and unit — so the records, the analysis and the viewer can read them.
A step that raises is retried — max_retries times (3 by default), waiting
retry_sleep seconds between attempts — before the run stops as aborted,
ready to continue once the cause is fixed.
Next: Run & Resume.