You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)
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.
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 baselineY0. 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
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.
Fixed peak centres.BaseMultiPeakFitComputer.get_params_names() returns amp_i, sigma_i, andx0_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.
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.
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).
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_i → A## 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.
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.py→compute_multigaussianfit()/compute_multilorentzianfit()DataLab/datalab/widgets/signalpeak.py→SignalPeakDetectionDialogDataLab/datalab/widgets/fitdialog.py→multigaussian_fit()/multilorentzian_fit()Sigima/sigima/tools/signal/fitting.py→MultiGaussianFitComputer/MultiLorentzianFitComputer(both subclassBaseMultiPeakFitComputer)Sigima/sigima/tools/signal/peakdetection.py→peak_indices()The desktop sequence is two steps:
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 ofpeak_indices.multigaussian_fit(x, y, peak_indices)/multilorentzian_fit(x, y, peak_indices)build oneFitDialogwith, per detected peak, an amplitudeA##and a widthσ##, plus a single shared baselineY0. The peak centresx0are fixed at the detected positions (passed as thea_x0kwarg, 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
DataLab-Web/src/runtime/dlw_interactive_fit.pyexposeslist_interactive_fits,init_interactive_fit,evaluate_interactive_fit,auto_fit_interactive,commit_interactive_fit. Fit kinds are described by the_FitKindhelper and the_INTERACTIVE_FITScatalogue. The only "extra" currently supported isneeds_degree(polynomial).DataLab-Web/src/runtime/runtime.tsdeclaresInteractiveFitInfo/InteractiveFitParam/InteractiveFitInit/InteractiveFitAutoand the matchingDataLabRuntimemethods.DataLab-Web/src/components/InteractiveFitDialog.tsx(sliders + spin-boxes, auto-fit, live Plotly overlay, commit), wired inDataLab-Web/src/App.tsxviahandleLaunchInteractiveFit/pendingFitand surfaced in the menu bybuildInteractiveFitActionsinDataLab-Web/src/actions/registry.ts.Gaps / decisions to resolve
SignalPeakDetectionDialogin the web app. We need a peak-selection step that producespeak_indicesbefore 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 viapeak_indices(..., thres_abs=True)and drawing markers on the detected peaks.BaseMultiPeakFitComputer.get_params_names()returnsamp_i,sigma_i, andx0_iper peak plusy0. To match desktop, the interactive dialog must treatx0_ias fixed (initialised from the detected peak positions, hidden from the slider list) and only exposeamp_i/sigma_i/y0. The fixedx0_ivalues must still be forwarded toevaluate/optimize/commitso the model is evaluated correctly._FitKind.make_computer()currently only knows aboutdegree. Add aneeds_peaksflag (analogous toneeds_degree) so the multi-peak computers receivepeak_indicesfromextras. The peak positions / indices must travel throughextrasforinit_interactive_fit,evaluate_interactive_fit,auto_fit_interactiveandcommit_interactive_fitso every round-trip stays consistent.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 committedx0_istay 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_FitKindwith aneeds_peaksflag and teachmake_computer()to passpeak_indices(read fromextras["peak_indices"]) toMultiGaussianFitComputer/MultiLorentzianFitComputer.multigaussian_fitandmultilorentzian_fitin_INTERACTIVE_FITS(needs_peaks=True), with labels "Multi-Gaussian fit" / "Multi-Lorentzian fit".needs_peaksinlist_interactive_fits()output._make_param_descriptors(or a multi-peak variant), exclude thex0_iparameters from the slider rows while keeping them inextrasso they remain available for evaluation, and pretty-labelamp_i→A##andsigma_i→σ##to match desktop._evaluate,evaluate_interactive_fit,auto_fit_interactiveandcommit_interactive_fitreconstruct the full flat parameter list (interleaving the fixedx0_iwith the adjustableamp_i/sigma_iand the sharedy0) before calling the computer'sevaluate/ optimiser.detect_signal_peaks(oid, thres, min_dist)returning{ "peak_indices": [...], "peaks": [[x, y], ...] }, delegating tosigima.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__.x0_ifixed to match desktop).Runtime bridge —
src/runtime/runtime.tsneeds_peakstoInteractiveFitInfo.detectSignalPeaks(oid, thres, minDist)method returning the peak indices + coordinates (with a small typed interface, e.g.PeakDetectionResult).extrasplumbing ininitInteractiveFit/evaluateInteractiveFit/autoFitInteractive/commitInteractiveFitcarriespeak_indices(and the fixedx0values) end to end without widening any type.UI — peak detection dialog
src/components/PeakDetectionDialog.tsxreproducingSignalPeakDetectionDialog: Plotly curve of the source signal, a draggable/adjustable absolute threshold line, a "Minimum distance" slider (max ≈len(y) / 4), live recomputation throughruntime.detectSignalPeaks, vertical markers at detected peaks, and a peak count/positions readout. Resolves to the chosenpeak_indiceson accept.UI — interactive fit dialog —
src/components/InteractiveFitDialog.tsxpeak_indices(and detected peak X positions) viaextrasforneeds_peaksfits, analogous to the existingdegreehandling.A##,σ##,Y0); keep the fixed centres out of the slider list but feed them into everyevaluate/auto-fit/commitcall.Orchestration —
src/App.tsxhandleLaunchInteractiveFit, branch onfit.needs_peaks: first openPeakDetectionDialog, then openInteractiveFitDialogseeded with the resultingpeak_indicesinextras. Single-peak fits keep their current one-step flow.pendingFitmachinery.Menu wiring —
src/actions/registry.tsbuildInteractiveFitActionsalready maps every catalogue entry toProcessing/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
t("…")("Multi-Gaussian fit", "Multi-Lorentzian fit", "Minimum distance", "Threshold", "Peaks", peak-count messages, etc.).t()only through a variable (e.g. the cataloguelabel), add it tosrc/locales/_dynamic-keys.json.src/locales/fr.json, then run 🌍 i18n: Check catalog (step 2/2).Tests
tests/python): unit-testdetect_signal_peaksand the multi-peakinit/evaluate/auto_fit/commitround-trip (fixed centres preserved, correct number ofA##/σ##rows + singleY0, committed title andfit_paramsmetadata sane).tests/ts): updateregistry.test.ts(catalogue length / new entries) and cover theneeds_peaksbranch wiring; run the 🟢 Vitest (TS) task (mandatory pre-completion check).tests/e2e/_repro_*.spec.tsor a permanent regression spec per the testing strategy).Acceptance criteria
A##) and one width (σ##) per detected peak plus a single shared baselineY0, with peak centres held fixed at the detected positions.fit_paramsmetadata reflect the fit (matching the desktop convention).npm run i18n:checkpasses.References
DataLab/datalab/gui/processor/signal.py(compute_multigaussianfit,compute_multilorentzianfit)DataLab/datalab/widgets/signalpeak.py(SignalPeakDetectionDialog)DataLab/datalab/widgets/fitdialog.py(multigaussian_fit,multilorentzian_fit)Sigima/sigima/tools/signal/fitting.py(BaseMultiPeakFitComputer,MultiGaussianFitComputer,MultiLorentzianFitComputer)Sigima/sigima/tools/signal/peakdetection.py(peak_indices)DataLab-Web/src/runtime/dlw_interactive_fit.pyDataLab-Web/src/runtime/runtime.tsDataLab-Web/src/components/InteractiveFitDialog.tsx,DataLab-Web/src/App.tsx,DataLab-Web/src/actions/registry.ts