Generated reference › Spline Fit — Control Systems/Curve Fitting
kind: generated#block#control-systems-curve-fitting

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's aptknt. Interior Knots is ignored here – see Notes for why it must be.
  • 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 spcol at 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 reproduce spap2'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#

FactValue
registered typeControl_Systems/Curve_Fitting/Spline_Fit
familyControl_Systems/Curve_Fitting
solver environment classICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Spline_Fit
sourcesrc/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Spline_Fit/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Spline_Fit.cpp
headersrc/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Spline_Fit/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Spline_Fit.h
default size on canvas130 × 72 px
ports at insert1 in, 1 out
code generators implementedPython, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text

Ports#

#DirectionSignal typeDescription label
1inICoreDoubleu
2outICoreDoublec

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 variableDefaultSimulink parameter
Window Length12—
Spline Order4—
Knot PlacementExplicit interior knots%~%Averaged (interpolating)~~Expli…—
Interior Knots[-8 -5 -2]—
Abscissa Step1—

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.

supportSupport::None
Simulink path—
port-count rulePortsParam::None
SampleTime parameteryes

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's spapi as 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 spcol at 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 reproduce spap2'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. spcol reports [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#

Spline Fit — Step: 0 -> 1 at t = 1 sSpline Fit — Step: 0 -> 1 at t = 1 s00.51012345t (s)in ICoreDouble-Out-0out ICoreDouble-Out-0 [7x1] entry 0

The same rig also ran:

StimulusWhat it isOutput range
impulseImpulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair)-0.09925 … 0.979
rampRamp: slope 1 from t = 0-0.002922 … 4.7
sineSine Wave: amplitude 1, 2 rad/s, no phase, no bias-0.9962 … 0.9996
tableRepeating 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).