Model Fit Monitor — System Identification/Validation
System_Identification/Validation/Model_Fit_Monitor · 2 input / 1 output port(s) at insert · exports to Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text
Description#
The block's own DESCRIPTION_HTML, rendered verbatim — the same text the config dialog's info panel and the library navigator show. Fix a wrong sentence in the block's .cpp (R-D9), never here.
Model Fit Monitor
System Identification / Validation
Reports how well a model's output tracks the signal it is meant to predict, as a running NRMSE fit updated every sample:
fit = 1 − √(Se ÷ (Sy + ε)), where Se is the exponentially weighted energy of the error y − ŷ and Sy the exponentially weighted variance of y about its own running mean.
- 1 – the model reproduces y exactly.
- 0 – the model is no better than predicting y's own mean.
- Negative – the model is worse than that. This is deliberately not clamped: it is the most useful thing the block can tell you, and a floor at zero would hide it.
Because the sums are exponentially weighted rather than taken over the whole run, the number follows the model live: a plant that drifts away from its identified model shows the fit falling on the canvas while the simulation is still running, which a fit computed at the end cannot do.
Ports
- y – the measured signal, a scalar. This is the reference: the denominator is its running variance, so it is not interchangeable with the other port.
- yhat (ŷ) – the model's output for the same instant, a scalar.
- fit – the running fit, a scalar, as a fraction (not the
percentage MATLAB's
compareprints – multiply by 100).
Parameters
- Forgetting Factor – λ in (0, 1]. The memory is roughly 1 ÷ (1 − λ) samples: 0.98 averages over about 50, 0.9 over about 10. Smaller reacts faster and is noisier; 1 makes it an ordinary cumulative fit over the whole run with no forgetting at all.
- Epsilon – a floor on the variance divisor, not a branch. A constant y has zero running variance, and without this the fit would divide by zero the moment a signal settles. Keep it small relative to the variance you expect; it biases the fit slightly upward.
- Initial Reference Mean – the seed for y's running mean. It decays with λ like any other sample, so it matters only for the first few multiples of the memory; set it to y's expected level to stop the fit reading low while the mean converges from zero.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text. λ, ε and the seed are baked into the emitted arithmetic rather than exposed as tunable parameters: they define what the number means, so retuning one on the target would silently change the metric rather than the model.
The three HDL targets are simulation-only real
arithmetic. A square root and a per-sample division do not belong in a Q16.16
datapath, and this block needs both.
Simulink bridge
None. Simulink has no block that reports a running fit – the
NRMSE conventions this follows live in System Identification Toolbox
functions (goodnessOfFit, compare), which run
after a simulation over a whole logged signal rather than inside one, and which
no ParamRule could carry across the bridge in any case. The block
therefore has no parity testbench, which is the documented consequence of
Support::None rather than a gap.
Notes
- Stateful, with three scalars: the running mean of y and the two weighted sums. It is a discrete-only block – a fit has no derivative to integrate.
- The mean is updated before it is used. Sy takes the deviation from the new mean, not the previous one. The two conventions differ by order (1 − λ) every sample.
- The output is the current sample's fit, not the previous one: the sums are advanced and then published, which is the opposite order to the filter blocks.
- The fit is a ratio of two weighted sums, so the weighting constants cancel and no normalisation by the sample count is needed – which is also why a warm-up period is visible in the number rather than hidden by one.
Code facts#
| Fact | Value |
|---|---|
| registered type | System_Identification/Validation/Model_Fit_Monitor |
| family | System_Identification/Validation |
| solver environment class | ICoreBlock_0_System_Identification_1_Validation_2_Model_Fit_Monitor |
| source | src/ICoreSDK/ICoreBlockLibrary/Blocks/System_Identification/Validation/Model_Fit_Monitor/ICoreBlock_0_System_Identification_1_Validation_2_Model_Fit_Monitor.cpp |
| header | src/ICoreSDK/ICoreBlockLibrary/Blocks/System_Identification/Validation/Model_Fit_Monitor/ICoreBlock_0_System_Identification_1_Validation_2_Model_Fit_Monitor.h |
| default size on canvas | 128 × 80 px |
| ports at insert | 2 in, 1 out |
| code generators implemented | Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text |
Ports#
| # | Direction | Signal type | Description label |
|---|---|---|---|
| 1 | in | ICoreDouble | y |
| 2 | in | ICoreDouble | yhat |
| 3 | out | ICoreDouble | fit |
Ports the constructor creates. A block whose port list changes with its configuration adds or removes ports at load time; the count above is the one a freshly inserted block has.
Configuration variables#
| Config variable | Default | Simulink parameter |
|---|---|---|
Forgetting Factor | 0.98 | — |
Epsilon | 1e-12 | — |
Initial Reference Mean | 0 | — |
Every block also carries Sampling Time (s) from ICoreBlockSolverEnvironment: zero or less inherits the solver's rate, a positive value runs the block at that period.
Simulink bridge#
| support | Support::None |
| Simulink path | — |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
Caveat (shown to the user): no Simulink equivalent: it has no block that reports a running model fit. The NRMSE conventions this follows are System Identification Toolbox FUNCTIONS (goodnessOfFit, compare) that run after a simulation over a whole logged signal rather than inside one, so there is nothing for a ParamRule to map onto
Catalog contract: src/ICoreSDK/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The lists agree. check_block_descriptions.py finds no disagreement between the description's Ports, Parameters, Code export and Simulink bridge lists and the code's.
The verdict above is
tools/docs/check_block_descriptions.py(P7.1), which compares LISTS. It cannot read a sentence: "stateless" on a block with a state, an initial-value semantic the recursion does not implement, a "not synthesizable" caveat the HDL banner contradicts. That is the agent audit (P7.3) on BLOCK_DESCRIPTION_AUDIT.md, and this tool's green is not a substitute for one.
File banner (developer view)#
The top comment of the block's .cpp — the maths, the realization and the export strategy, addressed to whoever changes it. It must not contradict the description above (P7.5).
Model Fit Monitor — the running NRMSE fit between a measurement and a model's output m[k] = L*m[k-1] + (1-L)*y[k] Se[k] = L*Se[k-1] + (y[k] - yhat[k])^2 Sy[k] = L*Sy[k-1] + (y[k] - m[k])^2 fit = 1 - sqrt( Se[k] / (Sy[k] + eps) )
Three scalar states and no window, so a drifting model shows up live on the canvas rather than at the end of a run. The recursion is written ONCE in four *Expr() members and each generator supplies only a Dialect -- the update ORDER (mean first) and the floored divisor are the two things eleven evaluators have to agree on, and neither is visible in a diff.
The three HDL targets are simulation-only
real: a square root and a per-sample division have no place in a Q16.16 datapath.
Sample results#
| t | in ICoreDouble-Out-0 | in ICoreDouble-Out-0 | out ICoreDouble-Out-0 |
|---|---|---|---|
| 0 | -2 | -2 | 1 |
| 0.4 | 0.5 | 0.5 | 1 |
| 0.8 | -2 | -2 | 1 |
| 1.2 | 0.5 | 0.5 | 1 |
| 1.6 | -2 | -2 | 1 |
| 2 | 0.5 | 0.5 | 1 |
| 2.4 | -2 | -2 | 1 |
| 2.8 | 0.5 | 0.5 | 1 |
| 3.2 | -2 | -2 | 1 |
| 3.6 | 0.5 | 0.5 | 1 |
| 4 | -2 | -2 | 1 |
| 4.4 | 0.5 | 0.5 | 1 |
| 4.8 | -2 | -2 | 1 |
| 5.2 | 0.5 | 0.5 | 1 |
Every 4th of 60 samples, from the table stimulus.
The same rig also ran:
| Stimulus | What it is | Output range |
|---|---|---|
impulse | Impulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair) | 1 … 1 |
ramp | Ramp: slope 1 from t = 0 | 1 … 1 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | 1 … 1 |
step | Step: 0 -> 1 at t = 1 s | 1 … 1 |
Plotted: table — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample
Category static · sample time 0.1 · 60 steps · commit ccf005c8 · produced by docsSample --out <folder> --steps 60 · data docs/generated/samples/System_Identification__Validation__Model_Fit_Monitor.json · the SVG is generated from those numbers by tools/docs/plot_svg.py, so it is a run and not a drawing (R-D10).