Skip to content

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

@PierreRaybaut

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions