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
fnvaltakes 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.
- ⚠ 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#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Curve_Fitting/Thin_Plate_Spline |
| family | Control_Systems/Curve_Fitting |
| solver environment class | ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Thin_Plate_Spline |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Thin_Plate_Spline/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Thin_Plate_Spline.cpp |
| header | src/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 canvas | 150 × 90 px |
| ports at insert | 3 in, 2 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 | x |
| 2 | in | ICoreDouble | y |
| 3 | in | ICoreDouble | z |
| 4 | out | ICoreDouble | zf |
| 5 | out | ICoreDouble | zq |
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 | — |
Smoothing Parameter Choice | Automatic (tpaps default)%~%Given~~Automatic (tpaps default) | — |
Smoothing Parameter | 0.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.
Simulink bridge#
| support | Support::None |
| Simulink path | — |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
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, thenfnval(st, ...)at the newest site and at the configured query points. The surface isf(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#
| t | in ICoreDouble-Out-0 | in ICoreDouble-Out-0 | in ICoreDouble-Out-0 | out ICoreDouble-Out-0 | out ICoreDouble-Out-1 |
|---|---|---|---|---|---|
| 0 | -2 | -2 | -2 | 0 | 0 |
| 0.4 | 0.5 | 0.5 | 0.5 | 0 | 0 |
| 0.8 | -2 | -2 | -2 | 0 | 0 |
| 1.2 | 0.5 | 0.5 | 0.5 | 0 | 0 |
| 1.6 | -2 | -2 | -2 | 0 | 0 |
| 2 | 0.5 | 0.5 | 0.5 | 0 | 0 |
| 2.4 | -2 | -2 | -2 | 0 | 0 |
| 2.8 | 0.5 | 0.5 | 0.5 | 0 | 0 |
| 3.2 | -2 | -2 | -2 | 0 | 0 |
| 3.6 | 0.5 | 0.5 | 0.5 | 0 | 0 |
| 4 | -2 | -2 | -2 | 0 | 0 |
| 4.4 | 0.5 | 0.5 | 0.5 | 0 | 0 |
| 4.8 | -2 | -2 | -2 | 0 | 0 |
| 5.2 | 0.5 | 0.5 | 0.5 | 0 | 0 |
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) | 0 … 0 |
ramp | Ramp: slope 1 from t = 0 | 0 … 0 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | 0 … 0 |
step | Step: 0 -> 1 at t = 1 s | 0 … 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).