plesty.pow_pl.rig

Instrument composite (rig) for the power-dependent PL experiment.

Wires the three remote device servers of the power-dependent measurement into one CompositeDevice:

  • hwp — Thorlabs K10CR1 rotation stage carrying the half-wave plate that sets the excitation power (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, and acquire_row returns spectra without a power reading.

Everything the composite already does is used rather than repeated: the resilient call (timeout → reconnect → retry), one thread per sub-device so no ZMQ socket is touched from a foreign thread, and submit for the simultaneity PowPlRig.acquire_row() needs — the exposure and the power reading cover the same time window. What is left here is this rig’s own vocabulary: home the plate, move it, acquire a row.

The instruments and their operation budgets are declared in the packaged config.yaml; the server addresses come from the environment. Neither is written in this file.

Frame data never crosses the TCP transport: the spectrometer writes each frame to the data path the run routed it to, and reports only the file path.

Attributes

_logger

DEVICES

_HWP

_SPEC

_PM

Classes

PowPlRig

HWP rotation stage + spectrometer + powermeter as one instrument.

Functions

_angular_error_deg(→ float)

Return the signed shortest angular distance from target to reported.

Module Contents

plesty.pow_pl.rig._logger
plesty.pow_pl.rig.DEVICES: dict[str, Any]
plesty.pow_pl.rig._HWP
plesty.pow_pl.rig._SPEC
plesty.pow_pl.rig._PM
plesty.pow_pl.rig._angular_error_deg(reported_deg: float, target_deg: float) → float

Return the signed shortest angular distance from target to reported.

Parameters:
  • reported_deg (float)

  • target_deg (float)

Return type:

float

class plesty.pow_pl.rig.PowPlRig(devices: collections.abc.Mapping[str, Any] | None = None, env: plesty.lib.utils.settings.EnvSettings | None = None, env_prefix: str | None = None, with_powermeter: bool = True)

Bases: plesty.lib.device.composite_device.CompositeDevice

HWP rotation stage + spectrometer + powermeter as one instrument.

The rig owns the whole device vocabulary of the experiment; the experiment layer never issues raw device commands.

Connect the rig’s instruments, each on its own thread.

Addresses are not arguments: each is read from <NAME>_ADDRESS in the environment (HWP_ADDRESS, SPEC_ADDRESS, PM_ADDRESS), so a deployment is configured by its .env. Thread affinity is not optional here — this rig drives its instruments concurrently, and ZMQ sockets belong to the thread that opened them.

Parameters:
  • devices (Optional[collections.abc.Mapping[str, Any]]) – Sub-device overrides in the form CompositeDevice accepts; defaults to the devices block of the packaged config.yaml.

  • env (Optional[plesty.lib.utils.settings.EnvSettings]) – Environment source for the address lookups.

  • env_prefix (Optional[str]) – Deployment prefix for the derived variable names.

  • with_powermeter (bool) – False builds the rig without the pm sub-device — no PM_ADDRESS needed, and rows carry spectra only.

Raises:
  • RuntimeError – If a device server cannot be reached.

  • ValueError – If a sub-device’s address is neither configured nor present in the environment.

has_powermeter
_last_hwp_deg: float | None = None
_pm_averaging
_pm_delay_s
home_hwp() → bool

Home the rotation stage and return its homed status.

Return type:

bool

set_hwp(angle_deg: float) → float

Move the HWP to an absolute angle and return the settled position.

Parameters:

angle_deg (float) – Target angle in degrees (user frame, applied mod 360).

Return type:

float

Small steps from a known position (the 2 deg sweep moves) run under a fast 5 s window; larger or first moves keep the full mechanical budget — a park from 360 deg is a ~70 s physical rotation.

The stage answers only once it has physically stopped (plesty-k10cr1#2), so the reported angle is the angle the row is measured at. It is checked against the target: on a mismatch beyond position_tolerance_deg the move is issued once more, and a second mismatch raises so the step is retried instead of recording a row at the wrong angle.

Returns:

The stage-reported position in degrees after the move.

Raises:

RuntimeError – If the stage settles away from the target twice.

Parameters:

angle_deg (float)

Return type:

float

configure(exposure_s: float, frames: int, pm_wavelength_nm: float | None = None, pm_averaging: int = int(_PM['averaging']), pm_delay_s: float = float(_PM['delay_s'])) → None

Push the acquisition settings to the spectrometer and powermeter.

The powermeter wavelength is written once here; the averaging count is applied per reading via measure_power(averaging=...). On a rig without a powermeter (or with no wavelength given), only the spectrometer is configured.

Parameters:
  • exposure_s (float) – Spectrometer exposure time in seconds.

  • frames (int) – Frames stored per acquisition.

  • pm_wavelength_nm (Optional[float]) – Powermeter correction wavelength in nanometres; None skips the powermeter configuration.

  • pm_averaging (int) – Powermeter samples per reading (~3 ms each).

  • pm_delay_s (float) – Delay before the power sample so it lands inside the exposure (LightField opens the shutter only after ~0.3-0.5 s of setup).

Return type:

None

set_exposure(exposure_s: float) → None

Change only the spectrometer exposure, leaving every other setting.

LightField takes the exposure in whole milliseconds and rejects a fraction, so the value is rounded.

Parameters:

exposure_s (float) – Exposure time in seconds.

Return type:

None

center_wavelength_nm() → float

Return the grating center wavelength (nm) currently set on the spectrometer.

Return type:

float

acquire_row(exposure_s: float, frames: int, file_stem: str | None = None) → dict[str, Any]

Expose the spectrometer and read the optical power simultaneously.

The exposure and the power reading are launched together on two worker threads so the powermeter samples during the exposure window. On a rig without a powermeter only the exposure runs, and the row carries no power keys. The frame stays on the spectrometer host’s (shared) data disk; only its path is fetched afterwards.

Parameters:
  • exposure_s (float) – Exposure time in seconds (used to size the timeout).

  • frames (int) – Frames stored by the acquisition.

  • file_stem (Optional[str]) – Name the frame is saved under, unique per acquisition; None keeps whatever is set.

Returns:

{"data_file": str, "power_w": float, "pm_averaging": int} — the saved frame’s path on the shared disk, the optical power in watts, and the averaging count applied to that reading. Without a powermeter, {"data_file": str} only.

Raises:

RuntimeError – If the spectrometer reports no saved frame.

Return type:

dict[str, Any]