# Architecture The module has two load-bearing layers, an analysis and a viewer, each in its own file: ``` plesty/pow_pl/ ├── rig.py # PowPlRig — the instruments as one composite device ├── experiment.py # Experiment — the measurement schedule and its records ├── analysis.py # power dependence after the sweep, and its report ├── __main__.py # CLI and manifest: schema, environment, viewer ├── config.yaml # packaged constants: sub-devices, operation budgets └── viz.py # the run viewer: read_spectrum and four views on plesty.lib.monitor.Viz ``` The bench reads all it needs from the module and the run directory: the manifest (`python -m plesty.pow_pl --manifest`) for the settings form and the viewer, `setup.yaml` for which bench serves which device, the run's journal for its state, and `analysis/.report.json` for its report. ## The rig (`PowPlRig`) A `plesty.lib` `CompositeDevice` wiring the remote device servers into one instrument: - `hwp` — Thorlabs K10CR1 rotation stage carrying the half-wave plate (`plesty-k10cr1`), - `spec` — Princeton Instruments LightField spectrometer (`plesty-lightfield-spectrometer`), - `pm` — Thorlabs powermeter (`plesty-thorlabs-powermeter`), **optional**: a rig built with `with_powermeter=False` connects only the stage and the spectrometer. Addresses are never arguments — each sub-device reads `_ADDRESS` from the environment, which the bench serving it provides (`setup.yaml`). The composite runs with thread affinity (one thread per sub-device; ZMQ sockets belong to the thread that opened them) and every call rides the lib's resilient ladder: timeout → reconnect → retry. The rig owns the experiment's whole device vocabulary; the experiment layer never issues raw device commands: - `home_hwp()` / `set_hwp(angle)` — homing and absolute moves, with the timeout scaled to the physical travel (a 2° sweep step gets a 5 s window, a park from 360° the full mechanical budget). The stage answers only once it has settled (plesty-k10cr1#2); `set_hwp` verifies the reported angle against the target, re-issues the move once on a mismatch and raises on a second one, - `configure(exposure_s, frames, pm_wavelength_nm=None, ...)` — pins free-run triggering and a normal shutter, pushes the acquisition settings; the powermeter is configured only when a wavelength is given, - `acquire_row(exposure_s, frames)` — the atomic acquisition: the exposure and (when fitted) the power reading are launched together on two threads so the power sample covers the exposure window; returns `{"data_file", "power_w", "pm_averaging"}` or `{"data_file"}` alone, - `center_wavelength_nm()` — the grating center, read once per process. Operation budgets (move/home timeouts, acquire grace) are declared in the packaged `config.yaml` — properties of the hardware, not of a measurement. Frame data never crosses the TCP transport: the spectrometer host saves each frame to the shared disk and reports only the file path. ## The experiment (`Experiment`) Built on `plesty.lib.experiment`: the configuration is expanded once into a frozen `Plan` — one `rig[identity]` step recording the instrument identities, then one `measure_row` step per QD per HWP angle (`qd[QD-01]-hwp[004.00]`). The plan is content-hashed and stored with the run, so a resumed run refuses a changed configuration and rebuilds identically. Each `measure_row` step: rotate to the absolute angle (no drift accumulates over the series), acquire the row, and return a self-contained JSON record — identifiers, chip coordinates, commanded and reported angle, the frame's shared-disk path, timestamps, grating center, and (when measured) the power. The framework journals every record the moment the step completes: ``` /pow-pl// # pow_pl_ ├── plan.json # frozen schedule + config + content hash ├── journal.jsonl # run/step events; run_started names the share ├── records.jsonl # one line per completed row ``` `setup()` routes the spectrometer's frames with `self.route(rig, ["spec"])`: `raw_dir` is `pow-pl//raw`, and the spectrometer writes into `pow-pl//raw/spec` under its own bench's folder on the Plesty share, creates it, and answers with that folder from the share — e.g. `platform-a/pow-pl//raw/spec` (plesty-lib#59). It then homes the wave plate; `teardown()` parks it at 0°. Each row names its frame `__hwpdeg_expms_` before exposing, and its record keeps the frame as `/` (`raw_file`), the same path on every machine once joined to its `PLESTY_DATA`. The rig pins LightField's file settings once per run — no online export, no `-raw` backup copy, nothing attached to the name — so the experiment last loaded in LightField never decides how a run saves. An interrupted sweep resumes at the exact angle it stopped (`--resume `).