Feature request — Refactor sigimax.widgets.fitdialog for parameter injection and UI/computation separation
Is your feature request related to a problem? Please describe.
The interactive fit dialogs in sigimax/widgets/fitdialog.py cannot be re-opened with a known set of parameter values. Every per-type function (gaussian_fit, lorentzian_fit, voigt_fit, multigaussian_fit, …) unconditionally:
- re-estimates initial parameters from the data (
FitComputer.compute_initial_params()),
- runs
win.autofit() on opening — guifit() even carries a # TODO: [P3] make this optional on that exact line.
So a client that already owns a canonical fit_params dictionary (as produced by sigima.tools.signal.fitting.create_fit_params) has no way to display the fit window seeded with those values. The motivating case is DataLab's history panel: replaying an interactive fit in edit mode must re-open the same fit window on the source signal, starting exactly from the previously recorded parameters. Today the only options are to duplicate the whole FitDialog construction outside fitdialog.py, or to fork each per-type function — both fragile whenever fitdialog.py evolves.
This limitation has already been hit — and worked around — twice in DataLab's history panel branch (feature/history-panel):
- DataLab-Platform/DataLab@877f197 (C1 — headless deterministic fit evaluator): a GUI-free
evaluate_fit(fit_type, x, values, extra) plus a FIT_TYPE_BY_DLGFUNC mapping had to be added inside DataLab's copy of fitdialog.py, re-dispatching by hand to pulse.GaussianModel.func, np.polyval, multigaussian, … — i.e. re-extracting from the dialog the computation layer that should live in Sigima. Its ad-hoc interface (positional values list ordered "as produced by the interactive fit function", extra={"a_x0": ...} for multi-peak structure) is exactly what the canonical fit_params format replaces.
- DataLab-Platform/DataLab@c26c2fb (C2/C3 — record interactive fits as deterministic replay actions): on top of C1, interactive fits are recorded and replayed headlessly via a
recompute_fit processor method — without reopening the dialog, because seeding it is impossible. Coverage had to be restricted to 7 fit types (polynomial, linear, gaussian, lorentzian, voigt, multigaussian, multilorentzian); all other interactive fits remain "interactive-only" and cannot be replayed. Edit-mode replay (reopening the seeded window) remained out of reach and later required rebuilding the whole dialog outside fitdialog.py.
The module also mixes three concerns inside each of the 13 per-type functions:
- Computation: model evaluation (
pulse.GaussianModel.evaluate, fitting.PlanckianFitComputer.evaluate, local modelfunc closures, …) and initial estimation (Sigima FitComputer classes).
- UI presentation: translated labels and symbols (
_("Std-dev") + " (σ)", "σ" + stri), number format, param_cols/winsize layout choices.
- Bounds heuristics: hand-tuned per function, with hard-earned fixes scattered as comments — symmetric amplitude bounds ("a
(0, guess * 10) range silently inverts into an empty interval whenever the guess is negative"), free-sign rates ("forcing b_left > 0 … makes the opposite shape unreachable"), abscissa-based sigma bounds in cdf_fit, dx * 0.001 floors in twohalfgaussian_fit, etc.
The result is heavy duplication (gaussian_fit/lorentzian_fit/voigt_fit are near copies; multigaussian_fit/multilorentzian_fit likewise) and per-type knowledge that cannot be reused: UI↔canonical conversions (sinusoidal_fit edits the phase in degrees but stores radians) and fit-type name mappings (piecewiseexponential_fit stores "doubleexponential") are buried inside function bodies.
Describe the solution you'd like
Split the module into three layers, keeping the existing public API as thin wrappers:
-
Computation stays in Sigima (single source of truth). All fitfunc closures should delegate to sigima.tools.signal.fitting.evaluate_fit(x, **fit_params) (or the corresponding FitComputer.evaluate), and validation to validate_fit_params. fitdialog.py should not re-implement any model (e.g. the local modelfunc in exponential_fit, sinusoidal_fit, cdf_fit).
-
A declarative per-type UI spec — a registry mapping each canonical fit_type to its parameter presentation:
- translated label + symbol (e.g. "Std-dev (σ)", "A01"),
- number format (
DEFAULT_FORMAT), optional logscale,
- a bounds strategy computed from
(x, y, value) — e.g. x0-like → abscissa range; sigma-like → strictly positive floor (as enforced by validate_fit_params, which rejects non-positive sigma/sigma_*); amplitude-like → symmetric, data-range based,
- optional UI↔canonical value converters (degrees↔radians for the sinusoidal phase) and the canonical fit-type name (
"doubleexponential" for the piecewise exponential dialog),
- data-driven parameter generation for multi-peak types (
amplitude_i/sigma_i/x0_i per peak), which the canonical fit_params format already supports.
-
One generic entry point with parameter injection:
def interactive_fit(
x, y, fit_type, *,
initial_params: dict | None = None, # canonical fit_params values
run_autofit: bool | None = None, # default: initial_params is None
parent=None, wintitle=None, name=None, **type_specific_kwargs,
) -> tuple[np.ndarray, list[FitParam], dict] | None:
initial_params=None → current behaviour: estimate via FitComputer + autofit.
initial_params={...} → seed the sliders with the given values and skip the opening autofit (the auto-fit button stays available).
- Returns the canonical
(y_fitted, params, fit_params) triple, making the dialog a round-trip on fit_params: fit_params in → dialog → fit_params out.
As a first, independent step, guifit() gains the long-promised run_autofit: bool = True keyword (default keeps current behaviour for all existing call sites and resolves the TODO: [P3]). The 13 per-type functions in __all__ remain as thin backward-compatible wrappers around interactive_fit.
Describe alternatives you've considered
- Rebuilding the dialog outside
fitdialog.py (current DataLab workaround for the history panel): duplicates the FitDialog construction (options, icon, do_autoscale, param_cols wiring) and the bounds heuristics; drifts silently when fitdialog.py changes.
- Adding a seeding keyword to each of the 13 per-type functions: 13 signature changes, still leaves the duplication and the UI/computation coupling in place.
- Patching
guifit only (run_autofit flag): minimal and useful as a first step, but callers would still have to rebuild the FitParam lists themselves to seed values, so the per-type UI spec (layer 2) is what actually removes the duplication.
Additional context
- Prior explorations in DataLab (linked above): 877f197 proved the computation layer can be cleanly separated from the dialog (headless evaluator), and c26c2fb proved deterministic record/replay works end-to-end on top of it — but both had to live in DataLab's fork of
fitdialog.py, with per-type dispatch duplicated and a hard 7-type coverage limit. This proposal upstreams those findings: layer 1 generalises the C1 evaluator through sigima...evaluate_fit + canonical fit_params, and layer 3 removes the C2/C3 restriction that replay must stay headless.
- Motivating client: DataLab history panel, edit-mode replay of interactive fits. The recorded state is the canonical
fit_params dict (create_fit_params(fit_type, values, residual_rms=..., interactive=True) via create_interactive_fit_params); replay must re-open the same window seeded with those values (no autofit on opening) and return an updated canonical dict on acceptance.
- Constraint learned the hard way: seeded lower bounds must respect Sigima validation —
sigma/sigma_* must stay strictly positive for peak fit types, otherwise every slider refresh raises ValueError: Non-positive fit widths through evaluate_fit. Centralising bounds strategies in the UI spec would encode this once, next to the other bounds fixes currently documented only as comments in individual functions.
- The canonical
fit_params format (metadata keys fit_type, residual_rms, fit_params_version, peak_parameterization, interactive) already provides everything the generic entry point needs, including generated multi-peak parameter names.
Feature request — Refactor
sigimax.widgets.fitdialogfor parameter injection and UI/computation separationIs your feature request related to a problem? Please describe.
The interactive fit dialogs in
sigimax/widgets/fitdialog.pycannot be re-opened with a known set of parameter values. Every per-type function (gaussian_fit,lorentzian_fit,voigt_fit,multigaussian_fit, …) unconditionally:FitComputer.compute_initial_params()),win.autofit()on opening —guifit()even carries a# TODO: [P3] make this optionalon that exact line.So a client that already owns a canonical
fit_paramsdictionary (as produced bysigima.tools.signal.fitting.create_fit_params) has no way to display the fit window seeded with those values. The motivating case is DataLab's history panel: replaying an interactive fit in edit mode must re-open the same fit window on the source signal, starting exactly from the previously recorded parameters. Today the only options are to duplicate the wholeFitDialogconstruction outsidefitdialog.py, or to fork each per-type function — both fragile wheneverfitdialog.pyevolves.This limitation has already been hit — and worked around — twice in DataLab's history panel branch (
feature/history-panel):evaluate_fit(fit_type, x, values, extra)plus aFIT_TYPE_BY_DLGFUNCmapping had to be added inside DataLab's copy offitdialog.py, re-dispatching by hand topulse.GaussianModel.func,np.polyval,multigaussian, … — i.e. re-extracting from the dialog the computation layer that should live in Sigima. Its ad-hoc interface (positionalvalueslist ordered "as produced by the interactive fit function",extra={"a_x0": ...}for multi-peak structure) is exactly what the canonicalfit_paramsformat replaces.recompute_fitprocessor method — without reopening the dialog, because seeding it is impossible. Coverage had to be restricted to 7 fit types (polynomial, linear, gaussian, lorentzian, voigt, multigaussian, multilorentzian); all other interactive fits remain "interactive-only" and cannot be replayed. Edit-mode replay (reopening the seeded window) remained out of reach and later required rebuilding the whole dialog outsidefitdialog.py.The module also mixes three concerns inside each of the 13 per-type functions:
pulse.GaussianModel.evaluate,fitting.PlanckianFitComputer.evaluate, localmodelfuncclosures, …) and initial estimation (SigimaFitComputerclasses)._("Std-dev") + " (σ)","σ" + stri), number format,param_cols/winsizelayout choices.(0, guess * 10)range silently inverts into an empty interval whenever the guess is negative"), free-sign rates ("forcingb_left > 0… makes the opposite shape unreachable"), abscissa-based sigma bounds incdf_fit,dx * 0.001floors intwohalfgaussian_fit, etc.The result is heavy duplication (
gaussian_fit/lorentzian_fit/voigt_fitare near copies;multigaussian_fit/multilorentzian_fitlikewise) and per-type knowledge that cannot be reused: UI↔canonical conversions (sinusoidal_fitedits the phase in degrees but stores radians) and fit-type name mappings (piecewiseexponential_fitstores"doubleexponential") are buried inside function bodies.Describe the solution you'd like
Split the module into three layers, keeping the existing public API as thin wrappers:
Computation stays in Sigima (single source of truth). All
fitfuncclosures should delegate tosigima.tools.signal.fitting.evaluate_fit(x, **fit_params)(or the correspondingFitComputer.evaluate), and validation tovalidate_fit_params.fitdialog.pyshould not re-implement any model (e.g. the localmodelfuncinexponential_fit,sinusoidal_fit,cdf_fit).A declarative per-type UI spec — a registry mapping each canonical
fit_typeto its parameter presentation:DEFAULT_FORMAT), optionallogscale,(x, y, value)— e.g.x0-like → abscissa range;sigma-like → strictly positive floor (as enforced byvalidate_fit_params, which rejects non-positivesigma/sigma_*); amplitude-like → symmetric, data-range based,"doubleexponential"for the piecewise exponential dialog),amplitude_i/sigma_i/x0_iper peak), which the canonicalfit_paramsformat already supports.One generic entry point with parameter injection:
initial_params=None→ current behaviour: estimate viaFitComputer+ autofit.initial_params={...}→ seed the sliders with the given values and skip the opening autofit (the auto-fit button stays available).(y_fitted, params, fit_params)triple, making the dialog a round-trip onfit_params:fit_params in → dialog → fit_params out.As a first, independent step,
guifit()gains the long-promisedrun_autofit: bool = Truekeyword (default keeps current behaviour for all existing call sites and resolves theTODO: [P3]). The 13 per-type functions in__all__remain as thin backward-compatible wrappers aroundinteractive_fit.Describe alternatives you've considered
fitdialog.py(current DataLab workaround for the history panel): duplicates theFitDialogconstruction (options, icon,do_autoscale,param_colswiring) and the bounds heuristics; drifts silently whenfitdialog.pychanges.guifitonly (run_autofitflag): minimal and useful as a first step, but callers would still have to rebuild theFitParamlists themselves to seed values, so the per-type UI spec (layer 2) is what actually removes the duplication.Additional context
fitdialog.py, with per-type dispatch duplicated and a hard 7-type coverage limit. This proposal upstreams those findings: layer 1 generalises the C1 evaluator throughsigima...evaluate_fit+ canonicalfit_params, and layer 3 removes the C2/C3 restriction that replay must stay headless.fit_paramsdict (create_fit_params(fit_type, values, residual_rms=..., interactive=True)viacreate_interactive_fit_params); replay must re-open the same window seeded with those values (no autofit on opening) and return an updated canonical dict on acceptance.sigma/sigma_*must stay strictly positive for peak fit types, otherwise every slider refresh raisesValueError: Non-positive fit widthsthroughevaluate_fit. Centralising bounds strategies in the UI spec would encode this once, next to the other bounds fixes currently documented only as comments in individual functions.fit_paramsformat (metadata keysfit_type,residual_rms,fit_params_version,peak_parameterization,interactive) already provides everything the generic entry point needs, including generated multi-peak parameter names.