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_row moves 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 analyze reads 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.