Sum Of Sines Fit — Control Systems/Curve Fitting
Control_Systems/Curve_Fitting/Sum_Of_Sines_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.
Sum of Sines Fit
Control Systems / Curve Fitting
Fits a sum of N sinusoids at given frequencies to the last W
samples and emits each term's quadrature pair. The model is MATLAB's
sin<n>,
ŷ(x) = Σi=1..n ai·sin( bi·x + ci )
and with the frequencies bi given, each term expands into the equivalent pair
ai·sin(bix + ci) = Ai·sin(bix) + Bi·cos(bix)
which is linear. The output carries A₁, B₁, …, An, Bn. The newest sample sits at x = 0 and the sample i steps back at x = −i·h, the same abscissa Polynomial Fit and Fourier Fit use.
Ports
- u – the sampled signal being fitted. Scalar: one channel and its own window – see Notes.
- c – the coefficient column, [2n, 1], in the order above: the sine amplitude then the cosine amplitude of each term. Its height follows Number of Terms alone.
Parameters
- Window Length – W, how many samples the fit sees. A whole number from 3 to 101, and it must be at least 2n: 2n coefficients cannot be determined by fewer than 2n points. Bounded above because the dot products are unrolled at export.
- Number of Terms – n, a whole number from 1 to 8, matching
MATLAB's
sin1…sin8. Each term adds a sine and a cosine, so the output grows by two. - Frequencies (rad per unit) – [b₁ … bn], an n element vector of strictly positive angular frequencies, in radians per unit of x. They need not be related to one another – that is what separates this block from Fourier Fit. ⚠ Only the products bi·h reach the arithmetic, and each must stay clear of a multiple of π – see Notes.
- Abscissa Step – h, the spacing between consecutive samples on the fitted axis. A single positive number. Setting it to the sampling period and giving the frequencies in radians per second is the same fit as leaving it at 1 and giving them in radians per sample.
- 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 2n×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 four settings, and changing any of them changes how many multiplies and how many outputs the core contains. Re-export after changing them. No sine or cosine is evaluated in any generated core – they were 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. Emitting the amplitude and phase instead would have cost that – see Notes.
Simulink bridge
None (Support::None). Fitting a sum of sinusoids to a
running window is a Curve Fitting Toolbox function (fit with a
sin<n> model), 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 output is the quadrature pair, not MATLAB's amplitude and phase. The two carry the same information: ai = √(Ai² + Bi²) and ci = atan2(Bi, Ai), one Trigonometry / Atan2 and one Sqrt away. The pair is what this block emits because the arctangent has a branch cut: the phase of a fit to a moving window walks around the circle, and every crossing of ±π is a jump of 2π that two implementations only agree about when they agree to the last bit. The quadrature coefficients are continuous everywhere, and they are what keeps the three HDL targets synthesizable.
- ⚠ The frequencies are parameters here and MATLAB solves for them. With b unknown the fit is nonlinear and needs an iterative solver every sample. If the frequencies are what you are looking for, this is the wrong block – reach for Machine Learning / Feature Engineering / FFT Magnitude or Transforms / Goertzel first, then set the result here.
- ⚠ This is not Fourier Fit with different numbers. Fourier Fit
fits harmonics of one fundamental and carries a constant term
a₀; this fits n independent frequencies and carries none. A signal
made of two unrelated tones has no fundamental to be a harmonic of, which is why
MATLAB ships
fourier<n>andsin<n>as separate models. Use Fourier Fit when the content really is harmonic: it determines one frequency parameter instead of n, and it fits the mean. - There is no constant term. Every basis function averages to nearly zero over a long window, so a signal with an offset is fitted through that offset rather than having it removed. Put a Signal Smoothing / Detrend ahead of the block if the offset is not wanted.
- One solve, then arithmetic. The small normal-equation system is solved once when the configuration loads; every sample afterwards is 2n fixed dot products.
- ⚠ Not every setting is a fit, and the block refuses rather than guessing. The basis degenerates on a sample grid: a product bi·h at an exact multiple of π makes that term's sine column vanish identically, and two frequencies that alias to the same value on the grid cannot be told apart. Either makes the normal matrix singular, which is detected when the configuration loads and reported with a reason – rather than emitting the arbitrary answer a pseudo-inverse would have produced. Keep every bi·h under π, and the frequencies apart by more than one bin of the window, and neither case arises.
- Verified against MATLAB. At W = 13, h = 0.3 and frequencies [0.9 2.3], R2026a's own least-squares solve of the same design matrix answers [−0.20722798579525406, −0.40790884283512951, −0.63871062749727303, −0.12000205236902012] on a fixed 13-sample window; this block reproduces all four entries to 3.1e−16 relative, with cond(A'A) = 1.47. Solving the normal equations squares the condition number where a QR solve 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; it costs nothing at a conditioning like that one.
- 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 and Fourier 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/Sum_Of_Sines_Fit |
| family | Control_Systems/Curve_Fitting |
| solver environment class | ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Sum_Of_Sines_Fit |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Sum_Of_Sines_Fit/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Sum_Of_Sines_Fit.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Curve_Fitting/Sum_Of_Sines_Fit/ICoreBlock_0_Control_Systems_1_Curve_Fitting_2_Sum_Of_Sines_Fit.h |
| default size on canvas | 140 × 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 | — |
Number of Terms | 2 | — |
Frequencies (rad per unit) | [0.5 1.1] | — |
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 sum of sinusoids to a running window is a Curve Fitting Toolbox function (fit with a sin<n> model), 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).
Sum of Sines Fit -- the least-squares sum of N sinusoids at given frequencies MATLAB's
sin<n>fittype with the frequencies GIVEN rather than identified:y(x) = SUM(i = 1..n) a_i * sin( b_i * x + c_i )
coeffnames(fittype('sin2'))in R2026a reports {a1 b1 c1 a2 b2 c2}; b is this block's parameter, and what it emits is each term's QUADRATURE pair rather than its amplitude and phase:a_i * sin(b_i*x + c_i) = A_i * sin(b_i*x) + B_i * cos(b_i*x)
so the output column carries A1, B1, A2, B2, ... An, Bn. The header says why the amplitude and phase form was rejected -- the arctangent's branch cut, measured on this block's own tune set at c = -2.96 rad.
With b given the fit is LINEAR, so the whole normal-equation system is solved once per configuration load and what every target carries is 2n fixed dot products over one shift register. The solve itself is ICoreCurveFitBankSupport's, shared with Gaussian_Fit.
TRANSCRIBED FROM R2026a and checked against a run of it rather than inferred. At W = 13, h = 0.3 and frequencies [0.9 2.3], over the window
y = [1.2 -0.7 2.5 0.3 -1.9 0.8 1.1 -2.2 0.45 1.7 -0.35 0.62 -1.4] (oldest first)
MATLAB's own least-squares solve of the same design matrix (A\y, a QR solve) answers [-0.20722798579525406, -0.40790884283512951, -0.63871062749727303, -0.12000205236902012]. This block reproduces all four entries to 3.1e-16 relative, and cond(A'A) there is 1.47.
⚠ NO SINE OR COSINE SURVIVES INTO A GENERATED CORE. Every one is evaluated once, here, to build the bank; the ten targets carry the resulting numbers and nothing else. That is what makes the three HDL targets genuine synthesizable Q16.16 rather than simulation-only.
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.1669 … 0.1548 |
ramp | Ramp: slope 1 from t = 0 | -0.59 … 1.089e-4 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -0.6586 … 0.6579 |
table | Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample | -1.257 … 0.8447 |
Plotted: step — Step: 0 -> 1 at t = 1 s
Category dynamic · sample time 0.1 · 60 steps · commit 5ec6520e9ab09131166ee577848f504dc1883d0f · produced by docsSample --out <folder> --blocks Gaussian_Fit Sum_Of_Sines_Fit --steps 60 · data docs/generated/samples/Control_Systems__Curve_Fitting__Sum_Of_Sines_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).