Skip to content

Add "Multi-Gaussian fit" and "Multi-Lorentzian fit" to interactive fitting #6

Description

@PierreRaybaut

Warning

This implementation plan is an initial draft and must not be taken as an absolute blueprint. A thorough analysis of constraints, dependencies, and architectural impacts is strictly required before starting the implementation.

Summary

Add two new entries to the Processing > Fitting > Interactive fitting submenu — Multi-Gaussian fit and Multi-Lorentzian fit — reproducing the behaviour already shipped in DataLab desktop. Both are multi-peak fits: the user first detects/selects the peaks of interest, then interactively adjusts one amplitude and one width per peak (plus a single shared baseline) while a live overlay shows the fitted curve, before committing the result as a new signal.

Motivation

DataLab desktop exposes these two interactive fits and they are commonly used for spectral analysis (overlapping peaks). DataLab-Web already implements the full single-peak interactive fitting machinery (Linear, Polynomial, Gaussian, Lorentzian, Voigt, Exponential, Sinusoidal, Planckian, Two half-Gaussians, CDF, Sigmoid, Piecewise exponential). The two multi-peak fits are the only entries from the desktop submenu still missing, so this closes a known feature gap and reaches parity for the fitting submenu.

Desktop reference behaviour (what we must reproduce)

The desktop flow lives in:

  • DataLab/datalab/gui/processor/signal.pycompute_multigaussianfit() / compute_multilorentzianfit()
  • DataLab/datalab/widgets/signalpeak.pySignalPeakDetectionDialog
  • DataLab/datalab/widgets/fitdialog.pymultigaussian_fit() / multilorentzian_fit()
  • Sigima/sigima/tools/signal/fitting.pyMultiGaussianFitComputer / MultiLorentzianFitComputer (both subclass BaseMultiPeakFitComputer)
  • Sigima/sigima/tools/signal/peakdetection.pypeak_indices()

The desktop sequence is two steps:

  1. Peak detection. A modal dialog (SignalPeakDetectionDialog) shows the signal with a horizontal threshold cursor and a "Minimum distance" slider. As either changes, sigima.tools.signal.peakdetection.peak_indices(y, thres=<cursor>, min_dist=<slider>, thres_abs=True) recomputes the detected peaks, which are drawn as vertical markers. Accepting the dialog yields a list of peak_indices.
  2. Interactive fit. multigaussian_fit(x, y, peak_indices) / multilorentzian_fit(x, y, peak_indices) build one FitDialog with, per detected peak, an amplitude A## and a width σ##, plus a single shared baseline Y0. The peak centres x0 are fixed at the detected positions (passed as the a_x0 kwarg, not as adjustable fit parameters). Initial estimates and bounds come from the matching Sigima *FitComputer. The desktop dialog uses a 4-column parameter layout when there are more than 8 parameters.

Key modelling detail to honour: in the desktop fit the peak centres are not free parameters — only amplitude + width per peak and one global baseline are adjustable.

Current DataLab-Web state

  • Backend: DataLab-Web/src/runtime/dlw_interactive_fit.py exposes list_interactive_fits, init_interactive_fit, evaluate_interactive_fit, auto_fit_interactive, commit_interactive_fit. Fit kinds are described by the _FitKind helper and the _INTERACTIVE_FITS catalogue. The only "extra" currently supported is needs_degree (polynomial).
  • Runtime bridge: DataLab-Web/src/runtime/runtime.ts declares InteractiveFitInfo / InteractiveFitParam / InteractiveFitInit / InteractiveFitAuto and the matching DataLabRuntime methods.
  • UI: DataLab-Web/src/components/InteractiveFitDialog.tsx (sliders + spin-boxes, auto-fit, live Plotly overlay, commit), wired in DataLab-Web/src/App.tsx via handleLaunchInteractiveFit / pendingFit and surfaced in the menu by buildInteractiveFitActions in DataLab-Web/src/actions/registry.ts.
  • No peak-detection capability exists in DataLab-Web yet — this is the main new building block required by this feature.

