Fit

Interactive curve fitting for Linux, Windows and macOS

Architecture

Everything on this page is generated from the registries this build actually contains, so it cannot describe a version of the code that no longer exists.

Three processes

The client holds no fitting engine at all. fit_server is the only client-facing endpoint; the Python sidecar is a child process it owns and never something a client talks to directly.

flowchart LR
    client["`**Desktop client**
no fitting engine at all`"]
    server["`**fit_server**
the only client-facing endpoint`"]
    sidecar["`**Python sidecar**
child process of fit_server`"]
    remote["`**Another fit_server**
optional`"]
    client -- "HTTP + JSON" --> server
    server -- "spawns, owns" --> sidecar
    server -. "may delegate" .-> remote

Client and server

The client sends a request and polls; the server never calls it back. That is the whole of the wire protocol, and it is why the desktop can be closed and reopened while a problem is still on the server.

flowchart LR
    subgraph desktop["Desktop client - Fit.lpr"]
        direction TB
        form["`**TFormMain**
the UI, and nothing else`"]
        client["`**TFitClient**
RunAsync hands a blocking call
to TServerCallThread`"]
        http["`**THttpFitService**
implements IFitService`"]
        form --> client --> http
    end
    subgraph server["fit_server.lpr - the only client-facing endpoint"]
        direction TB
        rest["`**TFitRestApi**
verbs from the action registry`"]
        reg["`**TSessionRegistry**
one problem, one session`"]
        session["`**TFitSession**
implements IClientCallback
and records the progress reported to it`"]
        service["`**TFitService**
one TFitTask per fit interval`"]
        task["`**TFitTask**
sums the curves, evaluates the objective`"]
        rest --> reg --> session --> service --> task
    end
    http -- "HTTP + JSON" --> rest
    service -. "IClientCallback" .-> session
    http -. "polls /state, /async, /stats" .-> rest

Watching a fit run

Progress travels in two hops that never meet. Inside the server the minimizer reports each accepted step up through the task to the session, which records it; the client asks for that record on its own timer. A REST session therefore owns the synchronous TFitService, whose RecreateMainCalcThread runs the work inline — a headless server has no UI thread to pump, so a callback marshalled with Synchronize would never arrive.

sequenceDiagram
    autonumber
    participant ui as TFormMain
    participant client as TFitClient
    participant api as TFitRestApi
    participant session as TFitSession
    participant service as TFitService
    participant task as TFitTask
    participant min as TMinimizer
    ui->>client: start the fit
    client->>api: POST the asynchronous verb
    api->>session: run it under the session lock
    session->>service: MinimizeDifference
    service->>task: RecreateMainCalcThread runs the task inline
    task->>min: Minimize, with OnShowCurMin bound to ShowCurMin
    loop every accepted step
        min-->>task: OnShowCurMin
        task->>service: ServerShowCurMin, the R-factor recomputed
        service->>session: ShowCurMinInternal, then IClientCallback
    end
    service->>session: Done
    par the client is not called back
        loop while the operation is running
            ui->>client: TimerCheckState
            client->>api: GET /state, /async, /stats
            api-->>client: the values the session recorded
        end
    end
    client->>ui: OnAsyncOperationFinished
    note over service,task: TFitServiceWithThread and TFitTaskWithThread are the threaded subclasses, reporting through TMainCalcThread and Synchronize. A headless server never pumps that, so the REST session deliberately owns a plain TFitService.

The view seam

The client holds one IFitViewer and names no visual class at all, so the fitting logic links and tests without a widgetset. A module draws through the same seam, describing its series in the framework's own vocabulary rather than the charting component's — which is what lets that component be replaced without touching a module.

flowchart LR
    client["`**TFitClient**
holds one IFitViewer
and names no visual class`"]
    iface["`**IFitViewer**
the view seam`"]
    viewer["`**TFitViewer**
the only place the charting component is known`"]
    chart["`**Chart series, legend, grids**`"]
    module["`**A module's presenter**
PlotModuleSeries, ShowModulePanel`"]
    vocab["`**TModuleSeriesStyle**
the framework's vocabulary, not the chart's`"]
    client --> iface
    iface -. "implemented by" .-> viewer
    viewer --> chart
    module --> iface
    module --> vocab
    vocab --> iface

The extension seams

Fit is a framework as much as an application. A new curve type, data loader, engine, objective, REST verb or whole analysis vertical is added by registration — a directory, plus one entry on a project's unit search path. No framework file changes.

flowchart LR
    subgraph yours["Your module - one directory"]
        direction TB
        code["`**Your units**`"]
        door["`**The front door**
one exported procedure`"]
        code --> door
    end
    subgraph fw["The framework - unedited"]
        direction TB
        curve_types["`**Curve type**
RegisterCurveType
13 registered`"]
        data_loaders["`**Data loader**
RegisterDataLoader
2 registered`"]
        minimizers["`**Minimizer**
RegisterMinimizer
2 registered`"]
        losses["`**Loss function**
RegisterLoss
4 registered`"]
        actions["`**REST action**
RegisterAction
14 registered`"]
        app_modules["`**Server module**
RegisterAppModule
0 registered`"]
        ui_modules["`**UI module**
RegisterUiModule
0 registered`"]
        sidecar_packs["`**Python sidecar pack**
RegisterSidecarModule
0 registered`"]
        curve_builders["`**Curve builder**
RegisterCurveBuilder
0 registered`"]
        module_overlays["`**Chart overlay**
RegisterModuleOverlay
0 registered`"]
    end
    door --> curve_types
    door --> data_loaders
    door --> minimizers
    door --> losses
    door --> actions
    door --> app_modules
    door --> ui_modules
    door --> sidecar_packs
    door --> curve_builders
    door --> module_overlays
    classDef empty stroke-dasharray: 4 3;
    class app_modules,ui_modules,sidecar_packs,curve_builders,module_overlays empty;
Seam You write Entry point Registered here
Curve typeTCurveClass (class of TNamedPointsSet)RegisterCurveType13
Data loaderTDataLoaderClass + extensions + format nameRegisterDataLoader2
MinimizerTMinimizerInfo (with a TBackendFactory)RegisterMinimizer2
Loss functionTLossInfo (evaluate + pool)RegisterLoss4
REST actionTActionInfo (with a TActionHandler)RegisterAction14
Server moduleIAppModuleRegisterAppModule0 — module-only seam
UI moduleIUiModuleRegisterUiModule0 — module-only seam
Python sidecar packa package nameRegisterSidecarModule0 — module-only seam
Curve builderpoint set name -> TCurveBuilderRegisterCurveBuilder0 — module-only seam
Chart overlayTModuleOverlayProcRegisterModuleOverlay0 — module-only seam

The 5 seams shown dashed — Server module, UI module, Python sidecar pack, Curve builder, Chart overlay — have nothing registered in this build. That is not disuse: they exist for modules, and the public framework deliberately ships none. They are listed precisely because an extender needs to know they are there and unclaimed.

Anatomy of a module

A module is a directory. It wins by being first on the unit search path: Free Pascal resolves app_modules and module_tests to the module's copies instead of the framework's stubs, so the framework calls into the module without naming it.

flowchart TB
    subgraph sp["The project's unit search path, in order"]
        direction TB
        first["`**1. Modules/your-module/**`"]
        rest["`**2. Desktop/, Server/, Common/**
and tests/no-modules/`"]
    end
    yours["`**your-module/app_modules.pas**
RegisterAppModules calls your front door`"]
    stub["`**Common/app_modules.pas**
the stub: does nothing`"]
    ytests["`**your-module/module_tests.pas**
uses your testcase units`"]
    stests["`**tests/no-modules/module_tests.pas**
the stub: empty`"]
    host["`**The client, server and test runner**
all call RegisterAppModules`"]
    first --> yours
    first --> ytests
    rest -.-> stub
    rest -.-> stests
    yours --> host
    ytests --> host
    classDef soft stroke-dasharray: 4 3;
    class stub,stests soft;

The trap this design has to guard. A unit nobody uses is never linked, so its initialization never runs and its curve type silently does not exist. That is what ExpectCurveTypes is for: a pack declares the types it must have, and start-up fails naming every missing class rather than running with a shorter menu.

Engines and backends

A backend is never registered directly. A minimizer declares a factory, and the factory decides what can actually run here — returning nil when it cannot, which is not a failure: the caller falls back to the default engine, which is why the application still fits with no Python installed at all.

flowchart LR
    info["`**TMinimizerInfo**
the engine, and what it can do`"]
    factory["`**CreateBackend**
TBackendFactory`"]
    ctx["`**TBackendContext**
PythonUrl, ServerUrl`"]
    native["`**TNativeFitBackend**
in process`"]
    python["`**TPythonFitBackend**
the sidecar`"]
    remote["`**TServerFitBackend**
another fit_server`"]
    fallback["`**nil - not available here**
caller falls back`"]
    info --> factory
    ctx --> factory
    factory --> native
    factory --> python
    factory --> remote
    factory --> fallback
    classDef soft stroke-dasharray: 4 3;
    class fallback soft;
Engine Needs a formula Needs the sidecar Weighting Curve scaling
Downhill Simplex (native)yes
Levenberg-Marquardt (Python/lmfit)yesyesyes

Objectives

What “best fit” means. Whether an objective may be used with a curve type is derived from these two flags — a self-normalising objective can be reduced by inflating a model whose amplitude is free, so that one pairing is refused. No table of type names is involved.

Objective Self-normalising Least squares Poolable across intervals
R-factoryesyes
R-factor (legacy)yes
Sum of squaresyesyes
Relative deviationyes

The REST API

fit_server answers 14 verbs. An asynchronous one starts work that outlives the request, so a caller polls rather than waiting.

Verb Does Asynchronous
minimize-differenceFit the current model to the profile.yes
minimize-difference-againContinue fitting from where the last fit stopped.yes
minimize-number-of-curvesFit, then drop curves that do not earn their place.yes
do-all-automaticallyBackground, positions and fit, in one pass.yes
smooth-profileSmooth the experimental profile.yes
compute-curve-boundsWork out where each curve begins and ends.yes
compute-background-pointsPropose background points from the profile.yes
compute-curve-positionsPropose a curve position for each peak found.yes
select-all-points-as-curve-positionsUse every profile point as a curve position.yes
select-entire-profileFit over the whole profile rather than a marked interval.yes
create-curve-listRebuild the curve list from the current model.
stopStop the operation now, keeping what it has reached.
subtract-backgroundSubtract the background from the profile.
select-profile-intervalRestrict the fit to the interval between start and stop.