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 type | TCurveClass (class of TNamedPointsSet) | RegisterCurveType | 13 |
| Data loader | TDataLoaderClass + extensions + format name | RegisterDataLoader | 2 |
| Minimizer | TMinimizerInfo (with a TBackendFactory) | RegisterMinimizer | 2 |
| Loss function | TLossInfo (evaluate + pool) | RegisterLoss | 4 |
| REST action | TActionInfo (with a TActionHandler) | RegisterAction | 14 |
| Server module | IAppModule | RegisterAppModule | 0 — module-only seam |
| UI module | IUiModule | RegisterUiModule | 0 — module-only seam |
| Python sidecar pack | a package name | RegisterSidecarModule | 0 — module-only seam |
| Curve builder | point set name -> TCurveBuilder | RegisterCurveBuilder | 0 — module-only seam |
| Chart overlay | TModuleOverlayProc | RegisterModuleOverlay | 0 — 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) | yes | yes | yes | — |
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-factor | — | yes | yes |
| R-factor (legacy) | yes | — | — |
| Sum of squares | — | yes | yes |
| Relative deviation | — | — | yes |
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-difference | Fit the current model to the profile. | yes |
minimize-difference-again | Continue fitting from where the last fit stopped. | yes |
minimize-number-of-curves | Fit, then drop curves that do not earn their place. | yes |
do-all-automatically | Background, positions and fit, in one pass. | yes |
smooth-profile | Smooth the experimental profile. | yes |
compute-curve-bounds | Work out where each curve begins and ends. | yes |
compute-background-points | Propose background points from the profile. | yes |
compute-curve-positions | Propose a curve position for each peak found. | yes |
select-all-points-as-curve-positions | Use every profile point as a curve position. | yes |
select-entire-profile | Fit over the whole profile rather than a marked interval. | yes |
create-curve-list | Rebuild the curve list from the current model. | — |
stop | Stop the operation now, keeping what it has reached. | — |
subtract-background | Subtract the background from the profile. | — |
select-profile-interval | Restrict the fit to the interval between start and stop. | — |