Gaps / decisions to resolve

  1. Peak detection step. There is no equivalent of SignalPeakDetectionDialog in the web app. We need a peak-selection step that produces peak_indices before the fit dialog opens. Reproduce the desktop UX: a modal showing the signal, an adjustable threshold (absolute Y level) and a "Minimum distance" (in points) control, recomputing peaks live via peak_indices(..., thres_abs=True) and drawing markers on the detected peaks.
  2. Fixed peak centres. BaseMultiPeakFitComputer.get_params_names() returns amp_i, sigma_i, and x0_i per peak plus y0. To match desktop, the interactive dialog must treat x0_i as fixed (initialised from the detected peak positions, hidden from the slider list) and only expose amp_i / sigma_i / y0. The fixed x0_i values must still be forwarded to evaluate/optimize/commit so the model is evaluated correctly.
  3. Passing peak indices through the existing pipeline. _FitKind.make_computer() currently only knows about degree. Add a needs_peaks flag (analogous to needs_degree) so the multi-peak computers receive peak_indices from extras. The peak positions / indices must travel through extras for init_interactive_fit, evaluate_interactive_fit, auto_fit_interactive and commit_interactive_fit so every round-trip stays consistent.
  4. Auto-fit with fixed centres. Confirm optimize_fit_with_scipy() on the multi-peak computers keeps the centres free or fixed, and align the web behaviour with the desktop (centres fixed). If the Sigima optimiser frees the centres, constrain or post-process so committed x0_i stay at the detected peaks (or explicitly decide to allow centre refinement and document the divergence).

Proposed implementation plan

Python backend — src/runtime/dlw_interactive_fit.py

  • Extend _FitKind with a needs_peaks flag and teach make_computer() to pass peak_indices (read from extras["peak_indices"]) to MultiGaussianFitComputer / MultiLorentzianFitComputer.
  • Register multigaussian_fit and multilorentzian_fit in _INTERACTIVE_FITS (needs_peaks=True), with labels "Multi-Gaussian fit" / "Multi-Lorentzian fit".
  • Surface needs_peaks in list_interactive_fits() output.
  • In _make_param_descriptors (or a multi-peak variant), exclude the x0_i parameters from the slider rows while keeping them in extras so they remain available for evaluation, and pretty-label amp_iA## and sigma_iσ## to match desktop.
  • Ensure _evaluate, evaluate_interactive_fit, auto_fit_interactive and commit_interactive_fit reconstruct the full flat parameter list (interleaving the fixed x0_i with the adjustable amp_i/sigma_i and the shared y0) before calling the computer's evaluate / optimiser.
  • Add a peak-detection entry point, e.g. detect_signal_peaks(oid, thres, min_dist) returning { "peak_indices": [...], "peaks": [[x, y], ...] }, delegating to sigima.tools.signal.peakdetection.peak_indices(y, thres=thres, min_dist=min_dist, thres_abs=True) over the ROI-restricted X/Y (reuse _roi_xy). Export it in __all__.
  • Decide and implement the auto-fit centre policy (keep x0_i fixed to match desktop).

Runtime bridge — src/runtime/runtime.ts

  • Add needs_peaks to InteractiveFitInfo.
  • Add a detectSignalPeaks(oid, thres, minDist) method returning the peak indices + coordinates (with a small typed interface, e.g. PeakDetectionResult).
  • Verify extras plumbing in initInteractiveFit / evaluateInteractiveFit / autoFitInteractive / commitInteractiveFit carries peak_indices (and the fixed x0 values) end to end without widening any type.

UI — peak detection dialog

  • New component src/components/PeakDetectionDialog.tsx reproducing SignalPeakDetectionDialog: Plotly curve of the source signal, a draggable/adjustable absolute threshold line, a "Minimum distance" slider (max ≈ len(y) / 4), live recomputation through runtime.detectSignalPeaks, vertical markers at detected peaks, and a peak count/positions readout. Resolves to the chosen peak_indices on accept.

