plesty.fss.monitor ================== .. py:module:: plesty.fss.monitor .. autoapi-nested-parse:: The fine-structure views, for the viewer of any experiment that runs a sweep. Two views follow the emission lines of a polarization sweep, each line with the same number and colour in both — ``line 0`` is the shortest wavelength: * :class:`LineSpectrumView` — the newest spectrum, with a marker on every line. * :class:`FineStructureView` — every line's position as a photon-energy shift in µeV from its mean position, against the rotator angle. Both fill in two stages: 1. **During the sweep** — a :class:`~plesty.fss.tracking.LineTracker` finds the lines the way the analysis does, in the spectra measured so far, and fits each spectrum as it arrives. Early in a sweep a line can still appear or disappear, and the numbering follows. 2. **After the sweep** — an experiment that runs :class:`plesty.fss.FineStructure` and saves its ``fine_structure`` and ``fit`` outputs with ``save_result`` records where it saved them. That record replaces the running estimate with the analysis: the fits of the whole sweep, each line's fitted sine, and its splitting and polarization axis written in the line's colour — or that the line has no valid splitting. An experiment adds them to its :class:`~plesty.lib.monitor.Viz` by name. It says which of *its* record fields hold the sweep; nothing else is configured: .. code-block:: python from plesty.lib.monitor import Viz from plesty.fss.monitor import FineStructureView, LineSpectrumView, LineTracking viz = Viz("Polarization PL", experiment="pol_pl", reads=read_spectrum) lines = LineTracking(angle="hwp_deg", label="qd") viz.add_plot(LineSpectrumView(lines, weight=3.0)) viz.add_plot(FineStructureView(lines, angle_label="HWP angle (deg)", weight=3.0)) One :class:`LineTracking` is shared by both views on purpose: it fits each spectrum once and answers both from that one fit. These views draw lines, points and text, so they have no error bars and no residual panel; :func:`plesty.fss.plot.plot_fine_structure` draws both for reports. Attributes ---------- .. autoapisummary:: plesty.fss.monitor.LINE_COLORS Classes ------- .. autoapisummary:: plesty.fss.monitor.LineTracker plesty.fss.monitor.LineTracking plesty.fss.monitor.SweepSoFar plesty.fss.monitor.LineSpectrumView plesty.fss.monitor.FineStructureView Functions --------- .. autoapisummary:: plesty.fss.monitor.line_color plesty.fss.monitor.line_drawing plesty.fss.monitor.sweep_channels Module Contents --------------- .. py:class:: LineTracker(fit_options: Mapping[str, Any]) Find and fit the lines of a sweep while it is measured. Lines are found the way :class:`~plesty.lib.analyzer.SpectrumFit` finds them for the analysis: in the average of the spectra — so far — with the same ``prominence``, ``distance`` and ``average`` (use ``"median"``: in the mean of a few spectra a cosmic ray passes as a line). Given ``peaks``, those positions are the lines and nothing is searched. Each new spectrum is fitted with the analysis's other fit options, starting from the previous spectrum's positions. When the lines found change — a line appears or disappears — the positions fitted so far are kept for the lines that remain, so the numbering by wavelength can shift but no spectrum is refitted. A line whose position moves by up to :data:`SAME_LINE_PIXELS` pixels is the same line. One tracker serves both views of a run: each spectrum is fitted once, and a view that reads a spectrum later than the other gets the same answer. A spectrum with a different label starts a new sweep. Validate the fit options. :param fit_options: :class:`~plesty.lib.analyzer.SpectrumFit` options, with either the line positions in ``peaks`` or a ``prominence`` to find them. :raises ValueError: If neither ``peaks`` nor ``prominence`` is given, or an option is invalid. .. py:attribute:: fixed :type: Optional[numpy.ndarray] :value: None .. py:attribute:: prominence .. py:attribute:: distance .. py:attribute:: average :value: '' .. py:attribute:: fit_options .. py:attribute:: fits :value: 0 .. py:attribute:: _seen :type: dict[str, SweepSoFar] .. py:attribute:: _label :type: Optional[str] :value: None .. py:attribute:: _wavelength :type: Optional[numpy.ndarray] :value: None .. py:attribute:: _spectra :type: list[numpy.ndarray] :value: [] .. py:attribute:: _angles :type: list[float] :value: [] .. py:attribute:: _rows :type: list[numpy.ndarray] :value: [] .. py:attribute:: _positions :type: numpy.ndarray .. py:method:: observe(channels: Mapping[str, Any]) -> SweepSoFar Add one spectrum and return the sweep so far. :param channels: One spectrum's ``wavelength`` in nm and ``counts``, with ``row`` (the rotator angle in degrees) and ``label`` when known. :returns: The lines and their positions after this spectrum. .. py:method:: _reset(label: Optional[str]) -> None Start a new sweep. .. py:method:: _update_lines() -> None Find the lines in the spectra so far; keep the columns of the lines that remain. .. py:method:: _fit(counts: numpy.ndarray) -> numpy.ndarray Fit the lines in one spectrum, from the previous spectrum's positions. .. py:class:: LineTracking(options: Optional[Mapping[str, Any]] = None, *, at: Sequence[str] = ('analysis', 'fit_options'), counts: str = 'counts', wavelength: str = 'wavelength', angle: str = '', label: str = '', fit_key: str = 'line_fit_document', budget: int = 4) Follows a run's emission lines while the sweep is being measured. Hand the same instance to both views. A tracker fits each spectrum once and answers from its own record afterwards, so two views cost one fit per spectrum rather than two. The fit options are the analysis's own, so the lines followed during the sweep are the lines reported after it. Without line positions (``peaks``) or a ``prominence`` to find them nothing is tracked: every bump of the noise would otherwise be a line, and the views simply show none. :ivar options: The fit options, or ``None`` to read them from each run's own configuration. Say where the sweep is in the records, and how its lines are found. :param options: :class:`~plesty.lib.analyzer.SpectrumFit` options, or ``None`` to take them from the run's frozen configuration. :param at: Keys leading to the fit options in that configuration, used only when *options* is ``None``. The default is where an experiment that configures a :class:`plesty.fss.FineStructure` under ``analysis`` keeps them. :param counts: Record field holding each spectrum's counts. :param wavelength: Record field holding its wavelength axis, in nm. :param angle: Record field holding the rotator angle in degrees; ``""`` when the run does not record one. :param label: Record field labelling the sweep — the quantum dot, say. A spectrum with a different label starts a new sweep. :param fit_key: Record field naming the analysis's saved line fit. Once a run has written one there is nothing left to track: the analysis fitted every spectrum of the sweep already, and its answer is the one the run reports. Reading it costs one file instead of re-fitting the whole sweep. :param budget: How many spectra one :meth:`sweep` may fit. Fitting happens inside a view's draw, on the thread that answers the window, so an unbounded call is a window that stops answering for as long as the backlog takes. The rest are fitted on the calls that follow. ``0`` removes the bound, for a caller that is not drawing — a script, or a test that wants the whole sweep in one go. .. py:attribute:: options :value: None .. py:attribute:: at :value: ('analysis', 'fit_options') .. py:attribute:: counts :value: 'counts' .. py:attribute:: wavelength :value: 'wavelength' .. py:attribute:: angle :value: '' .. py:attribute:: label :value: '' .. py:attribute:: fit_key :value: 'line_fit_document' .. py:attribute:: budget :value: 4 .. py:attribute:: _analysed :type: dict[str, Optional[SweepSoFar]] .. py:attribute:: _trackers :type: dict[str, Optional[LineTracker]] .. py:method:: tracker(data: Any) -> Optional[LineTracker] The tracker that finds this run's lines, built once per run. :param data: The run's data. :returns: The tracker, or ``None`` when this run gives it nothing to find lines by. .. py:method:: _configured(data: Any) -> dict[str, Any] Return the fit options the run's own configuration carries at :attr:`at`. .. py:method:: sweep(data: Any) -> Optional[SweepSoFar] The lines of the current sweep, as far as it has been measured. While the sweep runs, every spectrum committed so far is handed to the tracker in order; it fits each one once and answers from its own record afterwards, so calling this from both views costs one fit per spectrum. Once the analysis has written its line fit there is nothing left to track, and the tracker is not used at all. That matters for opening a *finished* run: re-fitting ninety spectra to draw a marker on one of them took seconds before the first frame appeared, and produced a worse answer than the analysis had already saved beside it. :param data: The run's data. :returns: The sweep so far, or ``None`` when nothing is tracked or no spectrum has been committed. .. py:method:: _analysis(data: Any) -> Optional[SweepSoFar] The sweep as the analysis fitted it, or ``None`` before it wrote one. Read once per run and kept: a view redrawn twice a second must not re-parse the document each tick. :param data: The run's data. :returns: The fitted positions of every line in every spectrum of the sweep. .. py:class:: SweepSoFar The lines of a sweep as a :class:`LineTracker` knows them after one spectrum. :ivar label: Label of the sweep, e.g. the quantum dot, or ``None``. :ivar positions: Position of each line in nm, shortest wavelength first. :ivar angles: Rotator angle of every spectrum so far, in degrees (``nan`` if unknown). :ivar centers: Fitted position of each line in each spectrum so far, (spectra, lines), ``nan`` where the fit failed or the line was not yet found. .. py:attribute:: label :type: Optional[str] .. py:attribute:: positions :type: numpy.ndarray .. py:attribute:: angles :type: numpy.ndarray .. py:attribute:: centers :type: numpy.ndarray .. py:property:: latest :type: numpy.ndarray Fitted position of each line in the newest spectrum. .. py:data:: LINE_COLORS :type: tuple[str, ...] :value: ('cyan', 'amber', 'violet', 'mint', 'rose', 'blue', 'text_bright', 'slate') .. py:function:: line_color(index: int) -> str The colour of the *index*-th line, counting from the shortest wavelength. :param index: Which line. :returns: A palette token; the palette repeats for a sweep with more lines than it has colours, which is better than refusing to draw them. .. py:class:: LineSpectrumView(tracking: plesty.fss.tracking.LineTracking, **kwargs: Any) Bases: :py:obj:`plesty.lib.monitor.common.Spectrum` The newest spectrum, with a marker on every line the fine structure follows. Each line keeps its colour and its number — ``line 0``, ``line 1``, … — from :class:`FineStructureView`, so a marker here and a point series there are the same line. A marker sits on the spectrum at the line's fitted position; a line whose fit failed in this spectrum has none. Every count is drawn as recorded. This is the library's :class:`~plesty.lib.monitor.common.Spectrum` with the markers added, so everything it takes — which fields hold the axis and the values, the axis labels, the colour of the curve — it takes here too. Mark the lines *tracking* follows on the spectrum. :param tracking: The run's line tracking, shared with the fine-structure view so each spectrum is fitted once. :param \*\*kwargs: Forwarded to :class:`~plesty.lib.monitor.common.Spectrum`. .. py:attribute:: title :value: 'Spectrum' .. py:attribute:: tracking .. py:method:: draw(data: Any) -> plesty.lib.monitor.Drawing Draw the newest spectrum, then mark each fitted line on it. :param data: The run's data. :returns: The curve, and one marker per line whose fit succeeded. .. py:class:: FineStructureView(tracking: Optional[plesty.fss.tracking.LineTracking] = None, *, fine_structure_key: str = 'fine_structure_document', fit_key: str = 'line_fit_document', angle_label: str = 'Rotator angle (deg)', title: str = '', name: str = '', weight: Optional[float] = None, area: str = '') Bases: :py:obj:`plesty.lib.monitor.Plot` Energy shift of every line against the rotator angle, with its fit and result. A splitting is an energy, and the wavelengths it separates differ in the fourth decimal: drawn in nanometres it is a flat line, drawn as a shift from each line's mean position in µeV it is the measurement. While the sweep runs the shift is measured from the mean *so far*, which moves as spectra arrive; once the analysis has written its result, its own fits, sines and splittings replace the running estimate. The text under the plot gives each line's result in the line's colour. A class rather than a function because it remembers which analysis document it has already read — loading it on every tick would re-parse the same file twice a second for the rest of the run. Draw the sweep *tracking* follows, and the analysis of it when there is one. :param tracking: The run's line tracking, shared with the spectrum view; ``None`` shows nothing until the analysis is written. :param fine_structure_key: Record field naming the saved ``fine_structure`` result, relative to the run directory. :param fit_key: Record field naming the saved ``fit`` result. :param angle_label: Label of the angle axis, including the unit. :param title: What the view is called. :param name: Layout key. :param weight: Relative size. :param area: Which side of the window it starts on. .. py:attribute:: title :value: 'Fine structure' .. py:attribute:: tracking :value: None .. py:attribute:: fine_structure_key :value: 'fine_structure_document' .. py:attribute:: fit_key :value: 'line_fit_document' .. py:attribute:: angle_label :value: 'Rotator angle (deg)' .. py:attribute:: _channels :type: Optional[dict[str, Any]] :value: None .. py:attribute:: _loaded_from :type: tuple[str, str] :value: ('', '') .. py:method:: analysis(data: Any) -> Optional[dict[str, Any]] What the analysis made of the sweep, or ``None`` before it has written it. :param data: The run's data. :returns: The channels of :func:`sweep_channels`, read once per document. .. py:method:: draw(data: Any) -> plesty.lib.monitor.Drawing Draw the analysis when there is one, else the sweep so far. :param data: The run's data. :returns: One point series per line, its fitted sine once analysed, and one line of result text per line. .. py:method:: _draw_sweep(data: Any) -> plesty.lib.monitor.Drawing Each line's shift from its mean so far, without fitted sines. .. py:method:: _draw_analysis(channels: dict[str, Any]) -> plesty.lib.monitor.Drawing The analysis's own fits, sines and results. .. py:function:: line_drawing(channels: dict[str, Any], index: int, angle_label: str, label: str = '') -> list[Any] One analysed line: its measured shifts as points, and its fitted sine. Shared by :class:`FineStructureView` and the analysis report (:mod:`plesty.fss.report`), so the figure in a report is the view on screen. :param channels: What :func:`sweep_channels` computed. :param index: Which line. :param angle_label: Label of the angle axis, including the unit. :param label: Legend label; ``line `` by default. :returns: A ``Scatter`` of the points and a ``Line`` of the sine, in the line's colour. .. py:function:: sweep_channels(fine_structure: plesty.fss.data.FineStructureData, fit: Any) -> dict[str, Any] Compute what the view draws from the analyzer's ``fine_structure`` and ``fit`` outputs. :param fine_structure: The analyzer's ``fine_structure`` output. :param fit: The analyzer's ``fit`` output (``SpectrumFitData``). :returns: ``angle_deg`` (points), ``shift_ueV`` (lines × points, ``nan`` where a frame's fit failed), ``curve_angle_deg``, ``curve_ueV`` (lines × curve points) and ``summary``: the result of each line on its own text line, in line order. :raises ValueError: If the fit carries no rotator angle or the two outputs describe different lines.