Plesty Documentation

Plan & Steps

The experiment class — a plan of atomic steps built from the settings, one row per plate angle, and one method per kind of step.

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.
  • op names 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 PlestyArray with 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.