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

Thin Plate Spline — Control Systems/Curve Fitting

Control_Systems/Curve_Fitting/Thin_Plate_Spline · 3 input / 2 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.

Thin-Plate Spline

Control Systems / Curve Fitting

Fits the smoothing thin-plate spline to the last W scattered samples (x, y, z) and reads the surface back at the newest site and at fixed query points:

f(x, y) = Σj cj·φ(|(x, y) − sj|²) + ax·x + ay·y + a0,   φ(r²) = r²·log(r²)

with the coefficients chosen by the smoothing system (K + λ·I)·c + P·a = z, P'·c = 0, where Kij = φ(|si − sj|²), P = [x y 1] and λ = (1 − p)/p. This is MATLAB's tpaps([x; y], z, p) read back by fnval, and p means what it means there: 1 interpolates every site, 0 is the least-squares plane, and anything between trades closeness for bending energy.

Ports

  • x – the first coordinate of the current site, a scalar.
  • y – the second coordinate of the current site, a scalar.
  • z – the value measured at that site, a scalar. The block keeps the last W triples (x, y, z) itself.
  • zf – the fitted surface at the newest site, f(x, y), [1, 1]: the smoothed version of z. At p = 1 it equals z.
  • zq – the fitted surface at each column of Query Points, [Q, 1], Q the number of query points, in their order.