UI — interactive fit dialog — src/components/InteractiveFitDialog.tsx

  • Accept the peak_indices (and detected peak X positions) via extras for needs_peaks fits, analogous to the existing degree handling.
  • Render only the adjustable rows (A##, σ##, Y0); keep the fixed centres out of the slider list but feed them into every evaluate / auto-fit / commit call.
  • Use a multi-column parameter layout for many peaks (desktop switches to 4 columns above 8 parameters) so the dialog stays usable.

Orchestration — src/App.tsx

  • In handleLaunchInteractiveFit, branch on fit.needs_peaks: first open PeakDetectionDialog, then open InteractiveFitDialog seeded with the resulting peak_indices in extras. Single-peak fits keep their current one-step flow.
  • Thread the new state (pending peak detection → pending fit) through the existing pendingFit machinery.

Menu wiring — src/actions/registry.ts

  • No structural change expected: buildInteractiveFitActions already maps every catalogue entry to Processing/Fitting/Interactive fitting/<label>. Confirm the two new entries appear in the right place and order, and add an icon mapping if the submenu uses per-entry icons.

Internationalisation

  • Wrap all new user-facing strings in t("…") ("Multi-Gaussian fit", "Multi-Lorentzian fit", "Minimum distance", "Threshold", "Peaks", peak-count messages, etc.).
  • If any label reaches t() only through a variable (e.g. the catalogue label), add it to src/locales/_dynamic-keys.json.
  • Run the 🌍 i18n: Extract keys (step 1/2) task, fill the new French values in src/locales/fr.json, then run 🌍 i18n: Check catalog (step 2/2).

Tests

  • Python (tests/python): unit-test detect_signal_peaks and the multi-peak init / evaluate / auto_fit / commit round-trip (fixed centres preserved, correct number of A##/σ## rows + single Y0, committed title and fit_params metadata sane).
  • Vitest (tests/ts): update registry.test.ts (catalogue length / new entries) and cover the needs_peaks branch wiring; run the 🟢 Vitest (TS) task (mandatory pre-completion check).
  • Playwright: end-to-end probe driving Create signal → Processing > Fitting > Interactive fitting > Multi-Gaussian fit → peak detection → fit dialog → OK, asserting a new fitted signal appears (use a throwaway tests/e2e/_repro_*.spec.ts or a permanent regression spec per the testing strategy).

Acceptance criteria

  • Processing > Fitting > Interactive fitting lists Multi-Gaussian fit and Multi-Lorentzian fit, enabled only when a signal is selected and the runtime is ready.
  • Selecting either entry opens a peak-detection step (threshold + minimum distance) that previews detected peaks live and yields the peak set on accept.
  • The fit dialog then exposes one amplitude (A##) and one width (σ##) per detected peak plus a single shared baseline Y0, with peak centres held fixed at the detected positions.
  • Slider/spin-box changes update the live overlay; Auto fit runs the Sigima optimiser; OK commits a new signal whose title and fit_params metadata reflect the fit (matching the desktop convention).
  • Behaviour and results are consistent with DataLab desktop for representative multi-peak spectra.
  • All new user-facing strings are translated; npm run i18n:check passes.
  • Vitest, the Python fitting tests, and the Playwright probe all pass.

References

  • Desktop processor: DataLab/datalab/gui/processor/signal.py (compute_multigaussianfit, compute_multilorentzianfit)
  • Desktop peak dialog: DataLab/datalab/widgets/signalpeak.py (SignalPeakDetectionDialog)
  • Desktop fit dialog: DataLab/datalab/widgets/fitdialog.py (multigaussian_fit, multilorentzian_fit)
  • Sigima computers: Sigima/sigima/tools/signal/fitting.py (BaseMultiPeakFitComputer, MultiGaussianFitComputer, MultiLorentzianFitComputer)
  • Sigima peak detection: Sigima/sigima/tools/signal/peakdetection.py (peak_indices)
  • Web backend: DataLab-Web/src/runtime/dlw_interactive_fit.py
  • Web bridge: DataLab-Web/src/runtime/runtime.ts
  • Web UI: DataLab-Web/src/components/InteractiveFitDialog.tsx, DataLab-Web/src/App.tsx, DataLab-Web/src/actions/registry.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions