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 withwith_powermeter=Falseconnects 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_hwpverifies 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>).