Skip to content

Refactor sigimax.widgets.fitdialog for parameter injection and UI/computation separation #3

Description

@dappham-CODRA

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:

  1. re-estimates initial parameters from the data (FitComputer.compute_initial_params()),
  2. 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:

  1. 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).

  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions