Bilinear Transform — Control Systems/Polynomials
Control_Systems/Polynomials/Bilinear_Transform · 2 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.
Bilinear Transform
Control Systems / Polynomials
Turns an analog transfer function's coefficients into a digital
one's by the bilinear (Tustin) substitution
s = K·(z−1)/(z+1), with
K = 2·fs – or
K = 2πfp / tan(πfp/fs)
when a prewarp frequency is given, which makes the two responses agree exactly
at fp. This is MATLAB's bilinear.
Both polynomials come out L + 1 long, where L is the higher of the two input degrees. Nothing in the generated core substitutes, expands or divides: multiplying through by (z+1)L makes the whole map a constant matrix fixed by the configuration and the input lengths, so every target emits one multiply-accumulate per output entry.
It transforms a filter, not a sample. Feed the two outputs to Discrete / Discrete Transfer Function to run the result, or take the analog side from Polynomials / Pade Approximant.
Ports
- bs – the analog numerator's coefficients, descending powers of s, as a vector of n + 1 entries. Degree n at most 8.
- as – the analog denominator's coefficients, descending, m + 1 entries. Degree m at most 8.
- bz – the digital numerator, descending powers of z, as a column of L + 1 entries, L = max(n, m).
- az – the digital denominator, descending, the same length.
Parameters
- Sample Rate (Hz) – fs, the rate the digital filter is designed FOR. Strictly positive. This is not the block's own rate: the block is algebraic and runs whenever it is asked to, while this number is the one the substitution uses. Default 100.
- Prewarp Frequency (Hz) – fp. Zero (the default) means no prewarping and K = 2fs; a positive value below fs/2 bends the frequency axis so the analog and digital responses coincide at exactly that frequency.
- 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 two weight matrices are structural – they follow from the rate, the prewarp and the input lengths – so they are inlined at export time and nothing is exposed as a tunable parameter on the generated core.
The three HDL targets are genuine synthesizable Q16.16: the core carries no substitution and no division, only multiply-accumulate. ⚠ The numerator's precision falls as the sample rate rises, and only in fixed point. bz carries Kn−j−L, so an analog numerator of lower degree than its denominator produces entries of order 1/K or smaller: about 0.05 for a first-over-second-order design at fs = 4 Hz, and about 0.0015 at 100 Hz – a hundred Q16.16 quanta. The seven software targets are unaffected.
Simulink bridge
None (Support::None). bilinear is a MATLAB
function and the Signal Processing Toolbox it comes from ships no Simulink
library at all, so there is no library path a diagram could name. Simulink's own
discrete blocks are a different thing – they take an
already-discretized coefficient set and filter a signal with it – so
mapping onto one would be wrong rather than partial. The bridge reports this
block instead of dropping it silently, and it therefore has no parity
testbench; code export verification still covers it across all ten
languages. No configuration of it crosses either, including
"Sampling Time (s)", which has no counterpart to be written to.
Notes
- Algebraic, with no state: the output depends only on the current input.
- The scale is KL and MATLAB's is az[0].
bilineardivides both polynomials by the digital denominator's first entry, making it monic; this divides both by KL, a configuration-time constant. Same transfer function – both sides carry the same factor. The reason is that az[0] is the analog denominator evaluated at s = K, and that arrives on a port: it can pass through zero, and a core dividing by it would be undefined on those samples in ten languages at once. Divide both outputs by az's first entry, with a Divide block, wherever you know it is safe – that is MATLAB's answer exactly. Measured on bs = [0.3 1.1], as = [1 0.8 2.5], fs = 100:bilinearreports bz = [0.0015213196389667338, 5.4777466542477171e−05, −0.0014665421724250338] over az = [1, −1.9917833800186742, 0.99203236850295684], and this block's outputs reproduce them to 1e−15 after that one division. - Degree 8 is the ceiling on each side, and the bound is on the generated FILE: both weight matrices are unrolled in ten languages.
- It designs; it does not filter. Discrete / Discrete Transfer Function and Discrete / Discrete Filter are the blocks that run the result. Those blocks can discretize a continuous set of their own, from configuration, and keep it inside – which is the difference: this one puts the coefficients on a wire, so they can come from another block and change while the model runs.
- No state space: the map changes the signal's height – n + 1 and m + 1 in, L + 1 out twice – which a merged feed-through D cannot do, so model reduction correctly declines it.
Code facts#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Polynomials/Bilinear_Transform |
| family | Control_Systems/Polynomials |
| solver environment class | ICoreBlock_0_Control_Systems_1_Polynomials_2_Bilinear_Transform |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Polynomials/Bilinear_Transform/ICoreBlock_0_Control_Systems_1_Polynomials_2_Bilinear_Transform.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Polynomials/Bilinear_Transform/ICoreBlock_0_Control_Systems_1_Polynomials_2_Bilinear_Transform.h |
| default size on canvas | 128 × 84 px |
| ports at insert | 2 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 | bs |
| 2 | in | ICoreDouble | as |
| 3 | out | ICoreDouble | bz |
| 4 | out | ICoreDouble | az |
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 |
|---|---|---|
Sample Rate (Hz) | 100 | — |
Prewarp Frequency (Hz) | 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 bilinear transform of a transfer function is a MATLAB function (bilinear), not a Simulink library block -- the Signal Processing Toolbox it comes from ships no Simulink library at all -- and Simulink's discrete blocks are a different thing, filtering a signal with an already-discretized set rather than reporting one, so mapping onto one would be wrong rather than partial; 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).
Bilinear Transform -- bilinear on a wire Substituting s = K*(z-1)/(z+1) and multiplying through by (z+1)^L turns an analog transfer function's coefficients into a digital one's by a CONSTANT MATRIX:
bz[r] = SUM(j) Wb[r][j] * bs[j], Wb[r][j] = K^(n-j) * C[n-j][r] / K^L az[r] = SUM(j) Wa[r][j] * as[j], Wa[r][j] = K^(m-j) * C[m-j][r] / K^L
with C[p] the integer expansion of (z-1)^p * (z+1)^(L-p) and L = max(n, m). The weights are settled by the configuration and the two port sizes, so a generated core substitutes nothing, expands nothing and divides by nothing: one multiply-accumulate per output entry, in all ten targets, genuine Q16.16 in the three fixed-point ones.
Verified against MATLAB R2026a rather than asserted. bs = [0.3 1.1], as = [1 0.8 2.5], fs = 100 Hz:
bilinear(b,a,100) bz 0.0015213196389667338 5.4777466542477171e-05 -0.0014665421724250338 az 1 -1.9917833800186742 0.99203236850295684 this block, x az[0] the same to 1e-15 on every entry
and with a prewarp at 12 Hz, K = 190.43417492768447, the same again. The scale is the only difference and the header says why: MATLAB divides by az[0], which is the analog denominator evaluated at s = K and arrives on a PORT, so it can pass through zero; this divides by K^L, which is a configuration-time constant. ⚠ RENAMED AT THE 2026-09-10 MERGE:
emitis a Qt keyword MACRO and Qt is in the build again. This block was written while Qt was out, so the name was free. QT_NO_KEYWORDS is not the fix -- main's Q_OBJECT code uses them.
Sample results#
| t | in ICoreDouble-Out-0 | in ICoreDouble-Out-0 | out ICoreDouble-Out-0 | out ICoreDouble-Out-1 |
|---|---|---|---|---|
| 0 | -2 | -2 | -2 | -2 |
| 0.4 | 0.5 | 0.5 | 0.5 | 0.5 |
| 0.8 | -2 | -2 | -2 | -2 |
| 1.2 | 0.5 | 0.5 | 0.5 | 0.5 |
| 1.6 | -2 | -2 | -2 | -2 |
| 2 | 0.5 | 0.5 | 0.5 | 0.5 |
| 2.4 | -2 | -2 | -2 | -2 |
| 2.8 | 0.5 | 0.5 | 0.5 | 0.5 |
| 3.2 | -2 | -2 | -2 | -2 |
| 3.6 | 0.5 | 0.5 | 0.5 | 0.5 |
| 4 | -2 | -2 | -2 | -2 |
| 4.4 | 0.5 | 0.5 | 0.5 | 0.5 |
| 4.8 | -2 | -2 | -2 | -2 |
| 5.2 | 0.5 | 0.5 | 0.5 | 0.5 |
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 … 1 |
ramp | Ramp: slope 1 from t = 0 | 0 … 5.8 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -1 … 0.9996 |
step | Step: 0 -> 1 at t = 1 s | 0 … 1 |
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 cb7a648ce28eb1a3672e4a4ed23355825a816e94 · produced by docsSample --out <folder> --blocks Bilinear_Transform --steps 60 · data docs/generated/samples/Control_Systems__Polynomials__Bilinear_Transform.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).