A long IIR filter is safest run as a chain of small filters, each with two poles and two zeros. Below, an order-5 filter is split into its sections. Watch each section’s gain peak on its own, while the whole filter never rises above 0 dB.
Pair each pole pair with its nearest zeros
20.6's elliptic filter of order 5 as a first-order section and two biquads (SciPy zpk2sos, 'nearest').
Five poles and five zeros of the elliptic filter, to be split into sections.
Describe this picture
Two panels for 20.6’s elliptic filter of order 5 as a first-order section and two biquads (SciPy’s zpk2sos, “nearest”). The plane has the real part across and the imaginary part up, both from −1.2 to 1.2, and the unit circle; the poles are crosses and the zeros circles. As each section forms, a dashed loop encloses its roots, labelled 1, 2 or 3; section 1’s label also gives its gain, 0.022. The gain panel plots the gain from −60 to 25 dB against frequency from 0 to 4000 Hz. Each section’s gain is a thin dashed curve, and the whole filter is a thick solid curve labelled “whole filter”. Two readouts, the peaks of sections 2 and 3, read “not yet” until their section is drawn.
The clip lasts 15 s. It starts with the five poles and five zeros of the elliptic filter. Then section 3’s loop draws: the pole pair nearest the circle, 0.953∠±45.6°, with the zeros nearest it, at ±57.4°. At 4.75 s section 3 peaks at 14.00 dB, at 1000 Hz. Next comes section 2: the next pair, 0.806∠±35.0°, with the remaining zeros, at ±76.4°. Then section 1 forms: the real pole, the zero at −1, and the gain. At 9.75 s section 2 peaks at 15.72 dB, and section 1, with the gain 0.022, stays below −17 dB. Last the whole filter draws: the sections’ dB add up to it, and it never exceeds 0 dB. After the clip two buttons in a group named “Pairing” choose the nearest or the swapped pairing; the choice is kept in the link, as pairs.p.
Pair each pole pair with its nearest zeros
In Filter structures (21.1) we wired up one biquad in several ways, and saw that a long filter can be a cascade of them. This page asks how to split a long filter into biquads, and why it is worth doing. I’ll use the elliptic filter of Choosing FIR or IIR (20.6), which has order 5: five poles and five zeros.
Transfer functions, poles & zeros (16.3) wrote such a filter as a gain times one factor for each root:
Multiply the factors back together in small groups, and each group is a small filter. Run the small filters one after another, and by 21.1 the cascade is again.
Which groups? A complex pole comes with its mirror , and their two factors multiply to
Both coefficients are real. In the same way, a pair of zeros on the unit circle gives .
One pole pair over one zero pair is the biquad of Audio equalisers and biquads (20.5). Used as one piece of a longer filter, I call it a second-order section. Each section has its own five coefficients:
An odd order leaves one real pole over. It goes with a real zero into a first-order section, . So our order-5 filter becomes one first-order section and two biquads.
Which zeros go with which poles? That choice is the pairing. The arrows of 16.3 show why it matters. Where the walk round the circle passes close to a pole, that pole’s arrow is short and the gain climbs. A zero close by has a short arrow too, and it pulls the gain back down.
So give each pole pair the zeros nearest it, and no section climbs very high. It is like seating dinner guests next to the people they know, so that no table gets loud. SciPy’s zpk2sos(z, p, k, pairing='nearest') does it in this order:
- Take the pole pair nearest the unit circle, and give it the two zeros nearest to it.
- Repeat with the poles and zeros that are left.
- A real pole takes a real zero, and the gain goes into the first section’s ‘s. (SciPy’s
kis our .)
The cascade gives the same in any order, but SciPy still picks one. This is the ordering: the sections with poles nearest the circle go last. I’ll show why further down.
The picture at the top of the page splits 20.6’s filter this way, one section at a time, and draws each section’s gain. Notice that both biquads peak far above 0 dB on their own. Their peaks sit at different frequencies, and section 1 stays below −17 dB everywhere. Gains in a cascade multiply, so their dB add (20.5), and the sum never rises above 0 dB.
After the clip, choose the swapped pairing with its button: the loops redraw around the new groups, and the section curves change. The pole pair nearest the circle now gets the far zeros, and its section peaks at 22.99 dB; the other drops to 10.17 dB. The whole filter is unchanged.
The arrows of 16.3 explain the jump. At 1000 Hz the walk is at 45° on the circle, next to the poles of section 3. Their own zeros are 0.217 away, and the far zeros 0.541, two and a half times as far. Swapped, nothing near the poles pulls the gain down, and the peak rises by 9.0 dB.
Now the ordering. It does not change , but it changes the signals between the sections. In SciPy’s order the largest gain from the input to the end of section 1 is −17.43 dB, to the end of section 2 it is −4.11 dB, and to the output 0 dB. The sharp sections come last, so the signal reaches them already made smaller by the others.
Run the same three sections in the opposite order. After two of them, a 981 Hz sine comes out 24.47 dB louder than it went in, about 17 times. In floating point that does no harm. Finite word-length effects (21.3) shows what a stored value that large does in fixed point.
Raise the order until the direct form breaks
Why not skip the sections and run the whole filter as one difference equation, a direct form of 21.1? On paper the answer is the same. But a computer rounds every coefficient, and the direct form is far more sensitive to that rounding.
Most computers store a number in floating point: a sign, the first 24 binary digits of the number, and a power of 2 that says where the point goes. This is single precision, float32 in NumPy, and many DSP chips and audio plug-ins use it. 24 binary digits are about 7 decimal digits.
Rounding to single precision is the rounding to a grid of levels of Quantization & noise (11.1), with one difference: the step grows with the number. The error is at most of the number’s size. NumPy’s usual float64, double precision, keeps 53 binary digits, about 16 decimal digits.
My test filter is a Butterworth low-pass from Analog prototype filters (20.1), with cutoff 100 Hz at kHz. 100 Hz is a small slice of the circle, 4.5°, so all its poles crowd near .
I write the direct form’s denominator as , with the order. For order 6 the coefficients are 1, −5.696561, 13.528499, −17.144063, 12.227073, −4.653138 and 0.738190. Put , and is their sum. By the factor form, it is also the product of the six small numbers .
So the coefficients, up to 17 in size, add up to . Rounding −17.14406324 to single precision gives −17.14406395, a change of . That one change is larger than itself, so every digit of the coefficients matters.
The instrument below rounds the coefficients of this filter to single precision in two ways, as one direct form and as second-order sections. Then it finds the poles of what was rounded. The filtering itself runs in double precision, so the rounded coefficients are the only cause of what you see.
Raise the order until the direct form breaks
Butterworth low-pass, cutoff 100 Hz at f_s = 8 kHz, coefficients rounded to single precision: one direct form against second-order sections.
Order 2: both versions put the poles where they belong, radius 0.9460.
Describe this picture
Two panels for a Butterworth low-pass, cutoff 100 Hz at kHz, with its coefficients rounded to single precision: one direct form against second-order sections. The first is the plane close to , the real part from 0.85 to 1.1 and the imaginary part from −0.12 to 0.12, with an arc of the unit circle. The true poles are faint crosses, the direct form’s rounded poles are crosses, and the sections’ rounded poles are small rings. The second is the impulse response , from −0.04 to 0.04, against the sample from 0 to 400: the direct form is a solid line and the sections a dashed line. Where the direct form leaves the panel, an open triangle at the edge is labelled “grows without bound”. The readouts are the order and each version’s largest pole radius, to four decimals, followed by “unstable” above 1.
The clip lasts 13 s, with a blank caption while the order changes. At order 2 both versions put the poles where they belong, radius 0.9460. At 5.75 s, order 4: rounding moves the direct form’s poles a little, 0.9702 instead of 0.9704, and its gain at 0 Hz is off by 0.065 dB, while the sections’ poles stay at 0.9704. At the end, order 6: the direct form’s rounded coefficients have a pole at 1.0417, outside the circle, and its impulse response grows past 1 by ; the same poles as three sections, rounded the same way, stay at 0.9799. After the clip a slider named “Order N” takes the even orders from 2 to 12; the arrow keys move it by 2, and Home and End jump to 2 and 12. Its value reads like “6: direct form unstable”. At orders 2, 4 and 6 the caption is the clip’s; at order 8 it reads “Order 8: the direct form’s largest pole is 1.1368, unstable; the sections’ 0.9848.” From order 8 that pole lies beyond the plane’s edge at 1.1, so it is drawn as an open triangle on the edge, pointing outward and labelled with its radius. The order is kept in the link, as break.N.
Notice that the rings stay on the true poles at every order. The direct form’s crosses drift at order 4 and leave the circle at order 6.
After the clip, set the order yourself with the slider. Try 12: the direct form’s largest pole is 1.5037, while the sections’ is 0.9898.
Order 6 breaks in a way you can see in the numbers. After rounding, the coefficients add up to : the sign has flipped. Multiply by , and you have a polynomial in . Far out on the real axis it is positive, and at it is now negative.
So it crosses zero somewhere beyond . That crossing is a real pole outside the circle, the 1.0417 of the readout. By Stability and causality (16.4), one pole outside the circle is enough to make the filter unstable.
Why the direct form is so sensitive
Write , the same polynomial in powers of , so that . Rounding the coefficients adds a tiny polynomial to . Near one root , is close to a straight line, and its slope there is the product of the distances from to the other roots.
The root moves by about the change in at divided by that slope. When the roots crowd together, that slope is tiny. For order 6, the pole nearest the circle is 0.039, 0.075, 0.106, 0.131 and 0.149 away from the other five, and the product is only .
The rounding changes there by , so the estimate is a move of about 0.2. That pole is 0.020 inside the circle, ten times closer than that. The estimate only holds for small moves, but it says clearly that the pole will not stay put.
A biquad’s two roots are moved only by its own two coefficients, and the slope at each root is the single distance to its mirror. In the order-6 design that distance is 0.149 for the section nearest the circle, so its poles move by .
Double precision has 29 more binary digits, so it lasts longer, but it breaks too. The same Butterworth design as one direct form keeps every pole inside the circle up to order 11. At order 12 its largest pole is at 1.0241, with no rounding to single precision at all.
So I keep an IIR filter above order 2 or 3 as sections, from the design to the filtering. SciPy’s butter documentation says the same: the [b, a] form can have numerical problems “even for N >= 4”, and it recommends second-order sections, output='sos'.
The maths behind it · ill-conditioned eigenvalues
The roots of a polynomial are the eigenvalues of its companion matrix, which is how NumPy’s roots finds them. When a non-symmetric matrix has eigenvalues crowded together, they can be badly conditioned: a tiny change in the entries moves them far. Wilkinson’s polynomial is the classic case. Factoring into biquads is choosing a better-conditioned way to store the same filter.
Worked example
Let’s redo the page’s numbers, by hand where we can and with SciPy where we can’t.
1. The elliptic filter in sections. ellip(5, 0.4455, 40, 1000, fs=8000, output='sos') returns one row per section, with . Each row is one section of the form above, and the filter is their product, .
| Section | Numerator coefficients | Denominator coefficients |
|---|---|---|
| 1, first-order | 0.022446, 0.022446, 0 | 1, −0.665916, 0 |
| 2 | 1, −0.470440, 1 | 1, −1.319331, 0.649273 |
| 3 | 1, −1.076389, 1 | 1, −1.332241, 0.907593 |
Check section 3 against its roots. The poles have radius 0.952677 and angles ±45.636°, so and . The zeros are at ±57.439°, so .
2. The dB add up. At 0 Hz, , so each section’s gain is the sum of its ‘s over the sum of its ‘s. Section 1 gives , which is −17.43 dB. Section 2 gives , 13.32 dB, and section 3 gives , 4.11 dB.
The sum is dB, the gain of the whole filter at 0 Hz.
3. The swapped pairing at 0 Hz. Section 2 now has section 3’s zeros: , 8.94 dB. Section 3 has section 2’s zeros: , 8.49 dB. Together they give 17.43 dB, the same as .
So at 0 Hz the swap is hard to see. The cost is near 1 kHz, where the swapped section 3 peaks at 22.99 dB instead of 14.00.
4. Coefficient sizes. The order-6 Butterworth denominator at 100 Hz has coefficients up to 17.14 in size, and the order-12 one up to 681. Rounding to single precision changes each by at most of its size. Here the largest such change is at order 6 and at order 12.
5. Where double precision breaks. With no rounding to single precision, the direct form’s largest pole is 0.9876 at order 10 (true 0.9878) and 0.9944 at order 11 (true 0.9889). At order 12 it is 1.0241, and the impulse response first passes 1 at .
6. In SciPy. Design straight into sections with butter(6, 100, fs=8000, output='sos'), and filter with sosfilt(sos, x), which runs each section as the transposed direct form of 21.1. If you have poles and zeros, zpk2sos(z, p, k) pairs and orders them.
tf2sos(b, a) also exists, but it starts from the polynomial, so it cannot undo rounding already done to and . Applied to the single-precision coefficients of order 6, it gives sections with the same pole at 1.0417.
Where you’ll meet this
Running IIR filters as cascades of biquads is the usual practice. SciPy has sosfilt and sosfiltfilt, and MATLAB has zp2sos and sosfilt. Arm’s CMSIS-DSP library for microcontrollers has the arm_biquad_cascade functions. The Web Audio API’s BiquadFilterNode is one section, and you chain several for a higher order.
The parametric equalisers of 20.5 are already cascades, one biquad for each band. How many bits each coefficient and stored value needs is Finite word-length effects (21.3). Running the sections sample by sample, against a clock, is Real-time processing (21.4).
The maths behind it · sensitivity and error propagation
Rounding errors in the coefficients behave like small random changes. How far a small change moves a root is a sensitivity, a derivative: one over the slope of at the root. Error-propagation formulas use the same quantity to carry measurement error through a calculation.
Reference card
| Quantity | Rule | Notes |
|---|---|---|
| Second-order section | one pole pair, one zero pair | |
| Pole pair | real coefficients | |
| Odd order | one first-order section | a real pole, a real zero |
| Pairing | pole pair nearest the circle with the zeros nearest it, repeat | zpk2sos(pairing='nearest') |
| Ordering | poles nearest the circle last | SciPy’s default |
| Cascade | gains multiply, dB add | sosfilt |
| Single precision | 24 binary digits, about 7 decimal digits | error at most of the size |
| Why sections | a direct form’s crowded roots move far when rounded | order 6 breaks in single precision here, order 12 in double |