Parameters

  • Window Length – W, how many of the most recent sites the surface is fitted to. A whole number from 4 to 24: three sites already determine a plane and leave the spline part nothing to do.
  • Smoothing Parameter Choice – where p comes from:
    • Automatic (tpaps default) – recomputed every sample from the window's own sites, by tpaps's own rule when it is called without p: p = 1/(1 + mean(diag(Q2'·K·Q2))), where the columns of Q2 span the vectors orthogonal to P. The default.
    • Given – Smoothing Parameter is used as it stands.
  • Smoothing Parameter – p, a single number from 0 to 1, read only when the choice is Given. p = 0 fits the least-squares plane through the window and nothing else, which is what tpaps does with it. At p = 1 the surface interpolates every site – see Notes for what that costs when two sites come close.
  • Query Points – where zq reads the surface: a matrix of two rows, one column per point [x; y], 1 to 8 columns – the same shape fnval takes for a bivariate spline. The default, [0; 0], is one point at the origin.
  • 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. Every target keeps the window itself and, every sample, builds the kernel matrix (W² logarithms), solves the (W+3)×(W+3) bordered system by Gaussian elimination with partial pivoting, and evaluates the surface (W logarithms per point). The window length, the choice of p, a given p and the query points are all structural: they are inlined at export time rather than exposed as tunable parameters, so re-export after changing them.

The three HDL targets are simulation-only: the solve and the logarithm run in real arithmetic and only the ports are Q16.16. A dense solve whose matrix is built from the data every sample, with a logarithm in every entry, has no fixed-point form that would keep its accuracy, so these cores are offered for simulation and not for synthesis.

Simulink bridge

None (Support::None). The thin-plate smoothing spline is a Curve Fitting Toolbox function (tpaps), 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.
  • ⚠ Both outputs are 0 whenever the fit is undefined, rather than the arbitrary numbers a near-singular solve would produce:
    • for the first W − 1 samples, until the window is full. The window is not treated as zero-filled: a zero sample would be a site at the origin, and W−1 of them stacked there make a degenerate surface – measured at W = 11, a window with two real samples reaches a condition number of 6e52;
    • when the window's sites are collinear – a surface over a line is not determined, and tpaps refuses it too;
    • when the solve cannot separate the sites: p = 1 with a repeated site, or the automatic p on a window with fewer than four distinct sites, where tpaps's own rule gives p = 1.
    All three are relative tests on the elimination's pivots, so they do not depend on the units of x, y or z.
  • ⚠ p = 1 interpolates, and interpolation is ill-conditioned when two sites come close with different values: the surface has to bend hard to pass through both. A small amount of smoothing (p just below 1) cures it; the automatic p is well away from 1 on ordinary data.
  • Verified against MATLAB, on this block's own run. Its recorded output was replayed window by window in R2026a – 40 windows of 11 sites per setting – and agrees with fnval(tpaps(...)) at the newest site and at three query points to 2.8e−15 with the automatic p, 8.1e−15 at p = 0.83 and 1.8e−15 at p = 0; the automatic p chose 0.415 to 0.635 over those windows. The elimination here is not MATLAB's QR, but on windows like these the system's condition number stays below 200, which is what keeps the two in agreement.
  • The automatic p follows the sites. It is the rule tpaps applies when it is given no p, recomputed from each window; it is not a constant to be read once and reused, and the surface can therefore change its smoothness as the sites move.
  • ⚠ It is not Surface Fit or Cubic Spline. This block's surface bends to the data, with no model chosen beforehand; a polynomial surface of a given degree is Surface Fit's job, and a curve in one variable is Cubic Spline's or Spline Fit's.
  • Scalar ports. One site per sample; wire one block per surface.
  • No state space. The output is nonlinear in the sites, so model reduction correctly declines to merge it.

Code facts#

FactValue
registered typeControl_Systems/Curve_Fitting/Thin_Plate_Spline
familyControl_Systems/Curve_Fitting
solver environment classICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Thin_Plate_Spline
sourcesrc/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Thin_Plate_Spline/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Thin_Plate_Spline.cpp
headersrc/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Thin_Plate_Spline/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Thin_Plate_Spline.h
default size on canvas150 × 90 px
ports at insert3 in, 2 out
code generators implementedPython, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text

Ports#

#DirectionSignal typeDescription label
1inICoreDoublex
2inICoreDoubley
3inICoreDoublez
4outICoreDoublezf
5outICoreDoublezq

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—
Smoothing Parameter ChoiceAutomatic (tpaps default)%~%Given~~Automatic (tpaps default)—
Smoothing Parameter0.9—
Query Points[0; 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.

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

Caveat (shown to the user): the smoothing thin-plate spline is a Curve Fitting Toolbox function (tpaps, read back by fnval), 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).

Thin-Plate Spline -- tpaps over a running window of scattered samples, read back by fnval MATLAB's st = tpaps([x; y], z, p) over the last W samples, then fnval(st, ...) at the newest site and at the configured query points. The surface is

f(x, y) = SUM(j) c_j * phi(|(x, y) - s_j|^2) + a_x*x + a_y*y + a_0, phi(r2) = r2 * log(r2), and exactly 0 at r2 = 0,

which is stcol.m's 'tp00' form read line for line: it sets a zero squared distance to 1 before taking the logarithm, so a site's own column contributes nothing to itself.

THE SYSTEM. tpaps solves, in its QR null-space form,

(K + lambda*I) c + P a = z, P' c = 0, lambda = (1 - p)/p, P = [x y 1],

and this block solves the SAME equations as one bordered (W+3)x(W+3) system by Gaussian elimination with partial pivoting. They are the same answer in exact arithmetic; how close in floating point is measured below.

THE AUTOMATIC p. Called without p, tpaps picks p = 1/(1 + mean(diag(Q2'K*Q2))), Q2 the columns of the full QR of P that span the null space of P'. Because diag(K) is zero, trace(Q2'*K*Q2) = -trace(K*H) with H = P(P'P)^-1*P' the hat matrix, so lambda = (1-p)/p is just -trace(K*H)/(W-3) and no QR is needed in any target: one 3x3 inverse and a double sum.

⚠ MEASURED AGAINST R2026a TWICE, AND THE SECOND TIME IS THE BLOCK'S OWN RUN. Before a generated line existed, a transcription of exactly this operation order reproduced fnval(tpaps(...)) at the newest site and three query points to 1.2e-15 worst over eighteen windows, and tpaps's own automatic p to 6.7e-16 in lambda. Afterwards the block's RECORDED run was replayed window by window in R2026a -- 40 windows per configuration, taken out of the export-verification rig's own input file -- and agrees to 2.8e-15 (automatic p), 8.1e-15 (p = 0.83) and 1.8e-15 (p = 0, the plane). The automatic p chose 0.415 to 0.635 there.

⚠ THE SITES ARE SIGNALS, so nothing here can be solved at export time the way the fit blocks beside it collapse into a bank of constants. The whole solve is written ONCE as a statement program (ICoreStatementProgram, shared with the Signal Modeling blocks) and rendered into all ten targets; compute_h below runs the same operations in the same order. The window, the sample count and the port reads are each target's own wrapper.

⚠ UNDEFINED WINDOWS ANSWER 0 ON BOTH OUTPUTS, deliberately and in three places:

  • WARM-UP -- until W samples have arrived. Not a zero-prefilled window: here a zero sample

is a SITE, and W-k fake sites stacked on the origin are a degenerate cluster. Measured on

Sample results#

Thin Plate Spline — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sampleThin Plate Spline — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample-202012345t (s)in ICoreDouble-Out-0in ICoreDouble-Out-0in ICoreDouble-Out-0out ICoreDouble-Out-0out ICoreDouble-Out-1
tin ICoreDouble-Out-0in ICoreDouble-Out-0in ICoreDouble-Out-0out ICoreDouble-Out-0out ICoreDouble-Out-1
0-2-2-200
0.40.50.50.500
0.8-2-2-200
1.20.50.50.500
1.6-2-2-200
20.50.50.500
2.4-2-2-200
2.80.50.50.500
3.2-2-2-200
3.60.50.50.500
4-2-2-200
4.40.50.50.500
4.8-2-2-200
5.20.50.50.500

Every 4th of 60 samples, from the table stimulus.

The same rig also ran:

StimulusWhat it isOutput range
impulseImpulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair)0 … 0
rampRamp: slope 1 from t = 00 … 0
sineSine Wave: amplitude 1, 2 rad/s, no phase, no bias0 … 0
stepStep: 0 -> 1 at t = 1 s0 … 0

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 ae1a5a4f23bf9080195613e8ad1f6128ae5da42d · produced by docsSample --out <folder> --blocks Turbofan_Engine_System EOM_6DOF_Wind_Angles EOM_6DOF_Custom_Variable_Mass_Wind_Angles EOM_6DOF_Simple_Variable_Mass_Wind_Angles Surface_Fit Smoothing_Spline Thin_Plate_Spline LPC_To_LSF_LSP --steps 60 · data docs/generated/samples/Control_Systems__Curve_Fitting__Thin_Plate_Spline.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).