plesty.pow_pl.rig ================= .. py:module:: plesty.pow_pl.rig .. autoapi-nested-parse:: Instrument composite (rig) for the power-dependent PL experiment. Wires the three remote device servers of the power-dependent measurement into one :class:`~plesty.lib.device.composite_device.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 :meth:`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 ---------- .. autoapisummary:: plesty.pow_pl.rig._logger plesty.pow_pl.rig.DEVICES plesty.pow_pl.rig._HWP plesty.pow_pl.rig._SPEC plesty.pow_pl.rig._PM Classes ------- .. autoapisummary:: plesty.pow_pl.rig.PowPlRig Functions --------- .. autoapisummary:: plesty.pow_pl.rig._angular_error_deg Module Contents --------------- .. py:data:: _logger .. py:data:: DEVICES :type: dict[str, Any] .. py:data:: _HWP .. py:data:: _SPEC .. py:data:: _PM .. py:function:: _angular_error_deg(reported_deg: float, target_deg: float) -> float Return the signed shortest angular distance from *target* to *reported*. .. py:class:: PowPlRig(devices: Optional[collections.abc.Mapping[str, Any]] = None, env: Optional[plesty.lib.utils.settings.EnvSettings] = None, env_prefix: Optional[str] = None, with_powermeter: bool = True) Bases: :py:obj:`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 ``_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. :param devices: Sub-device overrides in the form :class:`CompositeDevice` accepts; defaults to the ``devices`` block of the packaged ``config.yaml``. :param env: Environment source for the address lookups. :param env_prefix: Deployment prefix for the derived variable names. :param with_powermeter: ``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. :raises ValueError: If a sub-device's address is neither configured nor present in the environment. .. py:attribute:: has_powermeter .. py:attribute:: _last_hwp_deg :type: Optional[float] :value: None .. py:attribute:: _pm_averaging .. py:attribute:: _pm_delay_s .. py:method:: home_hwp() -> bool Home the rotation stage and return its homed status. .. py:method:: set_hwp(angle_deg: float) -> float Move the HWP to an absolute angle and return the settled position. :param angle_deg: Target angle in degrees (user frame, applied mod 360). 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. .. py:method:: configure(exposure_s: float, frames: int, pm_wavelength_nm: Optional[float] = 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. :param exposure_s: Spectrometer exposure time in seconds. :param frames: Frames stored per acquisition. :param pm_wavelength_nm: Powermeter correction wavelength in nanometres; ``None`` skips the powermeter configuration. :param pm_averaging: Powermeter samples per reading (~3 ms each). :param pm_delay_s: Delay before the power sample so it lands inside the exposure (LightField opens the shutter only after ~0.3-0.5 s of setup). .. py:method:: 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. :param exposure_s: Exposure time in seconds. .. py:method:: center_wavelength_nm() -> float Return the grating center wavelength (nm) currently set on the spectrometer. .. py:method:: acquire_row(exposure_s: float, frames: int, file_stem: Optional[str] = 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. :param exposure_s: Exposure time in seconds (used to size the timeout). :param frames: Frames stored by the acquisition. :param file_stem: 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.