Savitzky Golay Filter — Control Systems/Signal Smoothing
Control_Systems/Signal_Smoothing/Savitzky_Golay_Filter · 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.
Savitzky-Golay Filter
Control Systems / Signal Smoothing
Smooths a stream by fitting a least-squares polynomial of degree p to the last N samples and reporting the value that polynomial takes at one chosen point of the window. Unlike a moving average of the same length it preserves the height and width of a peak, which is what it is usually reached for.
The value of a least-squares fit at a fixed point of its window is a
linear function of that window, so the block is one FIR filter: the small
normal-equation system is solved once from the parameters, and the coefficient
row it produces is applied to a shift register. That row is the same one
MATLAB's sgolay(p, N) produces.
Ports
- u – the noisy signal. Scalar – see Notes.
- y – the fitted value. Same size as the input, so scalar.
Parameters
- Frame Length – N, the number of samples the fit sees. An odd whole number from 3 to 101, and it must be greater than Polynomial Order. Odd because the centre of an even window falls between two samples.
- Polynomial Order – p, the degree fitted. A whole number from 0 to 6, and less than Frame Length. Order 0 makes the block a plain moving average; higher orders follow curvature at the cost of rejecting less noise.
- Evaluation Point – where in the window the fitted polynomial is
read:
- center – the middle of the window. This is what
sgolayfiltdoes for its interior samples, and it is the default. It delays the signal by (N−1)/2 samples, because the middle of the last N samples is that far in the past. - endpoint – the newest sample. No delay, and noisier: an endpoint fit is the least constrained point of a least-squares polynomial.
- center – the middle of the window. This is what
- 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 coefficients are structural and are inlined into the arithmetic at export time rather than exposed as tunable parameters: they follow from all three settings, and changing any of them changes how many multiplies the core contains, which no runtime parameter can do. Re-export after changing them.
The three HDL targets are genuine synthesizable Q16.16: a shift register and a fixed multiply-accumulate, with the products accumulated at full width and shifted back once at the end rather than per term. No solve reaches the hardware – it happened at export time.
Simulink bridge
No equivalent (Support::None). The Signal Processing
Toolbox ships no Simulink library at all, and sgolay and
sgolayfilt are MATLAB functions. 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. - center delays the signal and endpoint does not. This is the block's one real choice and no setting avoids it: a centred fit needs samples on both sides of the point it reports, and the only samples a stream has on the far side are ones already past. Compare a centred output against the input and it will look shifted – it is, by exactly (N−1)/2 samples.
- The window is zero-prefilled, and the zeros count. The first N−1
outputs of a run are a startup transient. MATLAB's
sgolayfiltswitches to the endpoint rows of the same matrix over the first and last (N−1)/2 samples instead; a streaming block cannot do the second of those, so it does neither. The convention matches Moving Median. - Verified against MATLAB. At frame 7 and order 2 the coefficient row
matches
sgolay(2,7)'s middle row to 1.1e−16 and its last row to 6.7e−16 in R2026a. - Order 0 is a moving average, exactly: the least-squares constant over a window is its mean.
- Scalar only. One channel and its own history; wire one block per channel.
- No state space. Linear in the input, but its output depends on N past samples through a fixed FIR rather than through an A/B/C/D pair seeded here, so it carries none and model reduction correctly declines to merge it.
Code facts#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Signal_Smoothing/Savitzky_Golay_Filter |
| family | Control_Systems/Signal_Smoothing |
| solver environment class | ICoreBlock_0_Control_Systems_1_Signal_Smoothing_2_Savitzky_Golay_Filter |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Signal_Smoothing/Savitzky_Golay_Filter/ICoreBlock_0_Control_Systems_1_Signal_Smoothing_2_Savitzky_Golay_Filter.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Signal_Smoothing/Savitzky_Golay_Filter/ICoreBlock_0_Control_Systems_1_Signal_Smoothing_2_Savitzky_Golay_Filter.h |
| default size on canvas | 134 × 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 | y |
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 |
|---|---|---|
Frame Length | 7 | — |
Polynomial Order | 2 | — |
Evaluation Point | center%~%endpoint~~center | — |
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): no Simulink equivalent: sgolay() and sgolayfilt() are MATLAB functions and the Signal Processing Toolbox ships no Simulink library at all, while nothing in the Simulink standard library or in DSP System Toolbox fits a least-squares polynomial over a sliding window. Reported rather than dropped, and it carries no parity testbench
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).
Savitzky-Golay Filter -- least-squares polynomial smoothing over the last N samples ONE COEFFICIENT ROW, TEN IDENTICAL DOT PRODUCTS. The value a least-squares polynomial takes at a FIXED point of its window is a LINEAR functional of that window, so the fit is never recomputed per sample: a small normal-equation system is solved once when the configuration is read, and the row it produces is inlined by every target as a fixed multiply-accumulate over a shift register. The emitted core solves nothing and branches nowhere.
MEASURED AGAINST MATLAB R2026a rather than asserted: at frame 7 and order 2 the row derived here matches sgolay(2,7)'s middle row to 1.1e-16 (centre) and its last row to 6.7e-16 (endpoint). The probe is recorded in the notes of the toolbox-blocks plan, Family B.
⚠ THE REGRESSION AXIS IS SCALED TO ROUGHLY [-1, 1] BEFORE THE SOLVE, and that is not cosmetic. Written on the raw sample index, the normal-equation matrix for a frame of 101 and an order of 6 carries entries around 1e26 and the solve loses digits to nothing but the choice of units. Scaling cannot change the answer -- the fit is evaluated exactly at the point the axis is centred on, where the polynomial's value is its constant term whatever the axis is worth per sample -- so it is a free improvement.
⚠ THE WINDOW IS ZERO-PREFILLED AND THE ZEROS COUNT, the same convention Moving Median carries (measured against Simulink). The first N-1 outputs of a run are a startup transient. MATLAB's sgolayfilt() instead switches to the endpoint rows of the same matrix over the first and last (N-1)/2 samples, which a streaming block cannot do at the end of a signal it has not seen.
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.09524 … 0.3333 |
ramp | Ramp: slope 1 from t = 0 | -0.009524 … 5.5 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -0.9993 … 0.9989 |
table | Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample | -1.095 … 1.714 |
Plotted: step — Step: 0 -> 1 at t = 1 s
Category dynamic · sample time 0.1 · 60 steps · commit 6db3032c0 · produced by docsSample --out <folder> --blocks Detrend Savitzky_Golay_Filter Hampel_Filter Envelope_Detector --steps 60 · data docs/generated/samples/Control_Systems__Signal_Smoothing__Savitzky_Golay_Filter.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).