Generated reference › Bilinear Transform — Control Systems/Polynomials
kind: generated#block#control-systems-polynomials

Bilinear Transform — Control Systems/Polynomials

s z K(z−1) z+1

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]. bilinear divides 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: bilinear reports 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#

FactValue
registered typeControl_Systems/Polynomials/Bilinear_Transform
familyControl_Systems/Polynomials
solver environment classICoreBlock_0_Control_Systems_1_Polynomials_2_Bilinear_Transform
sourcesrc/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Polynomials/Bilinear_Transform/ICoreBlock_0_Control_Systems_1_Polynomials_2_Bilinear_Transform.cpp
headersrc/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Polynomials/Bilinear_Transform/ICoreBlock_0_Control_Systems_1_Polynomials_2_Bilinear_Transform.h
default size on canvas128 × 84 px
ports at insert2 in, 2 out
code generators implementedPython, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text

Ports#

#DirectionSignal typeDescription label
1inICoreDoublebs
2inICoreDoubleas
3outICoreDoublebz
4outICoreDoubleaz

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
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.

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

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: emit is 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#

Bilinear Transform — Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sampleBilinear Transform — 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-0out ICoreDouble-Out-0out ICoreDouble-Out-1
tin ICoreDouble-Out-0in ICoreDouble-Out-0out ICoreDouble-Out-0out ICoreDouble-Out-1
0-2-2-2-2
0.40.50.50.50.5
0.8-2-2-2-2
1.20.50.50.50.5
1.6-2-2-2-2
20.50.50.50.5
2.4-2-2-2-2
2.80.50.50.50.5
3.2-2-2-2-2
3.60.50.50.50.5
4-2-2-2-2
4.40.50.50.50.5
4.8-2-2-2-2
5.20.50.50.50.5

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 … 1
rampRamp: slope 1 from t = 00 … 5.8
sineSine Wave: amplitude 1, 2 rad/s, no phase, no bias-1 … 0.9996
stepStep: 0 -> 1 at t = 1 s0 … 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).