plesty.lib.analyzer.spectrum_fit ================================ .. py:module:: plesty.lib.analyzer.spectrum_fit .. autoapi-nested-parse:: Spectrum fits: line shapes (Gaussian, Lorentzian, Voigt) on a background. Fitting is analysis, so it lives with the analyzers rather than on the data type. Two layers: * :class:`SpectrumFit` — the :class:`~plesty.lib.analyzer.Analyzer`: a :class:`~plesty.lib.data.SpectrumData` in; every line fitted in every frame out, as :class:`~plesty.lib.data.SpectrumFitData` (centre, FWHM, height, area and background, each ± σ, shaped ``(frames, peaks)``), plus the fitted curve. It seeds from the average spectrum, tracks each line from frame to frame, and fits overlapping lines together. * :func:`fit_spectrum` and :func:`fit_peak` — the primitives on any 1-D ``x``/``y``: several line shapes on a constant or linear background, with bounds and per-point errors, for analyzers that fit something other than a spectrum stack. Every line shape is height-parameterised, so ``height`` is the peak value above the background and ``area`` the integrated intensity, whatever the shape. Fitting needs SciPy, shipped in the ``fit`` extra (``uv add "plesty-lib[fit]"``). .. code-block:: python from plesty.lib.analyzer import SpectrumFit outputs = SpectrumFit(model="gaussian", background="linear")(spectrum=spectrum) fit = outputs["fit"] fit["center"], fit["center_err"], fit["area"] # (frames, peaks) Attributes ---------- .. autoapisummary:: plesty.lib.analyzer.spectrum_fit.MODELS plesty.lib.analyzer.spectrum_fit.BACKGROUNDS plesty.lib.analyzer.spectrum_fit.AVERAGES plesty.lib.analyzer.spectrum_fit._GAUSS_FWHM plesty.lib.analyzer.spectrum_fit._WIDTH_PARAMETERS plesty.lib.analyzer.spectrum_fit._AT_BOUND plesty.lib.analyzer.spectrum_fit._OUTLIER_Z plesty.lib.analyzer.spectrum_fit.DIAGNOSTICS plesty.lib.analyzer.spectrum_fit._FUNCTIONS Exceptions ---------- .. autoapisummary:: plesty.lib.analyzer.spectrum_fit.PeakFitError plesty.lib.analyzer.spectrum_fit.FitQualityWarning Classes ------- .. autoapisummary:: plesty.lib.analyzer.spectrum_fit.Peak plesty.lib.analyzer.spectrum_fit.PeakResult plesty.lib.analyzer.spectrum_fit.SpectrumFitResult plesty.lib.analyzer.spectrum_fit.SpectrumFit Functions --------- .. autoapisummary:: plesty.lib.analyzer.spectrum_fit._require_scipy plesty.lib.analyzer.spectrum_fit.gaussian plesty.lib.analyzer.spectrum_fit.lorentzian plesty.lib.analyzer.spectrum_fit.voigt plesty.lib.analyzer.spectrum_fit.voigt_fwhm plesty.lib.analyzer.spectrum_fit._fwhm plesty.lib.analyzer.spectrum_fit._area plesty.lib.analyzer.spectrum_fit._widths_from_fwhm plesty.lib.analyzer.spectrum_fit._model_function plesty.lib.analyzer.spectrum_fit.fit_spectrum plesty.lib.analyzer.spectrum_fit.fit_peak plesty.lib.analyzer.spectrum_fit._bounds plesty.lib.analyzer.spectrum_fit._inside plesty.lib.analyzer.spectrum_fit._estimate_width plesty.lib.analyzer.spectrum_fit._initial_guess plesty.lib.analyzer.spectrum_fit._propagate Module Contents --------------- .. py:data:: MODELS :type: tuple[str, Ellipsis] :value: ('gaussian', 'lorentzian', 'voigt') .. py:data:: BACKGROUNDS :type: tuple[str, Ellipsis] :value: ('constant', 'linear') .. py:data:: AVERAGES :type: tuple[str, Ellipsis] :value: ('mean', 'median') .. py:data:: _GAUSS_FWHM .. py:data:: _WIDTH_PARAMETERS :type: dict[str, tuple[str, Ellipsis]] .. py:exception:: PeakFitError Bases: :py:obj:`RuntimeError` Raised when a fit does not converge. Initialize self. See help(type(self)) for accurate signature. .. py:exception:: FitQualityWarning Bases: :py:obj:`UserWarning` Warned when fitted lines show signs of a fit that did not follow its line. Initialize self. See help(type(self)) for accurate signature. .. py:data:: _AT_BOUND :value: 0.001 .. py:data:: _OUTLIER_Z :value: 5.0 .. py:data:: DIAGNOSTICS :type: tuple[str, Ellipsis] :value: ('failed', 'no_error', 'center_at_bound', 'width_at_bound', 'jumps', 'outliers') .. py:function:: _require_scipy() -> Any Import SciPy, naming the extra that provides it when it is missing. .. py:function:: gaussian(x: numpy.typing.ArrayLike, amplitude: float, center: float, sigma: float, offset: float) -> numpy.ndarray Gaussian of peak height *amplitude* above *offset*. :param x: Positions. :param amplitude: Peak height above the offset. :param center: Peak position. :param sigma: Standard deviation; FWHM is ``2·sqrt(2·ln 2)·sigma``. :param offset: Constant background. :returns: The model evaluated at *x*. .. py:function:: lorentzian(x: numpy.typing.ArrayLike, amplitude: float, center: float, gamma: float, offset: float) -> numpy.ndarray Lorentzian of peak height *amplitude* above *offset*. :param x: Positions. :param amplitude: Peak height above the offset. :param center: Peak position. :param gamma: Half width at half maximum; FWHM is ``2·gamma``. :param offset: Constant background. :returns: The model evaluated at *x*. .. py:function:: voigt(x: numpy.typing.ArrayLike, amplitude: float, center: float, sigma: float, gamma: float, offset: float) -> numpy.ndarray Voigt profile of peak height *amplitude* above *offset* (needs SciPy). :param x: Positions. :param amplitude: Peak height above the offset. :param center: Peak position. :param sigma: Gaussian standard deviation. :param gamma: Lorentzian half width at half maximum. :param offset: Constant background. :returns: The model evaluated at *x*. .. py:function:: voigt_fwhm(sigma: float, gamma: float) -> float FWHM of a Voigt profile (Olivero & Longbothum, accurate to ~0.02 %). :param sigma: Gaussian standard deviation. :param gamma: Lorentzian half width at half maximum. :returns: The full width at half maximum. .. py:data:: _FUNCTIONS :type: dict[str, Callable[Ellipsis, numpy.ndarray]] .. py:function:: _fwhm(model: str, widths: Sequence[float] | numpy.ndarray) -> float Convert the shape's intrinsic width parameters to the line's FWHM. ``widths`` are the model-specific broadening parameters: ``sigma`` for a Gaussian, ``gamma`` for a Lorentzian, and ``(sigma, gamma)`` for a Voigt. The returned FWHM is the physical width at half maximum in the same units as the fitted axis. .. py:function:: _area(model: str, height: float, widths: Sequence[float] | numpy.ndarray) -> float Return the integrated area of a height-parameterised line shape. ``height`` is the peak value above the background, while ``widths`` are the model-specific width parameters rather than the final FWHM. For Gaussian and Lorentzian profiles, the area is analytic; for Voigt, it is derived from the central value of the Voigt profile. .. py:function:: _widths_from_fwhm(model: str, fwhm: float) -> list[float] Return the model's intrinsic width parameters for a target FWHM. This is used to seed a fit from a rough width estimate in the physical FWHM units of the data. The conversion differs for each line shape because each model uses a different definition of its broadening parameter. .. py:class:: Peak One line to fit, with its starting values and constraints. :ivar center: Initial line position. :ivar width: Initial FWHM; estimated from the data when ``None``. :ivar center_bounds: ``(low, high)`` for the position; the fitted range when ``None``. :ivar width_bounds: ``(low, high)`` bounds on the FWHM in the same units as ``x``. If omitted, the width is constrained to stay above roughly one sample spacing, preventing a narrow noise spike from fitting as a line. For a Voigt profile, the bound is applied to each of the two width components separately. :ivar nonnegative: Keep the height at or above zero. .. py:attribute:: center :type: float .. py:attribute:: width :type: float | None :value: None .. py:attribute:: center_bounds :type: tuple[float, float] | None :value: None .. py:attribute:: width_bounds :type: tuple[float, float] | None :value: None .. py:attribute:: nonnegative :type: bool :value: True .. py:class:: PeakResult The fitted values of one line, each with its one-sigma error. :ivar params: The raw line-shape parameters (``height``, ``center``, ``sigma`` and/or ``gamma``). :ivar errors: Their one-sigma errors (``nan`` when not estimable). .. py:attribute:: center :type: float .. py:attribute:: center_err :type: float .. py:attribute:: fwhm :type: float .. py:attribute:: fwhm_err :type: float .. py:attribute:: height :type: float .. py:attribute:: height_err :type: float .. py:attribute:: area :type: float .. py:attribute:: area_err :type: float .. py:attribute:: params :type: dict[str, float] .. py:attribute:: errors :type: dict[str, float] .. py:class:: SpectrumFitResult Result of :func:`fit_spectrum`. :ivar model: The line shape of every peak. :ivar background: ``"constant"`` or ``"linear"``. :ivar peaks: One :class:`PeakResult` per peak, in the order given. :ivar background_params: ``offset`` (at :attr:`x_ref`) and ``slope`` (``0`` for a constant background). :ivar background_errors: Their one-sigma errors. :ivar x_ref: Position the background offset refers to. :ivar r_squared: Coefficient of determination. :ivar values: The optimiser parameter tuple in fixed order: background terms first (``offset`` and optionally ``slope``), then for each peak ``(height, center, ...)`` with the width parameters in the model's native order (``sigma`` for Gaussian, ``gamma`` for Lorentzian, ``sigma, gamma`` for Voigt). Examples: ``(offset, slope, h1, c1, sigma1, h2, c2, sigma2)`` for two Gaussian peaks on a linear background; ``(offset, slope, h, c, gamma)`` for one Lorentzian peak on a linear background; ``(offset, h, c, sigma, gamma)`` for one Voigt peak on a constant background. This is the exact order expected by :func:`_model_function` and can be reused as a seed for a later fit. :ivar covariance: The parameter covariance matrix in the same order as :attr:`values`. .. py:attribute:: model :type: str .. py:attribute:: background :type: str .. py:attribute:: peaks :type: tuple[PeakResult, Ellipsis] .. py:attribute:: background_params :type: dict[str, float] .. py:attribute:: background_errors :type: dict[str, float] .. py:attribute:: x_ref :type: float .. py:attribute:: r_squared :type: float .. py:attribute:: values :type: tuple[float, Ellipsis] .. py:attribute:: covariance :type: numpy.ndarray .. py:method:: evaluate(x: numpy.typing.ArrayLike) -> numpy.ndarray Evaluate the fitted model — background plus every line — at *x*. .. py:method:: to_dict() -> dict[str, Any] Return the result as a JSON-serializable dictionary (no covariance). .. py:function:: _model_function(model: str, background: str, n_peaks: int, x_ref: float) -> Callable[Ellipsis, numpy.ndarray] Return ``f(x, *values)``: background plus *n_peaks* line shapes. .. py:function:: fit_spectrum(x: numpy.typing.ArrayLike, y: numpy.typing.ArrayLike, peaks: Sequence[Peak], model: str = 'gaussian', background: str = 'linear', *, sigma: numpy.typing.ArrayLike | None = None, seed: SpectrumFitResult | None = None, x_ref: float | None = None) -> SpectrumFitResult Fit one or more line shapes on a background. Without a *seed*, starting values come from the data: the background from a fit to the points away from the lines, each height from the data at its centre, each width from the half-maximum crossings (or :attr:`Peak.width`). Starting values are moved inside the bounds. :param x: Positions, 1-D. :param y: Values at *x*, same shape. :param peaks: The lines to fit. :param model: Line shape of every peak, one of :data:`MODELS`. :param background: One of :data:`BACKGROUNDS`. :param sigma: One-sigma error of each *y*, for a weighted fit. The covariance is scaled by the reduced chi-square, as a relative weighting. :param seed: A previous result with the same peaks, model and background, whose values start this fit. :param x_ref: Position the background offset refers to; the mean of the peak centres by default. :returns: The fit result. :raises ValueError: If an argument is unknown or the data are unusable. :raises PeakFitError: If the optimiser does not converge. :raises ImportError: If SciPy (the ``fit`` extra) is not installed. .. py:function:: fit_peak(x: numpy.typing.ArrayLike, y: numpy.typing.ArrayLike, model: str = 'gaussian', *, center: float | None = None, width: float | None = None, background: str = 'linear', sigma: numpy.typing.ArrayLike | None = None, center_bounds: tuple[float, float] | None = None, width_bounds: tuple[float, float] | None = None, nonnegative: bool = True) -> SpectrumFitResult Fit one line on a background — :func:`fit_spectrum` with a single :class:`Peak`. :param x: Positions, 1-D. :param y: Values at *x*. :param model: Line shape, one of :data:`MODELS`. :param center: Initial line position; the highest point by default. :param width: Initial FWHM. :param background: One of :data:`BACKGROUNDS`. :param sigma: One-sigma error of each *y*. :param center_bounds: ``(low, high)`` for the position. :param width_bounds: ``(low, high)`` for the FWHM. :param nonnegative: Keep the height at or above zero. :returns: The fit result; ``result.peaks[0]`` holds the line. .. py:function:: _bounds(xs: numpy.ndarray, peaks: Sequence[Peak], model: str, background: str) -> tuple[numpy.ndarray, numpy.ndarray] Return the lower and upper bound of every parameter. .. py:function:: _inside(values: numpy.ndarray, lower: numpy.ndarray, upper: numpy.ndarray) -> numpy.ndarray Move starting values strictly inside their bounds. .. py:function:: _estimate_width(xs: numpy.ndarray, ys: numpy.ndarray, index: int, level: float) -> float | None Return the distance between the half-maximum crossings around *index*. .. py:function:: _initial_guess(xs: numpy.ndarray, ys: numpy.ndarray, peaks: Sequence[Peak], model: str, background: str, x_ref: float) -> numpy.ndarray Return data-driven starting values for :func:`fit_spectrum`. .. py:function:: _propagate(function: Callable[[numpy.ndarray], float], values: numpy.ndarray, covariance: numpy.ndarray, indices: Sequence[int]) -> float Return the one-sigma error of ``function(values)`` from the covariance block. .. py:class:: SpectrumFit(peaks: str | Sequence[float] = 'auto', model: str = 'gaussian', background: str = 'linear', window: float | None = None, frames: str | int | Sequence[int] = 'all', order: Sequence[int] | None = None, track: bool = True, overlap: float = 2.0, prominence: float | None = None, distance: int = 1, tolerance: float | None = None, width: float | None = None, width_bounds: tuple[float, float] | None = None, center_tolerance: float | None = None, nonnegative: bool = True, average: str = 'mean') Bases: :py:obj:`plesty.lib.analyzer.base_analyzer.Analyzer` Fit every line in every frame of a spectrum. 1. **Lines** — ``peaks="auto"`` finds them in the average of the selected frames (``prominence``, ``distance``); a list of positions uses those, relocated to the nearest local maximum within ``tolerance`` if set. ``average="median"`` takes the per-pixel median of the frames instead of their mean: a cosmic ray hits one frame and drops out of the median, where in the mean of a few dozen frames it can still stand out as a line. 2. **Groups** — each line's FWHM is estimated on the average; neighbours closer than ``overlap`` × the larger FWHM are fitted together, every other line alone. ``overlap=0`` fits every line alone. 3. **Range** — a group is fitted over ``window`` beyond its outer lines on each side, re-centred on each frame's starting centres so the range follows a drifting line; without a window, up to halfway to the neighbouring groups. 4. **Frames** — each group is fitted to the average first; that result seeds the first frame in ``order``, and with ``track`` each frame seeds the next. ``center_tolerance`` keeps each line within that distance of its seed. A frame whose fit fails is ``nan``. 5. **Quality** — nothing above is changed by it: per line, frames that failed, lacked a centre error or sat at a centre or width bound, jumps wider than the line between frames, and outlying centres are counted into ``fit.metadata["diagnostics"]``, and a :class:`FitQualityWarning` names every line with a count. With ``track``, each frame's range is centred on the previous frame's fitted position, so when a line is weak in one frame its fit can move onto a brighter neighbouring spectral line inside the range and stay on it; these counts are how that shows. Outputs ``fit`` (:class:`~plesty.lib.data.SpectrumFitData`, one row per frame in ``frames``) and ``curve`` — the fitted model on the spectrum's axis, ``nan`` outside the fitted ranges. Validate and store the fit parameters. :param peaks: ``"auto"``, or line positions in the wavelength unit. :param model: Line shape, one of :data:`MODELS`. :param background: One of :data:`BACKGROUNDS`. :param window: Fitted range beyond a group's outer lines, each side. :param frames: ``"all"``, one frame index, or a list of them. :param order: The frames in the order to fit them. :param track: Seed each frame with the previous frame's fit. :param overlap: Grouping factor on the FWHM; ``0`` never groups. :param prominence: Minimum prominence for ``peaks="auto"``. :param distance: Minimum separation in pixels for ``peaks="auto"``. :param tolerance: Relocate given positions to a local maximum this close. :param width: Initial FWHM of every line. :param width_bounds: ``(low, high)`` FWHM bounds of every line. :param center_tolerance: Maximum distance of a line from its seed position. :param nonnegative: Keep heights at or above zero. :param average: ``"mean"`` or ``"median"`` of the frames: the spectrum lines are found in, their widths estimated on, and the first fit made to. :raises ValueError: If a parameter is out of range. .. py:attribute:: input_schema name → schema entry. :type: Declared inputs of :meth:`analyze` .. py:attribute:: output_schema name → schema entry. :type: Declared outputs of :meth:`analyze` .. py:method:: analyze(spectrum: plesty.lib.data.spectrum.SpectrumData) -> dict[str, Any] Fit every line in every selected frame. :param spectrum: The spectrum to fit. :returns: ``fit`` and ``curve``, see the class description. :raises ValueError: If a frame is out of range, ``order`` does not match ``frames``, or no line is found. :raises PeakFitError: If a group cannot be fitted even to the average. .. py:method:: _frames(selection: str | int | list[int], total: int) -> list[int] :staticmethod: Return the selected frame indices, normalised and checked. .. py:method:: _order(order: list[int], frames: list[int], total: int) -> list[int] Return the fitting order, which must be a permutation of *frames*. .. py:method:: _centers(spectrum: plesty.lib.data.spectrum.SpectrumData, average: numpy.ndarray) -> numpy.ndarray Return the sorted line positions to fit. .. py:method:: _fwhms(axis: numpy.ndarray, average: numpy.ndarray, centers: numpy.ndarray) -> numpy.ndarray Estimate each line's FWHM with a single-line fit to the average. .. py:method:: _groups(centers: numpy.ndarray, fwhms: numpy.ndarray) -> list[list[int]] Group neighbouring lines closer than ``overlap`` × the larger FWHM. .. py:method:: _range(axis: numpy.ndarray, centers: numpy.ndarray, fwhms: numpy.ndarray, groups: list[list[int]], g: int) -> tuple[float, float] Return the fitted range of group *g*. .. py:method:: _center_bounds(center: float, low: float, high: float) -> tuple[float, float] Return the position bounds of a line: its tolerance, within the range. .. py:method:: _check_bounds(diagnostics: list[dict[str, int]], group: list[int], result: SpectrumFitResult, bounded: list[Peak], x: numpy.ndarray) -> None :staticmethod: Count, for one frame, lines without a centre error or pinned at a bound. .. py:method:: _check_tracks(diagnostics: list[dict[str, int]], center: numpy.ndarray, frames: list[int], order: list[int], fwhms: numpy.ndarray) -> None :staticmethod: Count, per line, jumps wider than the line between frames and outlying centres. .. py:method:: _warn(diagnostics: list[dict[str, int]], centers: numpy.ndarray, unit: str | None) -> None :staticmethod: Warn once, naming every line with a count, without changing any result. .. py:method:: _store(table: dict[str, numpy.ndarray], row: int, group: list[int], result: SpectrumFitResult) -> None :staticmethod: Write one group's fit of one frame into the result table.