Spline Fit — Control Systems/Curve Fitting
Control_Systems/Curve_Fitting/Spline_Fit · 1 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.
Spline Fit
Control Systems / Curve Fitting
Fits a B-spline of order k to the last W samples and emits its B-form coefficients:
ŷ(x) = Σj=1..n cj·Bj,k(x | t)
where t is the knot sequence and n = len(t) − k. This is
MATLAB's spap2, and in its second Knot Placement mode it is
spapi. The knots are k-fold at both ends of the window's own
span (MATLAB's augknt), so the basis sums to one across it. The
newest sample sits at x = 0 and the sample i steps back at
x = −i·h, the same abscissa Polynomial Fit,
Fourier Fit and Gaussian Fit use.
Ports
- u – the sampled signal being fitted. Scalar: one channel and its own window – see Notes.
- c – the coefficient column, [n, 1], in the basis's own order. Its height follows Knot Placement: (number of interior knots) + k in the explicit mode, and W in the averaged one.
Parameters
- Window Length – W, how many samples the fit sees. A whole number from 3 to 101, and it must be at least n: n coefficients cannot be determined by fewer than n points. Bounded above because the dot products are unrolled at export.
- Spline Order – k, a whole number from 2 to 6. Order is degree + 1, so 4 – the default, and MATLAB's – is the cubic spline; 2 is the broken line.
- Knot Placement – which of MATLAB's two functions this is:
- Explicit interior knots – you give them, n is
(number of interior knots) + k, and with n < W this is a least-squares fit.
It is
spap2(knots, k, x, y), and it is the default. - Averaged (interpolating) – n = W and the interior knots
are derived, each the average of k−1 consecutive sample positions.
The collocation matrix is then square and the curve passes through every
sample of the window. It is
spapi, and the averaging is MATLAB'saptknt. Interior Knots is ignored here – see Notes for why it must be.
- Explicit interior knots – you give them, n is
(number of interior knots) + k, and with n < W this is a least-squares fit.
It is
- Interior Knots – a strictly increasing vector of positions inside the window's span (−(W−1)·h, 0), in the same units as Abscissa Step. Read only in Explicit interior knots mode, and it must name at least one knot. A spline with no interior knot is a single polynomial piece of degree k−1, which is exactly what Polynomial Fit already computes – reach for that block instead of leaving this one empty.
- Abscissa Step – h, the spacing between consecutive samples on the fitted axis. A single positive number.
- 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.
The n×W coefficient bank is structural and is inlined into the arithmetic at export time rather than exposed as a tunable parameter: it follows from all five settings, and changing any of them changes how many multiplies and how many outputs the core contains. Re-export after changing them. No Cox-de Boor recursion runs in any generated core – the basis was evaluated once, at export time, to build the bank.
The three HDL targets are genuine synthesizable Q16.16: a shift register and one fixed multiply-accumulate per coefficient, with the products accumulated at full width and shifted back once per row.
Simulink bridge
None (Support::None). Fitting a B-spline to a running
window is a Curve Fitting Toolbox function (spap2,
spapi), and that toolbox ships no Simulink library at all, so there
is no path a diagram could name. The bridge reports this block rather than
dropping it silently, and it therefore has no parity testbench; code
export verification still covers it across all ten languages.
Notes
- Stateful, and discrete by nature
(
setDiscreteOnlyBlock(true)): the window advances once per sample. - ⚠ The averaged mode derives its knots, and that is not a convenience. Putting the interior knots on the sample positions is the obvious way to reach n = W and it is the wrong one. Measured at W = 13, k = 4, h = 0.4: averaged knots give cond(A'A) = 15.3 and a largest bank entry of 2.35; knots on the sites give cond(A'A) = 3.6e10 and a bank entry of 1.0e5, which saturates a Q16.16 datapath outright and makes the three HDL targets meaningless. That is why the mode computes them.
- ⚠ Its output is NOT a polynomial, and Evaluate Fit cannot read it. Evaluate Fit, Fit Derivative and Fit Integral speak coefficients in descending powers; these are B-form coefficients, which mean nothing without the knot sequence. Wiring this block into any of the three produces a number rather than an error, so the mistake is silent.
- ⚠ It is not Cubic Spline either, and the difference is which way the data flows. Curve Fitting / Cubic Spline takes a curve as configuration and evaluates it at its input; this block takes a signal and fits a curve to it. Reach for Cubic Spline when the curve is known and you want its value, and for this block when the curve is what you are after.
- One solve, then arithmetic. The normal-equation system is solved once when the configuration loads; every sample afterwards is n fixed dot products.
- ⚠ Not every knot sequence is a fit, and the block refuses rather than guessing. Interior knots that repeat, that fall outside the window's span, or that leave a stretch of k consecutive knots with no sample between them make a basis column vanish or coincide with its neighbour, and the fit has no unique answer. That is detected when the configuration loads and reported with a reason – rather than emitting the arbitrary answer a pseudo-inverse would have produced.
- Verified against MATLAB, at the basis and at the answer. The
Cox-de Boor recursion was diffed entry by entry against R2026a's
spcolat W = 13, k = 4, h = 0.4, interior knots [−3.5 −2.2 −1.1]: all 91 entries of the 13×7 collocation matrix agree exactly – worst difference 0.0, not merely small. The fitted coefficients then reproducespap2's own answer to 2.3e−15 relative at cond(A'A) = 21.4. Solving the normal equations squares the condition number where MATLAB's QR does not, which is the price of collapsing the whole fit into one inlined bank of numbers instead of putting a factorization in ten generated cores. - The window is zero-prefilled, and the zeros count. The first W−1 coefficient vectors of a run are a startup transient, matching Moving Median, Detrend, Savitzky-Golay Filter, Polynomial Fit, Fourier Fit and Gaussian Fit.
- Scalar only. One channel and its own history; wire one block per channel.
- No state space. Linear in the input, but through a fixed filter bank over W past samples rather than an A/B/C/D pair, so model reduction correctly declines to merge it.
Code facts#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Curve_Fitting/Spline_Fit |
| family | Control_Systems/Curve_Fitting |
| solver environment class | ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Spline_Fit |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Spline_Fit/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Spline_Fit.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Spline_Fit/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Spline_Fit.h |
| default size on canvas | 130 × 72 px |
| ports at insert | 1 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 | u |
| 2 | out | ICoreDouble | c |
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 |
|---|---|---|
Window Length | 12 | — |
Spline Order | 4 | — |
Knot Placement | Explicit interior knots%~%Averaged (interpolating)~~Expli… | — |
Interior Knots | [-8 -5 -2] | — |
Abscissa Step | 1 | — |
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): fitting a B-spline to a running window is a Curve Fitting Toolbox function (spap2, and spapi in the averaged mode), not a Simulink library block -- that toolbox ships no Simulink library at all -- so there is no path a diagram could name; the block is reported rather than dropped when a model crosses
Catalog contract: src/ICoreBlocks/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).
Spline Fit -- the least-squares B-spline through the last W samples MATLAB's
spap2(knots, k, x, y)with the knot sequence GIVEN, and at one setting MATLAB'sspapias well:y(x) = SUM(j = 1..n) c_j * B_{j,k}(x | t), n = len(t) - k
The knots are k-fold at both ends of the window's own span (MATLAB's
augknt), so the basis is a partition of unity across it and the first and last coefficients are the curve's values at the two ends. With the knots fixed the fit is LINEAR, so the normal-equation system is solved once per configuration load and every target carries n fixed dot products over one shift register. The solve is ICoreCurveFitBankSupport's, shared with Gaussian_Fit and Sum_Of_Sines_Fit.⚠ THE BASIS IS TRANSCRIBED AND THEN DIFFED AGAINST MATLAB'S OWN, NOT INFERRED. The Cox-de Boor recursion below was checked entry by entry against R2026a's
spcolat W = 13, k = 4, h = 0.4 and interior knots [-3.5 -2.2 -1.1]: all 91 entries of the 13x7 collocation matrix agree EXACTLY -- worst difference 0.0, not merely small. The fitted coefficients then reproducespap2's own answer to 2.3e-15 relative, at cond(A'A) = 21.4.The right endpoint is the one place a naive recursion is wrong: at x = t[end] every basis function evaluates to zero unless the interval search is clamped to the last non-trivial span.
spcolreports [0 ... 0 1] there, and so does this.⚠ AND THE INTERPOLATING MODE DERIVES ITS KNOTS FOR A MEASURED REASON. Setting n = W makes the collocation matrix square and the fit exact, but only if the knots are placed by AVERAGING k-1 consecutive sites (MATLAB's
aptknt). Measured at W = 13, k = 4, h = 0.4:averaged knots cond(A'A) = 15.3 largest bank entry 2.35 knots on the sites cond(A'A) = 3.6e10 largest bank entry 1.0e5
The second is the obvious choice and it saturates a Q16.16 datapath outright. So the interpolating mode computes its own knots rather than letting them be typed.
⚠ NO COX-DE BOOR RECURSION SURVIVES INTO A GENERATED CORE. The basis is evaluated once, here, to build the bank; the ten targets carry the resulting numbers and nothing else, which is what makes the three HDL targets genuine synthesizable Q16.16.
Sample results#
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) | -0.09925 … 0.979 |
ramp | Ramp: slope 1 from t = 0 | -0.002922 … 4.7 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -0.9962 … 0.9996 |
table | Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample | -2.086 … 2.813 |
Plotted: step — Step: 0 -> 1 at t = 1 s
Category dynamic · sample time 0.1 · 60 steps · commit a679b46f8b39b82301c97f824f6deab352672b98 · produced by docsSample --out <folder> --blocks Spline_Fit --steps 60 · data docs/generated/samples/Control_Systems__Curve_Fitting__Spline_Fit.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).