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/<qd>.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 <NAME>_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:

<PLESTY_DATA>/pow-pl/<run_id>/  # pow_pl_<YYYYmmdd-HHMMSS>
├── 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/<run_id>/raw, and the spectrometer writes into pow-pl/<run_id>/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/<run_id>/raw/spec (plesty-lib#59). It then homes the wave plate; teardown() parks it at 0°. Each row names its frame <sample>_<qd>_hwp<angle>deg_exp<ms>ms_<HHMMSS> before exposing, and its record keeps the frame as <that folder>/<file> (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 <run_id>).