plesty.lib.data.spectrum ======================== .. py:module:: plesty.lib.data.spectrum .. autoapi-nested-parse:: Spectra: the flagship :class:`~plesty.lib.data.ExperimentData` type. A :class:`SpectrumData` is a wavelength axis and the intensity measured on it — one spectrum, or a stack of frames taken on the same axis. It reads LightField ``.spe`` files and LightField's CSV export, writes and reads every native format, and carries the processing a PL analysis starts with. Fitting is analysis and lives in :class:`plesty.lib.analyzer.SpectrumFit`. Every processing method returns a **new** spectrum and appends what it did to ``metadata["processing"]``, so a saved result says how it was derived. .. code-block:: python from plesty.lib.data import SpectrumData spectrum = SpectrumData.load("qd1.spe") prepared = spectrum.crop(770, 780).subtract_background("median") prepared.find_peaks(prominence=100) The raw ``.spe`` content — every region, the calibration, the XML footer — is available through :meth:`SpeFile.read` when a spectrum is not the whole story. An SPE 3.0 file is a 4100-byte binary header, the frame data, and an XML footer. The footer is authoritative: it declares the pixel format, the frame and region layout, and the wavelength calibration, and the legacy binary header fields may be zero. Only the footer offset (bytes 678–685) and the file version (bytes 1992–1995) are read from the header. Older WinSpec (SPE 2.x) files carry no XML footer and are refused rather than guessed at. Attributes ---------- .. autoapisummary:: plesty.lib.data.spectrum.HC_EV_NM plesty.lib.data.spectrum.COORDINATE_PREFIX plesty.lib.data.spectrum.COORDINATE_UNITS plesty.lib.data.spectrum._WAVELENGTH_DESCRIPTION plesty.lib.data.spectrum._INTENSITY_DESCRIPTION Classes ------- .. autoapisummary:: plesty.lib.data.spectrum.SpeFile plesty.lib.data.spectrum.SpectrumData plesty.lib.data.spectrum.SpectrumFitData Functions --------- .. autoapisummary:: plesty.lib.data.spectrum._as_array plesty.lib.data.spectrum._coordinate_unit plesty.lib.data.spectrum._frame_coordinates plesty.lib.data.spectrum._split_coordinates plesty.lib.data.spectrum._is_frame_block_header plesty.lib.data.spectrum._parse_block_row plesty.lib.data.spectrum._plateau_falls plesty.lib.data.spectrum._prominence Module Contents --------------- .. py:data:: HC_EV_NM :value: 1239.841984 .. py:data:: COORDINATE_PREFIX :value: 'coordinate_' .. py:data:: COORDINATE_UNITS :type: dict[str, str] .. py:class:: SpeFile The content of one LightField SPE 3.0 file; :meth:`read` builds it. :ivar version: File format version from the header (``3.0``). :ivar regions: One array per region of interest, each shaped ``(frames, height, width)``. :ivar wavelength: Wavelength in nm of each pixel column of the first region, or ``None`` when the file has no calibration. :ivar metadata: Acquisition context read from the XML footer — only the keys found are present (``exposure_time_ms``, ``center_wavelength_nm``, ``grating``, ``sensor_temperature_c``, ``created``, ``software``, ``camera``, ``spectrometer``). :ivar xml: The raw XML footer, for anything :attr:`metadata` omits. .. py:attribute:: HEADER_SIZE :type: ClassVar[int] :value: 4100 Size of the binary header preceding the frame data. .. py:attribute:: XML_OFFSET_POS :type: ClassVar[int] :value: 678 Header position of the XML footer offset (uint64). .. py:attribute:: VERSION_POS :type: ClassVar[int] :value: 1992 Header position of the file version (float32). .. py:attribute:: PIXEL_FORMATS :type: ClassVar[dict[str, str]] Footer pixel formats this reader decodes, mapped to NumPy dtypes. .. py:attribute:: version :type: float .. py:attribute:: regions :type: tuple[numpy.ndarray, Ellipsis] .. py:attribute:: wavelength :type: numpy.ndarray | None .. py:attribute:: metadata :type: dict[str, Any] .. py:attribute:: xml :type: str :value: '' .. py:method:: read(path: str | pathlib.Path) -> Self :classmethod: Read a LightField SPE 3.0 file. :param path: The ``.spe`` file. :returns: The file's regions, wavelength calibration and metadata. :raises ValueError: If the file is not SPE 3.0, is truncated, or uses a pixel format this reader does not know. .. py:method:: _wavelength(root: xml.etree.ElementTree.Element, width: int) -> numpy.ndarray | None :classmethod: Return the per-pixel wavelength of a region *width* pixels wide. The calibration covers the sensor; a region cropped or binned on the sensor is mapped through ``SensorMapping`` (x offset, width, x binning). .. py:method:: _metadata(root: xml.etree.ElementTree.Element) -> dict[str, Any] :classmethod: Collect the acquisition settings a spectrum report needs from the footer. .. py:method:: _local(element: xml.etree.ElementTree.Element) -> str :staticmethod: Return an element's tag without its XML namespace. .. py:method:: _find(root: xml.etree.ElementTree.Element, *names: str) -> xml.etree.ElementTree.Element | None :classmethod: Find the first element reached by descending through *names*, at any depth. .. py:method:: _text(root: xml.etree.ElementTree.Element, *names: str) -> str | None :classmethod: Return the stripped text of the element at *names*, or ``None``. .. py:method:: _float(root: xml.etree.ElementTree.Element, *names: str) -> float | None :classmethod: Return the element at *names* parsed as a float, or ``None``. .. py:data:: _WAVELENGTH_DESCRIPTION :value: 'Wavelength of each detector pixel.' .. py:data:: _INTENSITY_DESCRIPTION :value: 'Detected intensity per pixel; one row per frame when stacked.' .. py:function:: _as_array(value: numpy.typing.ArrayLike, name: str, unit: str, description: str) -> plesty.lib.data.array.PlestyArray Coerce *value* to a :class:`PlestyArray`, filling metadata it lacks. .. py:class:: SpectrumData(wavelength: numpy.typing.ArrayLike, intensity: numpy.typing.ArrayLike, metadata: dict[str, Any] | None = None, provenance: dict[str, Any] | None = None, coordinates: Mapping[str, numpy.typing.ArrayLike] | None = None) Bases: :py:obj:`plesty.lib.data.experiment_data.ExperimentData` A wavelength axis and the intensity measured on it. :ivar wavelength: 1-D axis, one value per pixel (default unit ``nm``). :ivar intensity: ``(pixels,)`` for one spectrum or ``(frames, pixels)`` for a stack (default unit ``counts``). :ivar coordinates: Per-frame values of the sweep the frames were taken over, by name — e.g. ``hwp_deg`` or ``power_w`` — each 1-D with one value per frame. Empty for a spectrum that is no sweep. Validate and store the axis, the intensity and the frame coordinates. :param wavelength: 1-D axis; a plain array gets unit ``nm``. :param intensity: 1-D spectrum or 2-D ``(frames, pixels)`` stack; a plain array gets unit ``counts``. :param metadata: Acquisition context. :param provenance: Origin of the data. :param coordinates: Per-frame sweep values by name; a plain array gets the unit its name ends in (``_deg``, ``_w``, ``_nm``, …). :raises ValueError: If the shapes do not describe a spectrum on the axis, or a coordinate does not have one value per frame. .. py:attribute:: kind :value: 'spectrum' Registry key and schema dtype token, e.g. ``"spectrum"``. .. py:attribute:: schema_version :value: 1 Version of the array layout this class writes and reads. .. py:attribute:: wavelength :type: plesty.lib.data.array.PlestyArray .. py:attribute:: intensity :type: plesty.lib.data.array.PlestyArray .. py:attribute:: coordinates :type: dict[str, plesty.lib.data.array.PlestyArray] .. py:method:: to_arrays() -> dict[str, plesty.lib.data.array.PlestyArray] Return ``wavelength``, ``intensity`` and each ``coordinate_``. .. py:method:: from_arrays(arrays: dict[str, plesty.lib.data.array.PlestyArray], metadata: dict[str, Any], provenance: dict[str, Any]) -> Self :classmethod: Rebuild a spectrum from ``wavelength``, ``intensity`` and its coordinates. :param arrays: Must hold ``wavelength`` and ``intensity``; every ``coordinate_`` becomes a coordinate. :param metadata: The saved metadata. :param provenance: The saved provenance. .. py:method:: example() -> Self :classmethod: Return a 101-pixel spectrum with one Lorentzian line at 775 nm. .. py:method:: from_text(lines: list[str], delimiter: str | None) -> Self :classmethod: Build a spectrum from a text export, in blocks or as one table. A file in which a ``Frame …`` line opens each frame — followed by ``wavelength intensity`` rows, whitespace-separated, where a comma may be the decimal separator — is read block by block. Anything else is a table, see :meth:`from_table`. :param lines: The file's non-empty lines, ``#`` comments removed. :param delimiter: ``","`` for ``.csv``; ``None`` for ``.txt``. :raises ValueError: If the blocks hold no data or frames of different lengths. .. py:method:: from_table(columns: list[numpy.ndarray], names: list[str] | None) -> Self :classmethod: Build a spectrum from a plain table. Two layouts are understood: * LightField's CSV export — columns ``Frame``, ``Wavelength``, ``Intensity`` (and ``Row`` for multi-row regions, summed per pixel); one line per pixel. * Wide — the first column is the wavelength, every further column one frame of intensity. :param columns: The table's columns. :param names: The header row, when the file had one. :raises ValueError: If the table has fewer than two columns. .. py:method:: _from_lightfield_table(columns: list[numpy.ndarray], lowered: list[str]) -> Self :classmethod: Rebuild frames from LightField's one-line-per-pixel CSV export. .. py:method:: from_spe(path: str | pathlib.Path, region: int = 0, wavelength: numpy.typing.ArrayLike | None = None) -> Self :classmethod: Read a LightField SPE 3.0 file. Rows of a multi-row region are summed per pixel (full vertical binning); a single frame becomes a 1-D spectrum, several a stack. :param path: The ``.spe`` file. :param region: Index of the region of interest to read. :param wavelength: Axis to use when the file has no calibration, or to override it. :returns: The spectrum, with the file's acquisition settings in :attr:`metadata` and its name under ``metadata["source"]``. :raises ValueError: If the file has no such region, or no calibration and none was given. .. py:method:: from_run(run: Any, file_key: str = 'data_file', coordinates: Sequence[str] = (), where: Mapping[str, Any] | None = None, wavelength: numpy.typing.ArrayLike | None = None) -> Self :classmethod: Stack the spectra an experiment run recorded, in record order. Each record holding *file_key* names the raw file a device wrote for that step; the path is translated to this machine through the run's data paths (a relative path is taken from the run directory). A ``.spe`` file is read with :meth:`from_spe`; a ``.npy`` frame, which carries no axis, needs *wavelength*, and its rows are summed per pixel; any other file goes through :meth:`load`. A record whose value is itself a :class:`SpectrumData` is used as it is. :param run: A :class:`~plesty.lib.experiment.runs.Run` or a run directory. :param file_key: Record field naming the raw file. :param coordinates: Record fields to keep per frame, e.g. ``("hwp_deg", "power_w")``. :param where: Only records whose fields equal these values, e.g. ``{"qd": "QD-004"}``. :param wavelength: Axis for files without a calibration. :returns: One frame per recorded frame, with the requested coordinates; ``metadata`` names the run and ``provenance`` the run and experiment. :raises ValueError: If no record matches, a record lacks a coordinate, or the files do not share one wavelength axis. .. py:method:: _read_raw(reader: Any, host_path: str, wavelength: numpy.typing.ArrayLike | None) -> SpectrumData :classmethod: Read one raw device file named by a record. .. py:property:: is_stack :type: bool Whether the intensity holds several frames. .. py:property:: n_frames :type: int Number of frames (``1`` for a single spectrum). .. py:property:: n_pixels :type: int Number of pixels on the wavelength axis. .. py:method:: frame(index: int) -> Self Return one frame as a single spectrum. :param index: Frame index; negative indices count from the end. :raises IndexError: If there is no such frame. .. py:method:: combine(method: Literal['mean', 'sum'] = 'mean') -> Self Collapse a stack into one spectrum; the frame coordinates are dropped. :param method: ``"mean"`` or ``"sum"`` over the frames. :raises ValueError: If *method* is unknown. .. py:method:: crop(start: float, stop: float) -> Self Keep the pixels whose wavelength lies in ``[start, stop]``. :param start: Lower bound, in the wavelength's unit. :param stop: Upper bound, in the wavelength's unit. :raises ValueError: If no pixel lies in the range. .. py:method:: subtract_background(background: float | Literal['min', 'median'] | numpy.typing.ArrayLike = 'median') -> Self Subtract a background from every frame. :param background: A constant; ``"min"`` or ``"median"`` of each frame; or an array broadcastable to the intensity (e.g. a dark frame). :raises ValueError: If *background* is an unknown keyword. .. py:method:: normalize(method: Literal['max', 'area'] = 'max') -> Self Scale every frame to a peak of 1 or a unit area; the unit becomes ``a.u.``. :param method: ``"max"`` or ``"area"`` (trapezoidal, over the axis). :raises ValueError: If *method* is unknown or a frame's norm is zero. .. py:method:: energy() -> plesty.lib.data.array.PlestyArray Return the photon energy of each pixel, in eV. :raises ValueError: If the wavelength unit is not a length. .. py:method:: find_peaks(frame: int = 0, height: float | None = None, prominence: float | None = None, distance: int = 1) -> numpy.ndarray Find local maxima in one frame (NumPy only). A plateau counts once, at its first pixel. *prominence* is the height above the higher of the two lowest points separating the peak from taller neighbours (or from the ends of the axis). :param frame: Frame to search. :param height: Minimum value of a peak. :param prominence: Minimum prominence of a peak. :param distance: Minimum separation in pixels; the taller peak wins. :returns: Pixel indices of the peaks, in ascending order. .. py:method:: peak_prominences(indices: numpy.typing.ArrayLike, frame: int = 0) -> numpy.ndarray Return the prominence of each peak at *indices*, as :meth:`find_peaks` defines it. :param indices: Pixel indices of peaks, e.g. from :meth:`find_peaks`. :param frame: Frame the peaks are in. :returns: One prominence per index, in the intensity's unit. :raises IndexError: If an index is outside the axis. .. py:method:: match_peaks(reference: numpy.typing.ArrayLike, tolerance: float, frame: int = 0) -> numpy.ndarray Relocate known line positions to the strongest local maximum near each. For each reference position, the local maxima within ``±tolerance`` are searched and the strongest is taken; without a local maximum in range, the highest pixel in range is. :param reference: Line positions, in the wavelength's unit — e.g. lines identified in another measurement of the same emitter. :param tolerance: Half-width of the search range around each position. :param frame: Frame to search. :returns: One pixel index per reference position, in the order given. :raises ValueError: If *tolerance* is not positive or a range holds no pixel. .. py:method:: _derive(wavelength: numpy.typing.ArrayLike, intensity: numpy.typing.ArrayLike, step: dict[str, Any], coordinates: Mapping[str, numpy.typing.ArrayLike] | None = None) -> Self Return a new spectrum carrying this one's context plus *step*. The frame coordinates are kept unless *coordinates* replaces them. .. py:class:: SpectrumFitData(values: Mapping[str, numpy.typing.ArrayLike], wavelength_unit: str | None = 'nm', intensity_unit: str | None = 'counts', metadata: dict[str, Any] | None = None, provenance: dict[str, Any] | None = None, coordinates: Mapping[str, numpy.typing.ArrayLike] | None = None) Bases: :py:obj:`plesty.lib.data.experiment_data.ExperimentData` Line-shape fit results of a spectrum: one row per fitted frame, one column per line. Produced by :class:`plesty.lib.analyzer.SpectrumFit`. Every quantity is an array of shape ``(frames, peaks)``, with ``nan`` where a frame's fit failed; ``frame`` names the spectrum frame each row came from, and ``x_ref`` (one per peak) the position the linear background's offset refers to. ====================== ==================================================== ``center`` line position (wavelength unit) ``fwhm`` full width at half maximum (wavelength unit) ``height`` peak height above the background (intensity unit) ``area`` integrated intensity (intensity × wavelength unit) ``background_offset`` background at ``x_ref`` (intensity unit) ``background_slope`` background slope (intensity / wavelength unit) ``r_squared`` coefficient of determination of the peak's group ====================== ==================================================== Each quantity but ``r_squared`` has a one-sigma ``_err`` array. ``metadata`` records the ``model``, the ``background`` mode and the ``groups`` of peaks fitted together. :attr:`coordinates` carries the fitted frames' sweep values (``hwp_deg``, ``power_w``, …) from the spectrum, one per row — what a sweep analysis plots the quantities against. Validate the arrays and attach their units. :param values: Every name in :attr:`names`, as arrays. :param wavelength_unit: Unit of the spectrum's axis. :param intensity_unit: Unit of the spectrum's intensity. :param metadata: Fit context (``model``, ``background``, ``groups``). :param provenance: Origin of the data. :param coordinates: Sweep values of the fitted frames, one per row. :raises ValueError: If a name is missing or the shapes disagree. .. py:attribute:: kind :value: 'spectrum_fit' Registry key and schema dtype token, e.g. ``"spectrum"``. .. py:attribute:: schema_version :value: 1 Version of the array layout this class writes and reads. .. py:attribute:: QUANTITIES :type: ClassVar[tuple[str, Ellipsis]] :value: ('center', 'fwhm', 'height', 'area', 'background_offset', 'background_slope') .. py:attribute:: values :type: dict[str, plesty.lib.data.array.PlestyArray] the quantities, their errors, ``r_squared``, ``frame``, ``x_ref``. :type: Every array by name .. py:attribute:: coordinates :type: dict[str, plesty.lib.data.array.PlestyArray] Sweep values of the fitted frames by name, one per row. .. py:method:: names() -> tuple[str, Ellipsis] :classmethod: Return every array name, in storage order. .. py:method:: _units(wavelength: str | None, intensity: str | None) -> dict[str, str | None] :staticmethod: Return the unit of each array from the spectrum's units. .. py:method:: __getitem__(name: str) -> plesty.lib.data.array.PlestyArray Return the array called *name*, e.g. ``fit["center"]``. .. py:property:: n_frames :type: int Number of fitted frames. .. py:property:: n_peaks :type: int Number of fitted lines. .. py:method:: peak(index: int) -> dict[str, plesty.lib.data.array.PlestyArray] Return every per-frame quantity of one line, as 1-D arrays over frames. :param index: Peak index. :raises IndexError: If there is no such peak. .. py:method:: to_arrays() -> dict[str, plesty.lib.data.array.PlestyArray] Return every array by name, coordinates as ``coordinate_``. .. py:method:: from_arrays(arrays: dict[str, plesty.lib.data.array.PlestyArray], metadata: dict[str, Any], provenance: dict[str, Any]) -> Self :classmethod: Rebuild fit results from :meth:`to_arrays`; units travel with the arrays. .. py:method:: example() -> Self :classmethod: Return one Gaussian line at 775 nm fitted in two frames. .. py:function:: _coordinate_unit(name: str) -> str | None Return the unit a coordinate's name ends in, e.g. ``deg`` for ``hwp_deg``. .. py:function:: _frame_coordinates(coordinates: Mapping[str, numpy.typing.ArrayLike] | None, n_frames: int) -> dict[str, plesty.lib.data.array.PlestyArray] Validate per-frame coordinates and give plain arrays their name and unit. .. py:function:: _split_coordinates(arrays: Mapping[str, plesty.lib.data.array.PlestyArray]) -> dict[str, plesty.lib.data.array.PlestyArray] Return the ``coordinate_`` arrays of a saved spectrum, keyed by name. .. py:function:: _is_frame_block_header(line: str) -> bool Return whether *line* opens a frame block (``Frame 3``), not a column header. .. py:function:: _parse_block_row(line: str) -> tuple[float, float] | None Parse ``wavelength intensity`` from a block row, commas read as decimals. .. py:function:: _plateau_falls(y: numpy.ndarray, start: int) -> bool Return whether the flat run starting at *start* is followed by a drop. .. py:function:: _prominence(y: numpy.ndarray, index: int) -> float Return the topographic prominence of the peak at *index*.