# Writing an Experiment An experiment is a class based on `Experiment` (from `plesty.lib.experiment`). You write two kinds of method: `build_plan()`, which lists the steps, and one method per kind of step, which does the work. [The parts](index.md#the-parts) introduces the words used here. ## The plan and the step methods ```python 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: # drive the devices through self.devices, and return the measurement return PlestyArray([float(x)], name="signal", unit="a.u.") run_id = asyncio.run(LineScan(run_root="runs").run()) ``` - `build_plan()` returns a `Plan`: the steps in order, and `config`, the settings the plan was built from. Both are saved with the run. - Each `Step` names the method to call (`op`) and the keyword arguments to call it with (`params`). - A step method can be an ordinary function or an `async` one; the framework awaits it either way. What it returns is saved as the step's record. - `run()` runs the plan and returns the run's id. It is `async`, so start it with `asyncio.run(...)`, or use [The Command Line](command_line.md), which does that for you. Three rules keep a run safe to continue after it stops: - **`build_plan()` returns the same plan for the same settings.** When a run continues, the plan is built again and compared, by fingerprint, with the one saved; a different plan is refused (`PlanMismatchError`). - **A step's id comes from its values**, such as `scan[x=3,y=5]`, never from the time or a random number, because finished steps are recognised by their id. - **Every `op` names a public method of the class**, and not one of the methods the framework itself calls (`build_plan`, `run`, `setup`, `teardown`). All steps are checked before the first one runs; a wrong one raises `InvalidOperationError` listing every offending step, before any device is touched. ## Devices The experiment reaches its devices through `self.devices`, a [`CompositeDevice`](../device/composite_device.md) passed to the constructor: ```python experiment = LineScan(devices=CompositeDevice(["stage", "camera"]), run_root="runs") ``` Inside a step method, `self.devices.call("stage", "write", "position", 3.0)` calls a method of the device named `stage`; for a device on another computer, a call that times out is retried after reconnecting. ## Setting up and tearing down Two more methods run around the steps: - `setup()` runs before the first step. By default it connects every device. - `teardown()` runs after the last step. By default it disconnects every device. It always runs, whether the run finished, failed or was stopped, so the devices are released however it ends. Both are `async`. To add your own work, override them and call the default: ```python async def setup(self) -> None: await super().setup() self.devices.call("stage", "home") ``` ## When a step fails A step that raises an exception is tried again, `max_retries` attempts in all (3 by default), waiting `retry_sleep` seconds (3 by default) in between. If every attempt fails, the run stops and is marked as aborted in the journal. Both are arguments of the constructor: ```python LineScan(devices=devices, run_root="runs", max_retries=5, retry_sleep=10.0) ``` ## Reporting progress within a step A step that takes minutes, such as a long exposure or a slow magnet ramp, looks the same from outside as one that hangs. Call `self.progress()` inside it to say how far it has got: ```python def acquire(self, frames: int) -> dict: for i in range(frames): self.devices.call("camera", "expose") self.progress(i + 1, frames, "exposing") return {"frames": frames} ``` This writes `progress.json` in the run folder, where a viewer or a console can show it. It holds only the present moment and is not part of the run's results. ## Other constructor arguments | Argument | Default | Meaning | |---|---|---| | `devices` | `None` | the `CompositeDevice` the steps use | | `run_root` | the shared disk, or `runs` without one | the folder that runs are saved in; a relative path is taken inside the shared disk when there is one | | `name` | the class name, in lower case | the name that starts each run's id | | `max_retries`, `retry_sleep` | `3`, `3.0` | see [When a step fails](#when-a-step-fails) | | `data_root` | from `PLESTY_DATA` | the shared disk; see [Data on a Shared Disk](shared_disk.md) |