plesty.pow_pl.experiment ======================== .. py:module:: plesty.pow_pl.experiment .. autoapi-nested-parse:: Power-dependent photoluminescence of quantum dots. The atomic step of the experiment is one excitation-power setting: rotate the half-wave plate (K10CR1 rotation stage) to the next angle of the series (2° increments by default) — the plate angle sets the excitation power — then acquire one spectrometer row and one powermeter reading **simultaneously**. The power sample covers the exposure window, so every row of the power series carries the excitation power it was actually measured at. The power reference is optional: a configuration without a ``powermeter`` block plans spectra-only rows — rotate, expose, record the frame path — and the rig is built without the powermeter (``with_powermeter=False``), so no ``PM_ADDRESS`` is needed and no powermeter server has to run. The product is the power series itself. Quantities derived from it are an analyzer's job, not this module's. All data lands on the shared disk, and frames never cross the network. A run is ``/pow-pl/``; at setup it routes the spectrometer once to the run's ``raw/spec`` folder, a path relative to the shared point that the spectrometer's bench resolves under its own view of the disk. Each row names its frame ``__hwpdeg_expms_``, no spaces, and its record keeps the frame as ``pow-pl//raw/spec/`` — the same path on every machine once joined to its ``PLESTY_DATA``. The framework commits each row's record (one line of ``records.jsonl``) the moment the step completes, and an interrupted sweep resumes at the exact angle it stopped (``run(resume=)``). Positioning is not yet automated: the steps assume the target dot is already localized (stage on target, signal on the spectrometer). The chip coordinates from the catalogue are recorded in every result for the upcoming localization stage (AMC300 positioner, DMK camera, PI steering mirror). Attributes ---------- .. autoapisummary:: plesty.pow_pl.experiment.RUN_ROOT plesty.pow_pl.experiment.SPEC plesty.pow_pl.experiment._DEFAULT_QDS plesty.pow_pl.experiment._UNSAFE Classes ------- .. autoapisummary:: plesty.pow_pl.experiment.Experiment Functions --------- .. autoapisummary:: plesty.pow_pl.experiment.frame_stem plesty.pow_pl.experiment._exposures plesty.pow_pl.experiment._angle_series Module Contents --------------- .. py:data:: RUN_ROOT :value: 'pow-pl' .. py:data:: SPEC :value: 'spec' .. py:data:: _DEFAULT_QDS .. py:data:: _UNSAFE .. py:function:: frame_stem(sample: str, qd: str, angle_deg: float, at: Optional[datetime.datetime] = None, exposure_s: Optional[float] = None) -> str Return the name a row's frame is saved under. Sample, dot, wave-plate angle and exposure — what tells one row's file from another's without its record — then the local time of the acquisition, which keeps a retaken row from meeting the name of its failed attempt (LightField would stop on an "already exists" dialog). Parts are joined with ``_``; any character a file name should not carry becomes ``-``. :param sample: Sample identifier. :param qd: Quantum-dot identifier. :param angle_deg: Commanded wave-plate angle in degrees. :param at: Time of the acquisition; now when ``None``. :param exposure_s: Exposure in seconds, named in whole milliseconds; left out of the name when ``None``. :returns: For example ``D104-C_QD01_hwp004.00deg_exp500ms_143512``. .. py:function:: _exposures(value: Any) -> list[float] Return the configured exposure(s) in seconds as a non-empty list. :param value: ``spectrum.exposure_s`` — one number, or a list of numbers. :returns: The exposures in the order given. :raises ValueError: If the list is empty or an exposure is not positive. .. py:function:: _angle_series(start_deg: float, stop_deg: float, step_deg: float) -> list[float] Return the inclusive HWP angle series without float accumulation drift. :param start_deg: First angle in degrees. :param stop_deg: Last angle in degrees (included when reachable by whole steps). :param step_deg: Angle increment in degrees; must be positive. :returns: The list of commanded angles in degrees. :raises ValueError: If ``step_deg`` is not positive. .. py:class:: Experiment(devices: Optional[plesty.pow_pl.rig.PowPlRig] = None, config: Optional[dict[str, Any]] = None, run_root: str | pathlib.Path | None = None, **kwargs: Any) Bases: :py:obj:`plesty.lib.experiment.Experiment` Measure the power-dependent spectral series of every catalogued QD. Configuration comes from a plain dict (loaded from ``config/*.yaml`` by ``__main__``) and is frozen into the plan, so a resumed run refuses a changed configuration. Instruments are accessed exclusively through the :class:`~.rig.PowPlRig` composite passed at construction. Initialize the experiment. :param devices: The instrument rig (HWP stage, spectrometer, powermeter); may be ``None`` for plan-only inspection. :param config: Measurement configuration (see ``config/default.yaml``); recorded in the plan. :param run_root: Directory under which each run creates its data directory — :data:`RUN_ROOT` under ``PLESTY_DATA`` for a run started by the command line. :param \*\*kwargs: Forwarded to the base ``Experiment`` (``name``, ``max_retries``, ``retry_sleep``, ``data_root``). .. py:attribute:: rig :value: None .. py:attribute:: config .. py:attribute:: _announced_qd :type: Optional[str] :value: None .. py:attribute:: _configured :type: Optional[tuple] :value: None .. py:attribute:: _grating_center_nm :type: Optional[float] :value: None .. py:attribute:: _rows_done :value: 0 .. py:attribute:: _rows_total :value: 0 .. py:method:: build_plan() -> plesty.lib.experiment.Plan Expand the configuration into one atomic step per QD per HWP angle. Each step commands one absolute angle (absolute targets, so no drift accumulates over the series) and one simultaneous spectrometer + powermeter acquisition; resume granularity is the single angle. With a list of exposures, every angle gets one step per exposure, in the order listed, before the plate moves on. :returns: The frozen measurement schedule; step ids look like ``qd[QD-01]-hwp[004.00]``, or ``qd[QD-01]-hwp[004.00]-exp[0.500s]`` with several exposures. :raises KeyError: If a catalogue entry has no ``id``. :raises ValueError: If the HWP sweep configuration is invalid. .. py:method:: record_rig() -> dict[str, Any] Record the instrument identities for replayability. :returns: Each device's identity string (model, serial, firmware) and the UTC time it was recorded. :raises RuntimeError: If the experiment was constructed without a rig. .. py:method:: measure_row(sample: str, qd: str, x_um: float, y_um: float, angle_deg: float, exposure_s: float, frames: int, pm_wavelength_nm: Optional[float] = None, pm_averaging: Optional[int] = None, pm_delay_s: float = 0.4, settle_s: float = 0.0) -> dict[str, Any] Rotate the HWP to one angle, then acquire the row. With powermeter parameters (a plan from a config with a ``powermeter`` block) the spectrum and the power are acquired together; without them the row is the spectrum alone. Settings are pushed to the instruments once per process — on the first row, or whenever a step carries different values — so a resumed run configures itself without rewriting identical settings on every row. :param sample: Sample identifier, recorded in the result. :param qd: Quantum-dot identifier from the catalogue. :param x_um: Catalogued chip-frame X coordinate in micrometres. :param y_um: Catalogued chip-frame Y coordinate in micrometres. :param angle_deg: Commanded absolute HWP angle in degrees. :param exposure_s: Spectrometer exposure in seconds. :param frames: Frames stored per acquisition (row is frame 0). :param pm_wavelength_nm: Powermeter correction wavelength in nanometres (written once per configure); ``None`` measures the spectrum without a power reference. :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 window. :param settle_s: Pause after the completed step — breathing room for the instrument stacks between rows (defaults to 0 for plans frozen before the knob existed). :returns: identifiers, coordinates, commanded and reported HWP angle, the shared-disk path of the saved frame, the grating center wavelength, and — when measured — the simultaneous optical power. :rtype: A JSON-serializable record :raises RuntimeError: If the experiment was constructed without a rig, or if the plan carries power parameters but the rig has no powermeter. .. py:method:: analyze_qd(qd: str) -> dict[str, Any] Analyse one dot's power series and save the results beside the run. Reads the dot's spectra back from this run's records, then the two analyses of :mod:`.analysis`: the integrated counts and every line's fitted area against the power. The line fit is saved as ``analysis/_line_fit`` and the report as ``analysis/.report.json``, which the bench's run report shows without importing this module. :param qd: Quantum-dot identifier. :returns: Each line's position and exponent, the integrated exponent, the saturation power when the data constrain it, the line fit's warnings, and where the results were saved. :raises RuntimeError: If the run has not started. .. py:method:: _grating_center() -> Optional[float] Return the grating center wavelength, queried once per process. The spectrometer's LightField IPC link is the fragile part of the rig — static settings are read (and written) once, not per row. A value the spectrometer cannot report (empty or non-numeric) is recorded as ``None`` with one warning; metadata must not abort a sweep whose frames and power are fine. .. py:method:: _ensure_at_qd(qd: str, x_um: float, y_um: float) -> None Positioning placeholder — the dot is assumed to be localized. Logs once per dot, not per angle. The upcoming localization stage will move the AMC300 to the catalogue coordinates, verify via the DMK camera, and fine-steer the PI mirror before the sweep starts. .. py:method:: setup() -> None :async: Connect the instruments, route the frames into ``raw/spec``, home the wave plate. The frame directory is set through the device's own data path rather than by writing a vendor parameter: the spectrometer resolves it under its bench's folder on the Plesty share, creates it, and answers with that folder from the share, where each frame is recorded (:meth:`route`). Only the spectrometer is routed — the stage and the powermeter write nothing. :raises RuntimeError: If this machine is not told where the Plesty share is, so it could not read the frames back. .. py:method:: teardown() -> None :async: Park the wave plate at 0 deg (best effort), then disconnect.