# SpectrumFit `SpectrumFit` is the analyzer that comes with plesty-lib, in `plesty.lib.analyzer`. It fits every emission line in every spectrum of a `SpectrumData` (a stack of spectra, one per step of a scan; see [Example: `SpectrumData`](../common/data_schemas.md#example-spectrumdata)), each on a background, and returns the lines' positions, widths and heights with their uncertainties. It needs SciPy, from the `fit` extra (see [Install](../install.md#extras)), and says so if it is missing. Each spectrum in the stack is called a **frame**. `SpectrumData.from_run` builds the stack from a run: one frame per step that saved a spectrum, with the values scanned in that step (here the angle `hwp_deg`) kept as the frame's coordinates. ```python from plesty.lib.analyzer import SpectrumFit from plesty.lib.data import SpectrumData spectrum = SpectrumData.from_run(run_dir, coordinates=("hwp_deg",)) outputs = SpectrumFit(prominence=250, window=0.05, center_tolerance=0.1)(spectrum=spectrum) fit = outputs["fit"] # SpectrumFitData fit["center"], fit["area_err"] # (frames, peaks) fit.coordinates["hwp_deg"] # one angle per row outputs["curve"] # the fitted model, nan outside fitted ranges ``` How it fits: 1. **Lines** — `peaks="auto"` finds them in the average of the selected frames (`prominence`, `distance`); a list of positions is used as given, or relocated to the strongest local maximum within `tolerance`. With `average="median"` the average is the per-pixel median: a cosmic ray hits one frame and drops out of the median, while 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. 3. **Range** — a group is fitted over `window` beyond its outer lines, re-centred on each frame's starting centres so it follows a drifting line; without a window, up to halfway to the neighbouring groups. 4. **Frames** — the average seeds the first frame in `order`; with `track`, each frame seeds the next, and `center_tolerance` keeps a line near its seed. A frame whose fit fails is `nan`. 5. **Quality** — reported, never acted on. With `track`, each frame's range is centred on the previous frame's fitted position. When a line is weak in one frame, its fit can move onto a brighter neighbouring spectral line inside that range and stay on it for all later frames. Per line, `fit.metadata["diagnostics"]` counts the frames that show this, and a `FitQualityWarning` names every line with a non-zero count: | Counter | Counts | |---|---| | `failed` | frames whose fit did not converge (`nan`) | | `no_error` | frames whose centre error could not be estimated | | `center_at_bound` | frames with the centre at its window or `center_tolerance` edge | | `width_at_bound` | frames with the FWHM at a width bound (not checked for Voigt) | | `jumps` | steps between consecutive frames larger than the line's FWHM | | `outliers` | centres more than 5 robust σ (MAD) from the line's median | A steadily drifting line raises none of them. Filter the warning with `warnings.simplefilter("ignore", FitQualityWarning)` once the counts are read. | Parameter | Default | Meaning | |---|---|---| | `peaks` | `"auto"` | `"auto"`, or line positions in the wavelength unit | | `model` | `"gaussian"` | `"gaussian"`, `"lorentzian"` or `"voigt"` | | `background` | `"linear"` | `"linear"` or `"constant"` | | `window` | halfway to neighbours | Fitted range beyond a group's outer lines | | `frames` / `order` | all, in index order | Frames to fit, and the order to fit them in | | `track` | `True` | Seed each frame with the previous frame's fit | | `overlap` | `2.0` | Grouping factor on the FWHM; `0` fits every line alone | | `prominence` / `distance` | none / `1` | Line detection for `peaks="auto"` | | `average` | `"mean"` | `"mean"` or `"median"` of the frames: where lines are found and the first fit is made | | `tolerance` | none | Relocate given positions to a local maximum this close | | `width` / `width_bounds` | estimated / none | Initial FWHM and its bounds | | `center_tolerance` | fitted range | Maximum distance of a line from its seed | | `nonnegative` | `True` | Keep heights at or above zero | Outputs: `fit`, a `SpectrumFitData`, and `curve`, a `SpectrumData`. Both keep the input spectrum's provenance and frame coordinates; the parameters enter the provenance stamp. `SpectrumFitData` holds every quantity as an array `(frames, peaks)`: `center`, `fwhm`, `height`, `area`, `background_offset`, `background_slope`, each with `_err`, plus `r_squared`, with `nan` where a frame's fit failed. `frame` names the spectrum frame of each row, `x_ref` the position each background offset refers to, and `coordinates` the rows' sweep values. Units follow the spectrum (`area` in intensity × wavelength). `fit["center"]` reads an array; `fit.peak(i)` gives one line's quantities over the frames. Analyzers that fit something other than a spectrum stack — a line centre across a polarization sweep, say — build on the same primitives: `fit_spectrum(x, y, [Peak(...), ...], model, background, sigma=...)` fits several lines with bounds and per-point errors and returns a `SpectrumFitResult` (per line `center`, `fwhm`, `height`, `area`, each ± error; background; `r_squared`; `evaluate(x)`); `fit_peak` is its one-line form.