<!-- canonical: https://docs.jmrp.io/phonometry/signals/filters/filter-compliance/ -->
Source: https://docs.jmrp.io/phonometry/signals/filters/filter-compliance/

# Filter class verification (IEC 61260-1)

A filter bank becomes a measuring instrument only once its bands have been
proved against a specification. IEC 61260-1:2014 writes that specification
as an **acceptance mask**: a corridor of relative attenuation around each
mid frequency, narrow in the passband, opening into a minimum-attenuation
requirement far outside the band, with one corridor per performance class.
A bank "is class 1" when every band of it stays inside the class 1 corridor
at every normalized frequency, and the margin in decibels says by how much.

This page is the verification half of the octave-filtering topic: the 2014
mask and the per-band verdict, the two requirements Part 2 of the series
computes from the same response (the effective bandwidth and the summation of
adjacent outputs) and the swept test of time-invariant operation, the stricter
**class 0** kept alive by the withdrawn IEC 61260:1995 and ANSI S1.11-2004
masks, a reading of what a class actually buys in a measurement (passband
error, stopband leakage, uncertainty budget), how the results of a periodic
test are graded, and the one-page accredited fiche that turns the verdict into
a document. The design half, the band mathematics and the
parameter reference, is [Filter Banks](https://docs.jmrp.io/phonometry/signals/filters/filter-banks/), and the five
architectures with their compared responses are
[Filter Architecture Gallery](https://docs.jmrp.io/phonometry/signals/filters/filter-gallery/); the same machinery
applied to the frequency weightings is section 6 of
[Frequency Weighting](https://docs.jmrp.io/phonometry/signals/levels/weighting/).

## 1. Verifying the class against IEC 61260-1:2014

`verify_filter_class` checks every band of a bank against the acceptance
limits of **IEC 61260-1:2014** (Table 1, with the fractional-octave breakpoint
mapping and log-frequency interpolation from the standard), and, for the 2014
edition, against the effective bandwidth and the summation of outputs the way
IEC 61260-2:2016 computes them (section 1b). It reports the performance class
per band with its margins in dB:

```python
from phonometry import filters

bank = filters.OctaveFilterBank(fs=48000, fraction=3, order=6)
result = filters.verify_filter_class(bank)
print(result.overall_class)          # 1
print(result.requirements)
# ('relative_attenuation', 'effective_bandwidth', 'summation')
band = result.bands[0]
print(band["freq"], band["class"], band["checked_to_omega"])
# 12.589254117941678 1 1906.3877633382747
print(band["margin_class1_db"], band["bandwidth_margin_class1_db"])
# 0.39999977486865645 0.35126579320766144
```

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_filter_class_check_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/diagram_filter_class_check.svg" alt="Calculation chain for grading a band filter against IEC 61260-1 Table 1: a one-third-octave band at 1 kHz, its relative attenuation at every breakpoint against the class 1 and class 2 limits, the class 1 margin at each breakpoint and the class it gives, with the IEC 61260-2 tests that need no specimen (the 24-point grid, the effective bandwidth, the summation and the sweep) computed on the design as well, and the tests a laboratory runs on a device, IEC 61260-2 on a specimen and IEC 61260-3 periodic tests graded by verify_filter_periodic, set apart as outside the check" width="88%"></picture>

*The 1 kHz band of a bank like the one above, walked through the check: its
relative attenuation at every Table 1 breakpoint, carried to one-third octave,
against the class 1 and class 2 limits, with the smallest margin deciding the
class. The check walks every band to half the input rate, 24 times this
band's mid-band frequency at 48 kHz, on the response the band has at that
rate, so a decimated band is graded on the alias images its decimation folds
onto it as well. The
box on the right is what section 1b computes on the design as well, the
IEC 61260-2 tests that need no specimen; the dashed column under it is what a
laboratory does to an instrument, and section 3b grades the periodic-test
results it returns.*

**How far up the mask the verdict actually reaches.** `checked_to_omega` is the
highest normalized frequency $\Omega = f/f_\mathrm{m}$ at which that band was
evaluated: half the input sampling rate, the highest frequency a sampled input
holds, over the band's mid-band frequency. A band the bank decimates is
evaluated that far too, not only to the Nyquist frequency of its decimated rate
(see below). Every band of the bank above is evaluated past
the end of the Table 1 mask (at least 70 dB for class 1 from
$\Omega \approx 5.39$ up, the octave $G^4$ row carried to one-third octave)
except those near the top: from 5 kHz up the 24 kHz Nyquist frequency lies
below $5.39\,f_\mathrm{m}$, and the 20 kHz band is evaluated only to
$\Omega = 1.20$. There the far-stopband requirement is not demonstrated *on
the band filter* at all. It is taken as satisfied because a sampled signal
carries no energy above its Nyquist frequency: the anti-alias filter of the
capture chain removed it before the band ever saw it. `range_limited` is the
flag that this argument was used, and it is `True` here.

Say that plainly in a report: the verdict attests the mask up to
`checked_to_omega`, and the rest is an argument about the capture chain. The
flag clears only when half the input rate lies past the end of the mask for
every band. The one-third-octave bank of section 1b, from 125 Hz to 4 kHz at
48 kHz, is not range-limited; the octave bank over the same range is, because
its 2 kHz and 4 kHz bands, filtered at the full rate, are evaluated only to
$\Omega \approx 12$ and 6, short of the $G^4 = 15.85$ where the octave mask
ends. When a document requires the full mask on every band filter, lower the
top of the bank or raise `fs`: designing the bank with
`design=filters.FilterDesign(resample=False)` changes nothing here, because
the bands that stop short already run at the full rate.

**A decimated band is graded on its alias images.** A band the bank decimates
by $M$ runs its input through an anti-alias low-pass, keeps one sample in $M$
and filters at $f_\mathrm{s}/M$, so a tone at $k\,f_\mathrm{s}/M \pm f$ comes
out of the band at $f$: the band reads images of itself all the way up the
input band. IEC 61260-1:2014 5.15 asks the anti-alias filters to keep those
images inside the Table 1 limits, and Table 1 covers every frequency.
IEC 61260:1995 4.8 asks them to keep the relative attenuation from exceeding
the greatest of the minimum limits of Table 1, which 5.7 tests with a tone at
the decimated sampling frequency minus the mid-band frequency; every image of a
decimated band lies past the last breakpoint of the mask, where its minimum is
that greatest one. So the check reads each band at the input rate in both
editions: the anti-alias filter at the input frequency times the band's
sections at the frequency it folds onto. Graded that way, the decimator
`scipy.signal.resample_poly` designs by default, a Kaiser window with
$\beta = 5$, left the first image of the default octave bank 68.9 dB down at 8,
16 and 32 kHz, short of the 70 dB of class 1, and the images of every other
default bank 70.9 dB to 73.1 dB down, short of the 75 dB of class 0 in the 1995
edition. Those 68.9 dB images sit above 1.5 times the highest mid-band
frequency of the bank, past the range over which IEC 61260-2:2016 7.2.2.2
measures the relative attenuation, so that class 1 verdict rests on 5.15 and
Table 1 of IEC 61260-1:2014 themselves. A check that stopped at the decimated
Nyquist frequency saw none of it. The bank now decimates and interpolates
through a filter of the same length whose Kaiser window is sized for a 120 dB
stopband: its images fall at least 125.4 dB down from 8 kHz to 192 kHz, and its
ripple inside any band drops from $8.4\times10^{-3}$ dB to under $10^{-5}$ dB.
Tones run through the bank read the graded images to within 0.01 dB, more than
120 dB down.

```python
from phonometry import filters

bank = filters.OctaveFilterBank(fs=16000, fraction=1, limits=[12, 5000])
print([int(m) for m in bank.factor[:4]])   # [22, 11, 5, 2]: decimated bands
result = filters.verify_filter_class(bank)
print(result.requirement_class("relative_attenuation"))   # 1
band = result.bands[0]
print(band["checked_to_omega"] * band["freq"])
# 8000.0: half the input rate, not the 364 Hz of the decimated rate
```

The Table 1 acceptance mask itself is public too: `class_limits(fraction,
filter_class, omega)` returns the minimum/maximum relative-attenuation
limits at normalized frequencies $\Omega = f/f_\mathrm{m}$, the same limits the verifier
and the figure below use.

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/class_mask_overlay.svg" alt="Butterworth band response threading between the forbidden regions of the IEC 61260-1 class 1 acceptance mask" width="80%"></picture>

*The order-6 Butterworth response (blue) threads between the forbidden
regions: it must attenuate at least the red mask outside the band and no more
than the purple mask inside it.*

<details>
<summary>Show the code for this figure</summary>

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import sosfreqz
from phonometry import filters

fs = 48000
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200])
idx = int(np.argmin(np.abs(np.array(bank.freq) - 1000)))
fm, fsd = bank.freq[idx], fs / bank.factor[idx]
w, h = sosfreqz(bank.sos[idx], worN=2**15, fs=fsd)
att = -20 * np.log10(np.abs(h) + 1e-12)
delta_a = att - np.interp(fm, w, att)     # relative attenuation

grid = np.logspace(np.log10(0.05), np.log10(8), 2000)
lo1, hi1 = filters.class_limits(1.0, 1, grid)     # class 1 min/max attenuation

fig, ax = plt.subplots(figsize=(9, 5.5))
ax.fill_between(grid, -10, lo1, alpha=0.15, color="tab:red",
                label="Forbidden: too little attenuation")
finite = np.isfinite(hi1)
ax.fill_between(grid[finite], hi1[finite], 90, alpha=0.15, color="tab:purple",
                label="Forbidden: too much attenuation")
ax.plot(w / fm, delta_a, label="Butterworth order 6")
ax.set(xscale="log", xlim=(0.08, 8), ylim=(-6, 90),
       xlabel="Normalized frequency f / fm",
       ylabel="Relative attenuation [dB]")
ax.legend()
plt.show()
```

</details>

With default parameters (order 6), **Butterworth meets the class 1 mask of
Table 1**, and so does
**Chebyshev II**: its `attenuation` default is now `72` dB, clearing the 70 dB
far-stopband class 1 limit (scipy pins the cheby2 equiripple floor at exactly
`attenuation`, so any value $\ge 70\ \text{dB}$ qualifies; the 72 dB default
keeps the same +0.400 dB passband margin as Butterworth). Chebyshev I,
Elliptic and Bessel do
not meet class limits at order 6: passband ripple (cheby1/ellip) and slow
roll-off (bessel) violate the mask. Whether a whole bank is class 1 also
depends on the two requirements of section 1b, and the default octave and
one-third-octave banks meet class 1 there too.

## 1b. Effective bandwidth and the summation of outputs (IEC 61260-2)

Table 1 judges a band one frequency at a time. IEC 61260-1:2014 adds two
requirements that judge a band, and a set of bands, as a whole, and
IEC 61260-2:2016 says how a pattern-evaluation laboratory computes them from
the same relative attenuation. Neither needs anything but the designed
response, so for the 2014 edition `verify_filter_class` grades both, and a
band's `class` is the strictest class it meets on all three.

**Effective bandwidth (5.12).** For broadband sound a band analyser reports
the power its band lets through, so its width in power is what matters. The
normalized effective bandwidth is Formula (13) of IEC 61260-1,

$$
B_\mathrm{e} = \int_0^\infty \frac{1}{\Omega}\,10^{-0.1\,\Delta A(\Omega)}\,\mathrm{d}\Omega ,
$$

and its deviation from the bandwidth of an ideal band,
$B_\mathrm{r} = (1/b)\ln G$, is $\Delta B = 10\lg(B_\mathrm{e}/B_\mathrm{r})$,
within ±0.4 dB for class 1 and ±0.6 dB for class 2 (5.12.2). A band with
$\Delta B = +0.05$ dB reads pink noise 0.05 dB high. The verifier evaluates the
integral as IEC 61260-2 7.2.3.2 recommends: the trapezoidal rule of its
Formula (2) over the test frequencies of its Formula (1),

$$
\Omega_i = G^{i/(bS)} ,
$$

with $S$ at least 24 frequencies per bandwidth (`points_per_bandwidth`, 24 by
default), carried out to the outermost breakpoint of Table 1, where a class 1
band is at least 70 dB down.

**Summation of output signals (5.16).** A tone between two mid-band
frequencies has to come out of the set with its power, shared between the
bands that see it, neither lost nor added. Formula (3) of IEC 61260-2 sums on
an energy basis the relative attenuation of band $j$ and of its two
neighbours, at the test frequencies inside band $j$
($|i| \le \lfloor S/2 \rfloor$):

$$
\Delta P_j = 10\lg\left[10^{-0.1\,\Delta A_{j-1}} + 10^{-0.1\,\Delta A_j} + 10^{-0.1\,\Delta A_{j+1}}\right]
$$

and the result has to stay between −1.8 dB and +0.8 dB for class 1, and between
−3.8 dB and +1.8 dB for class 2. The two end bands, with a neighbour on one side
only, are left out (7.2.4.4), so the requirement is graded only on a bank of
three bands or more, and `result.requirements` says whether it was. The printed
Formula (3) and the words of 7.2.4.3 and of 5.16 take the difference in
opposite directions, which with limits this lopsided is not the same test; the
verifier applies the limits to the formula as printed, as 7.2.4.5 instructs,
and the [errata registry](https://docs.jmrp.io/phonometry/reference/errata/) records the conflict.

```python
from phonometry import filters

third = filters.verify_filter_class(
    filters.OctaveFilterBank(48000, fraction=3, order=6, limits=[125, 4000]))
print(third.overall_class)                                  # 1

octave = filters.verify_filter_class(
    filters.OctaveFilterBank(48000, fraction=1, order=6, limits=[125, 4000]))
print(octave.requirement_class("relative_attenuation"))     # 1
print(octave.requirement_class("effective_bandwidth"))      # 1
print(octave.requirement_class("summation"))                # 1
print(round(octave.binding_margin_db("summation", 1), 2))   # 0.64
print(octave.overall_class)                                 # 1
```

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_summation_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_summation.svg" alt="Two panels of the summed output of adjacent bands against normalized frequency, from the lower to the upper band edge, with the class 1 limits dashed at +0.8 dB and −1.8 dB and the class 2 limits dotted at +1.8 dB and −3.8 dB. On the left, the decimated octave bank from 125 Hz to 4 kHz: every inner band sums to about 0 dB across the middle of the band and rises in a narrow bump just inside each band edge, to about +0.13 dB near the lower edge and to between +0.14 dB and +0.16 dB near the upper edge, before returning to 0 dB at the edges themselves; the 1995 Hz band, drawn heavier as the binding band, reaches +0.16 dB, far inside class 1. On the right, the one-third-octave bank over the same range traces the same shape from 0.00 dB to +0.16 dB, its 3162 Hz band the binding one, inside class 1" width="100%"></picture>

*Formula (3) of IEC 61260-2 on every inner band of the two default banks, drawn
by `result.plot(requirement="summation")`. Both banks return a tone's power to
within 0.16 dB: the sum stays within a few hundredths of a decibel of 0 dB
across the middle of each band and rises by a little more than a tenth of a
decibel just inside its edges, where two neighbouring bands share the tone.*

<details>
<summary>Show the code for this figure</summary>

```python
import matplotlib.pyplot as plt
from phonometry import filters

octave = filters.verify_filter_class(
    filters.OctaveFilterBank(48000, fraction=1, order=6, limits=[125, 4000]))
third = filters.verify_filter_class(
    filters.OctaveFilterBank(48000, fraction=3, order=6, limits=[125, 4000]))
fig, (ax_oct, ax_third) = plt.subplots(1, 2, figsize=(13, 6.2), sharey=True)
octave.plot(ax=ax_oct, requirement="summation")
third.plot(ax=ax_third, requirement="summation")
ax_oct.set_title("Octave bank, decimated: class 1 on §5.16")
ax_third.set_title("One-third-octave bank: class 1 on §5.16")
ax_third.set_ylabel("")
plt.show()
```

</details>

**Why the bank decimates a band only to sixteen times its upper edge.** The
summation is the requirement a multirate design puts at risk. Close to its own
Nyquist frequency the bilinear transform bends a band: just inside its upper
band edge it attenuates less than the same band designed far from Nyquist, and
above that edge it falls more steeply. Just below an upper band edge the band
and its neighbour above then pass more than the tone's power; just above a
lower band edge the band below has already fallen away and power is lost. An
octave bank (Butterworth, order 6, 48 kHz) decimated until each band's
processing Nyquist frequency was only 1.25 times its upper band edge sums its
adjacent outputs from −1.16 dB to +0.94 dB about the input: past the +0.8 dB of
class 1, a class 2 bank. The bend shrinks as the square of that ratio: it moves
the summation of an octave band by 0.074 dB at a ratio of four, 0.018 dB at
eight and 0.005 dB at sixteen, and that of a one-third-octave band by 0.002 dB
at sixteen. So the bank stops decimating a band while its processing Nyquist
frequency is still at least sixteen times its upper band edge, and its
decimated banks sum as the same banks designed at the full rate
(`design=filters.FilterDesign(resample=False)`): class 1 on every requirement
at 44.1, 48 and 96 kHz. Over its full default range at 48 kHz the octave bank
sums from −0.06 dB to +0.69 dB, decimated or not, the top set by the 8 kHz
band next to the 16 kHz band, which runs at the full rate close to the 24 kHz
Nyquist frequency: class 1 with 0.11 dB to spare. The one-third-octave bank
sums from −0.25 dB to +0.76 dB, both at its 16 kHz band, a margin of 0.04 dB,
and the decimated Chebyshev II octave bank from −0.08 dB to +0.64 dB. None of
this is an artefact of grading transfer functions: tones run through the bank
itself, its decimation filters included, read the Formula (3) sums to within
0.01 dB.

## 1c. Time-invariant operation (5.14)

A multirate bank does not filter a tone the same way wherever it falls against
the decimation, and a transfer function cannot show that. IEC 61260-1:2014
5.14 tests it on the running filter instead: a sinusoid of constant amplitude
whose frequency rises one decade in 2 s to 5 s, and each band's time-averaged
output has to stay within ±0.4 dB (class 1) or ±0.6 dB (class 2) of the level
of Formula (17),

$$
L_\mathrm{c} = L_\mathrm{in} - A_\mathrm{ref} + 10\lg\left[\frac{T_\mathrm{sweep}}{T_\mathrm{avg}}\,\frac{\lg(f_2/f_1)}{\lg(f_\mathrm{end}/f_\mathrm{start})}\right]
$$

the output of an ideal band of the same bandwidth. `swept_band_level` is that
formula (107.97 dB for the example Annex B of IEC 61260-2 and -3 works
through), and `swept_level_uncertainty` its standard uncertainty from the
uncertainties of the sweep (Annex A). `verify_time_invariance` runs the test of
IEC 61260-2 7.4 on a bank: the sweep of Formulas (A.3) and (A.4) goes through
every band exactly as `OctaveFilterBank.filter` processes a signal, polyphase
decimation included, at 2 s and at 5 s per decade.

```python
from phonometry import filters

bank = filters.OctaveFilterBank(48000, fraction=3, order=6, limits=[25, 10000])
swept = filters.verify_time_invariance(bank)
print(swept.overall_class)                       # 1
print(round(swept.worst_deviation_db, 3))        # 0.056
print(swept.seconds_per_decade)                  # (2.0, 5.0)
print(round(filters.swept_band_level(
    127.0, fraction=3, sweep_duration_s=30, averaging_time_s=30,
    start_frequency_hz=0.01, end_frequency_hz=1e6), 2))   # 107.97
```

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_time_invariance_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_time_invariance.svg" alt="The deviation of each band's swept output from the level of Formula (17), for the one-third-octave bank from 25 Hz to 10 kHz, at 2 s and at 5 s per decade, between the class 1 limits dashed at ±0.4 dB and the class 2 limits dotted at ±0.6 dB. Every band at both rates sits at about +0.05 dB, the two rates on top of each other, so the bank is class 1" width="90%"></picture>

*Every band of the decimated one-third-octave bank reads about +0.05 dB at both
sweep rates. That is its effective bandwidth deviation of section 1b, which is
what Annex G of IEC 61260-1 says a band that behaves as its transfer function
must read (G.2.8); decimation that folded energy back into a band would read
more, and the two rates would part.*

<details>
<summary>Show the code for this figure</summary>

```python
import matplotlib.pyplot as plt
from phonometry import filters

bank = filters.OctaveFilterBank(48000, fraction=3, order=6, limits=[25, 10000])
filters.verify_time_invariance(bank).plot()
plt.show()
```

</details>

## 2. Class 0 (IEC 61260:1995 / ANSI S1.11-2004)

The tightest performance class, **class 0**, was defined by the earlier
**IEC 61260:1995** and its US twin **ANSI S1.11-2004** (both withdrawn/superseded
but still referenced for laboratory-grade instruments); IEC 61260-1:2014 dropped
it. Its class 1/2 masks differ slightly from the 2014 edition, so it lives behind
an `edition` switch rather than being mixed into the 2014 mask:

```python
from phonometry import filters

fs = 48000
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200])

result = filters.verify_filter_class(bank, edition="1995")   # classes 0, 1, 2
print(result.overall_class)          # 0  (the default Butterworth clears it)
print(result.bands[0]["margin_class0_db"])
```

The 1995 edition is graded on all its clauses on the transfer function, not on
Table 1 alone. Its **filter integrated response** (4.5) is the quotient the
2014 edition calls the effective bandwidth deviation,
$\Delta B = 10\lg(B_\mathrm{e}/B_\mathrm{r})$, built its own way: equation (14)
integrates $10^{-0.1\,\Delta A}$ over $f/f_\mathrm{m}$ with no $1/\Omega$
weight, by the trapezoidal rule of equation (16) over at least $5S$ test
frequencies on each side (5.4.2), with $S$ raised from the 24 asked for in
steps of 12 until every band's response reads the same to the nearest tenth of
a decibel at $S$ and at $S + 12$ (5.3.3; the result's `points_per_bandwidth`
says where it stopped, and the summation runs at it too), against the reference
$B_\mathrm{r} = G^{1/(2b)} - G^{-1/(2b)}$ of equation (9), and 4.5.3 holds it
within ±0.15 dB, ±0.3 dB and ±0.5 dB for classes 0, 1 and 2. Its **summation of
output signals** (4.9) is equation (19), a band and its two neighbours summed
on an energy basis, held within ±1.0 dB for class 0, +1.0 dB and −2.0 dB for
class 1 and +2.0 dB and −4.0 dB for class 2, and run, as 5.8.4 asks, from the
lowest mid-band frequency to the highest: the end bands are read on the half
facing the set, where the neighbour the set lacks adds nothing. The default
Butterworth bank meets class 0 on all three:

```python
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[125, 4000])
result = filters.verify_filter_class(bank, edition="1995")
print({r: result.requirement_class(r) for r in result.requirements})
# {'relative_attenuation': 0, 'effective_bandwidth': 0, 'summation': 0}
print(max(b["bandwidth_deviation_db"] for b in result.bands))   # about +0.05 dB
print(result.points_per_bandwidth)   # 36: 5.3.3 raised S from 24
```

Two defects of the printed edition are in the [errata registry](https://docs.jmrp.io/phonometry/reference/errata/): the normalizing
constant of 3.15 carries a stray factor of ten, and the words of 4.9 and 5.8.3
take the summation difference the other way round from equation (19), which
the library follows as 5.8.5 instructs.

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_class0_mask_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_class0_mask.svg" alt="Nested pass-band acceptance corridors for class 0, 1 and 2 of IEC 61260:1995 with the order-6 Butterworth response sitting inside the tightest class 0 corridor" width="80%"></picture>

*The class 0 corridor (±0.15 dB at mid-band) is the tightest; class 1 (±0.3 dB)
and class 2 (±0.5 dB) are progressively wider. The order-6 Butterworth threads
inside class 0 across the whole pass-band.*

<details>
<summary>Show the code for this figure</summary>

```python
import matplotlib.pyplot as plt
import numpy as np
from scipy.signal import sosfreqz
from phonometry import filters

fs = 48000
bank = filters.OctaveFilterBank(fs, fraction=1, order=6, limits=[800, 1200])
idx = int(np.argmin(np.abs(np.array(bank.freq) - 1000)))
fm, fsd = bank.freq[idx], fs / bank.factor[idx]
w, h = sosfreqz(bank.sos[idx], worN=2**15, fs=fsd)
att = -20 * np.log10(np.abs(h) + 1e-12)
delta_a = att - np.interp(fm, w, att)

# Pass-band only: outside the band edges the maximum limit is +inf.
g = 10 ** (3 / 10)
grid = np.linspace(g ** -0.5, g ** 0.5, 1500)
pb = (w / fm >= g ** -0.5) & (w / fm <= g ** 0.5)

fig, ax = plt.subplots(figsize=(9, 5.5))
for cls in (2, 1, 0):                      # nested corridors, class 0 tightest
    lo, hi = filters.class_limits(1.0, cls, grid, edition="1995")
    ax.plot(grid, hi, label=f"Class {cls} corridor")
    ax.plot(grid, lo, color=ax.lines[-1].get_color())
ax.plot(w[pb] / fm, delta_a[pb], "k", lw=2, label="Butterworth order 6")
ax.set(xscale="log", xlim=(g ** -0.5, g ** 0.5), ylim=(-0.7, 6),
       xlabel="Normalized frequency f / fm",
       ylabel="Relative attenuation [dB]")
ax.legend()
plt.show()
```

</details>

## 3. What a class means physically

The masks are worst-case error bounds on a *measurement*, not abstract
grades:

- **In the passband** the corridor bounds how much the band can mis-read
  in-band content: a class 1 bank reads a mid-band tone within ±0.4 dB of
  its true level and a class 2 bank within ±0.6 dB (2014 Table 1; the
  stricter 1995 masks allowed ±0.3 dB for class 1 and ±0.15 dB for
  class 0). Toward the band edges the corridor widens, which is the honest
  admission that a tone sitting exactly on an edge is genuinely ambiguous
  between two bands (both read it about 3 dB down).
- **In the stopband** the minimum-attenuation mask bounds leakage from the
  rest of the spectrum: far from the band, class 1 demands at least 70 dB of
  relative attenuation (the reason the `cheby2` default is 72 dB). In energy
  terms, an out-of-band tone must be roughly 70 dB stronger than the band's
  own content before it doubles the band's energy reading (+3 dB). The
  practical consequence: measuring bands far below a dominant tone, the
  reading floors out at the leakage skirt about 70 dB down, and a steeper
  architecture (or higher order) is the only way to push that floor lower.
- **For the uncertainty budget**, the class is the filter's contribution to the
  measurement uncertainty: a class 1 bank adds up to a few tenths of a dB to
  a band level, comparable to a class 1 sound level meter's other tolerance
  terms, which is why instrument-grade chains specify the class of every
  stage rather than a single overall figure.

**Which architecture reaches which class?** The library's **default, Butterworth
order 6, meets class 0** in the configurations the conformance suite verifies
(octave and third-octave banks at 48 kHz), so no special setup is needed for
laboratory-grade banks in that range. The table below reports the best class
each architecture reaches under that same order-6 / 48 kHz setup; the other
architectures fall short of class 0 because they trade the IEC mask for a
different property *by construction*:

| Architecture | Best class (order 6, fs 48 kHz) | Why |
| :--- | :---: | :--- |
| `butter` (default) | **0** | Maximally-flat pass-band, monotone roll-off; fits the mask |
| `cheby2` | 1 | Flat pass-band but the mask relationship binds at class 1 |
| `cheby1` | — | Pass-band ripple violates the flatness limit |
| `ellip` | — | Pass- and stop-band ripple |
| `bessel` | — | Flat group delay bought with a slow roll-off |

So the sensible default is the common one (Butterworth order 6): it clears
class 0 in the verified configurations, while the alternative architectures are
deliberate opt-ins whose purpose (steeper roll-off, linear phase) works against
the class mask. Away from these settings (very high `fraction` or near-Nyquist
bands), always re-run `verify_filter_class` to confirm the class you need, and
raise the order if a band needs more margin.

## 3b. Periodic tests: grading a laboratory's results (IEC 61260-3)

`verify_filter_class` answers a question about a *design*. A working
instrument receives the **periodic tests** of IEC 61260-3:2016 in a
laboratory, on a date, and they are the laboratory's to run; the
verdict on what it measured is arithmetic, and `verify_filter_periodic` does
it. Each result goes in with the laboratory's expanded uncertainty, and each is
judged by the conformance rule of IEC TC 29 (`metrology.verify_conformance`):
the deviation within its acceptance limits and the uncertainty within the
maximum IEC 61260-1:2014 Annex B permits for that test. Clause by clause:

| Clause | What the laboratory measured | Acceptance limits | Maximum $U$ (Annex B) |
| :--- | :--- | :--- | :--- |
| 10.2 | relative attenuation of every filter at its exact mid-band | ±0.4 dB class 1, ±0.6 dB class 2 | 0.20 dB, or 0.30 dB past a $\Delta A$ of 2 dB |
| 10.3 | or, time-invariant filters, $\Delta B$ from one sweep | ±0.4 dB class 1, ±0.6 dB class 2 | 0.20 dB |
| 11.7 | level linearity on the reference range | ±0.5 dB class 1, ±0.6 dB class 2 within 40 dB of the upper boundary; ±0.7 dB and ±0.9 dB beyond | 0.20 dB within 40 dB, 0.35 dB beyond |
| 11.9 | level linearity on each other range, 30 dB below its upper boundary | as 11.7 | as 11.7 |
| 13 | relative attenuation of three filters at up to 15 $\Omega_k$ | IEC 61260-3 Table 1 | 0.20, 0.30 or 0.50 dB as $\Delta A$ is up to 2 dB, up to 40 dB, or more |

The fifteen frequencies of clause 13 come from Formulas (1) and (2) of
IEC 61260-3 for any bandwidth designator, `periodic_test_frequencies(b)`, and
its Table 1 is `PERIODIC_TEST_ATTENUATION_LIMITS_DB`, the stop-band rows with
no maximum. A laboratory drops the frequencies that fall outside the range of
the set (13.4) by writing NaN in their place.

```python
from phonometry import filters

omega = filters.periodic_test_frequencies(3)
print(round(float(omega[8]), 5))                          # 1.02667, Table C.1
print(filters.PERIODIC_TEST_ATTENUATION_LIMITS_DB[1][7])  # (70.0, inf)

row = [76.0, 63.0, 45.0, 20.0, 0.8, 0.3, 0.1, 0.0, 0.1, 0.2, 0.7, 19.0, 44.0, 63.0, 77.0]
row_u = [0.4, 0.4, 0.4, 0.25, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.25, 0.4, 0.4, 0.4]
record = filters.FilterPeriodicMeasurements(
    midband_attenuations_db=[0.12, -0.05, 0.08, 0.02, -0.1, 0.15],
    midband_uncertainties_db=[0.15] * 6,
    linearity_deviations_db=[0.0, 0.1, 0.2, -0.3, 0.4],
    linearity_levels_below_upper_db=[0.0, 10.0, 20.0, 45.0, 55.0],
    linearity_uncertainties_db=[0.12, 0.12, 0.25, 0.2, 0.3],
    relative_attenuations_db=[row, [x + 0.2 for x in row], row],
    relative_attenuation_uncertainties_db=[row_u, row_u, row_u],
)
verdict = filters.verify_filter_periodic(1, record, fraction=3)
print(verdict.passes)       # False
print(verdict.unusable)     # (('11.7', '20 dB below the upper boundary'),)
print(verdict.failed)       # ()
```

Every deviation of that record is inside its limits, and the verdict is still
not a pass: one linearity reading was taken with 0.25 dB of expanded
uncertainty where Annex B allows 0.20 dB, and 5.3 of IEC 61260-3 forbids using
it. `verdict.statement` says so in the words of the standard, and once every
result is usable it becomes the statement Clause 14 prescribes: 14 k) when the
model's pattern approval is public (`pattern_approval_public=True`), 14 l)
otherwise, with the caveat of 1.5 that no general conclusion about
IEC 61260-1 follows from the periodic tests alone.

A pass also needs the tests to cover what the standard asks of them: 11.3 and
13.1 measure three filters, and 13.4 measures each of them at every test
frequency above 0.5 times the lowest mid-band frequency of the set and below
1.5 times the highest. Given the mid-band frequencies of the set
(`set_midband_frequencies_hz`), of the three tested filters
(`tested_midband_frequencies_hz`) and of the filter behind each linearity
result (`linearity_midband_frequencies_hz`), the verdict counts them, and
`verdict.incomplete` lists each shortfall, a NaN where 13.4 asks for a
measurement included; it holds the pass back as a missing clause does. A
record without them cannot be checked for coverage: `verdict.coverage_checked`
is `False`, and a passing statement ends by saying the coverage was not
checked. A mid-band attenuation of 10.2 takes its Annex B maximum from what
was measured, as clause 13 does: a filter that has drifted past 2 dB at its
mid-band is allowed 0.30 dB, and so fails rather than being unusable.

<picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_periodic_verdict_dark.svg"><img src="https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/images/filter_periodic_verdict.svg" alt="The margin of every result of a class 1 periodic test to its nearer acceptance limit, grouped by clause: six mid-band results of 10.2 between 0.25 dB and 0.38 dB, five level-linearity results of 11.7 and forty-five relative attenuations of clause 13, on a scale linear up to 1 dB and logarithmic above. Every result lies above the red acceptance line at zero with its expanded uncertainty drawn as a bar, and all are green diamonds except one 11.7 result drawn as a hollow orange circle, unusable under 5.3 because its uncertainty exceeds the Annex B maximum. The title reads not passed" width="100%"></picture>

*`verdict.plot()`: every result's margin to its nearer limit with its expanded
uncertainty, clause by clause. The hollow circle is the one result 5.3 makes
unusable, and it alone holds the verdict back.*

<details>
<summary>Show the code for this figure</summary>

```python
import matplotlib.pyplot as plt

# `verdict` is the result of the snippet above.
verdict.plot()
plt.show()
```

</details>

What the periodic grader leaves to the laboratory report: the self-generated
noise of Clause 12, for which Annex B sets no maximum uncertainty, the overload
indication of 11.5 and 11.8, and the observations of Clauses 4 and 6 to 8
(preliminary inspection, environmental conditions, the manual) are not graded;
record them beside the verdict.

The equivalent regime for sound level *meters* (IEC 61672-3 periodic tests,
and IEC 60942 for the calibrator) is in
[Periodic tests of a sound level meter](https://docs.jmrp.io/phonometry/signals/metrology/sound-level-meter-periodic-tests/)
and [Calibration and dBFS](https://docs.jmrp.io/phonometry/signals/metrology/calibration/).

## 4. The compliance fiche (`.report()`)

`verify_filter_class(bank)` returns a result object that exposes `.plot()`
and `.report()`, so a type-test verdict can be rendered as a one-page
accredited fiche: a per-band classification table with each band's binding
margin over every requirement graded, a table of the requirements of sections
1 and 1b with each one's class, binding margin and range, the worst-margin
band's measured relative attenuation overlaid on the class corridor, and the
boxed overall class-compliance result. Pass a `required_class`
on the `ReportMetadata` to add a PASS/FAIL verdict row (a bank "meets class N"
when its achieved class is at least as strict, i.e. a class index of N or
lower). The fiche renders in English by default; pass `language="es"` for a
Spanish fiche (translated fixed strings and a comma decimal separator), e.g.
`result.report("iec61260_es.pdf", language="es")`.

The example is the decimated octave bank from 125 Hz to 4 kHz, the
configuration section 1b shows to be class 1 on every requirement:

```python
from phonometry import ReportMetadata, filters

bank = filters.OctaveFilterBank(fs=48000, fraction=1, order=6, limits=[125, 4000])
result = filters.verify_filter_class(bank)   # overall_class == 1
result.plot()   # the worst-margin band on its class corridor

result.report(
    "iec61260.pdf",
    metadata=ReportMetadata(
        specimen="1/1-octave filter bank",
        measurement_standard="IEC 61260-1:2014",
        required_class=1,                # class 1 (or stricter) required
    ),
)                                        # -> Class 1 - COMPLIES, PASS
```

The example fiche, regenerated with `make reports`, is kept rendered in the
repository. Click the preview to open the PDF:

[![One-page filter-class-compliance fiche: a metadata header, a per-band classification table listing each octave band's achieved class and binding margin, the worst-margin band's measured relative attenuation overlaid on the green class-1 acceptance corridor with the three requirements graded beneath it, the boxed Class 1 - COMPLIES result and a PASS verdict against the required class 1](https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/reports/iec61260_filter_example.webp)](https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/reports/iec61260_filter_example.pdf)

*Filter class compliance fiche (`FilterComplianceResult.report`), the achieved
class with its binding margin in dB, the measured relative attenuation over
the IEC 61260-1:2014 Table 1 corridor, and the class of each requirement
graded.*

Passing `edition="1995"` verifies against the older IEC 61260:1995 /
ANSI S1.11-2004 mask, which keeps the stricter **class 0** that the 2014 edition
dropped, together with the filter integrated response of 4.5.3 and the
summation of 4.9; the default order-6 Butterworth bank can then be certified to
class 0 on all three, the fiche listing each under its 1995 clause:

```python
bank = filters.OctaveFilterBank(fs=48000, fraction=1, order=6, limits=[250, 4000])
result = filters.verify_filter_class(bank, edition="1995")   # overall_class == 0
result.plot()   # the class-0 corridor of the 1995 edition
result.report(
    "iec61260_1995.pdf",
    metadata=ReportMetadata(
        measurement_standard="IEC 61260:1995",
        required_class=0,                # class 0 (1995 edition) required
    ),
)                                        # -> Class 0 - COMPLIES, PASS
```

[![One-page filter-class-compliance fiche under the 1995 edition: a per-band classification table showing every octave band achieving class 0, the measured relative attenuation overlaid on the green class-0 acceptance corridor with the three requirements of the 1995 edition graded beneath it, the boxed Class 0 - COMPLIES (margin +0.10 dB) result and a PASS verdict against the required class 0](https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/reports/iec61260_filter_1995_example.webp)](https://raw.githubusercontent.com/jmrplens/phonometry/main/.github/reports/iec61260_filter_1995_example.pdf)

*Class 0 is retained by the IEC 61260:1995 / ANSI S1.11-2004 edition
(`edition="1995"`); the 2014 edition keeps only classes 1 and 2.*

## Quick answers

### Which filter architecture meets IEC 61260-1 class 1 with default settings?

With the default order 6, Butterworth meets class 1 of the IEC 61260-1:2014
Table 1 acceptance limits, and so does Chebyshev II: its default
`attenuation` of 72 dB clears the 70 dB far-stopband class 1 limit.
Chebyshev I, Elliptic and Bessel do not: passband ripple (cheby1, ellip)
and slow roll-off (bessel) violate the mask. `verify_filter_class` reports
the achieved class per band, on Table 1 and on the effective bandwidth and
summation requirements too.

### Is the default octave bank class 1 under IEC 61260-1:2014?

Yes, on every requirement it is graded on: the Table 1 mask, the effective
bandwidth (5.12) and the summation of output signals (5.16). The decimated
Butterworth octave bank sums adjacent outputs within −0.06 dB and +0.69 dB of
the input over its default range at 48 kHz, inside the −1.8 dB and +0.8 dB of
class 1 with 0.11 dB to spare, exactly as the same bank designed at the full
rate with `FilterDesign(resample=False)`: the bank decimates a band only as
far as its processing Nyquist frequency stays at least sixteen times its upper
band edge, where the decimation moves the summation by at most 0.005 dB. The
one-third-octave bank is class 1 on every requirement too, with 0.04 dB to
spare on the summation.

### What is class 0 and which standard defines it?

Class 0 is the tightest filter performance class, defined by IEC 61260:1995
and its US twin ANSI S1.11-2004 and dropped by IEC 61260-1:2014. Its
passband corridor allows only ±0.15 dB at mid-band, against ±0.3 dB for
class 1 in the 1995 masks. It stays available through `edition="1995"`, and
the default order-6 Butterworth bank meets class 0 in the verified 48 kHz
configurations, on Table 1, on the filter integrated response of 4.5.3 and on
the summation of 4.9, its alias images graded too.

## See also

- [Filter Banks](https://docs.jmrp.io/phonometry/signals/filters/filter-banks/): the band mathematics and the
  architectures whose class is verified here.
- [Frequency Weighting](https://docs.jmrp.io/phonometry/signals/levels/weighting/): the companion verification of the A,
  C and Z curves against the IEC 61672-1 tolerance tables, with the G, B, D and
  AU curves covered in [Special Weightings](https://docs.jmrp.io/phonometry/signals/levels/special-weightings/).
- [Sound level meter](https://docs.jmrp.io/phonometry/signals/sound-level-meter/): the instrument chain whose
  spectrum stage this class applies to.
- [Conformance report](https://docs.jmrp.io/phonometry/reference/conformance/): the verified configurations behind
  the class claims of this page.
- API reference: [`filters.compliance`](https://docs.jmrp.io/phonometry/reference/api/filters/compliance/),
  [`filters.time_invariance`](https://docs.jmrp.io/phonometry/reference/api/filters/time-invariance/)
  and [`filters.periodic_tests`](https://docs.jmrp.io/phonometry/reference/api/filters/periodic-tests/).

## References

- International Electrotechnical Commission. (2014). *Electroacoustics —
  Octave-band and fractional-octave-band filters — Part 1: Specifications*
  (IEC 61260-1:2014).
  [IEC webstore](https://webstore.iec.ch/en/publication/5063).
  The Table 1 class 1 / class 2 acceptance limits verified here, with the
  fractional-octave breakpoint mapping and the log-frequency interpolation
  of the standard.
- International Electrotechnical Commission. (1995). *Electroacoustics —
  Octave-band and fractional-octave-band filters* (IEC 61260:1995).
  [IEC webstore](https://webstore.iec.ch/en/publication/5065).
  The withdrawn first edition whose Table 1 supplies the stricter class 0
  mask offered by `edition="1995"`.
- American National Standards Institute. (2004). *Specification for
  octave-band and fractional-octave-band analog and digital filters*
  (ANSI S1.11-2004). Acoustical Society of America.
  [ANSI webstore](https://webstore.ansi.org/standards/asa/ansis1112004).
  Its Table 1 class limits are identical to those of IEC 61260:1995 and back
  the same class 0 mask.
- International Electrotechnical Commission. (2016). *Electroacoustics —
  Octave-band and fractional-octave-band filters — Part 2: Pattern-evaluation
  tests* (IEC 61260-2:2016).
  [IEC webstore](https://webstore.iec.ch/en/publication/24560).
  Its Formulas (1) to (3) are how `verify_filter_class` grades 5.12 and 5.16
  of Part 1, and its swept test of 7.4, with Annexes A and B, is
  `verify_time_invariance`, `swept_band_level` and `swept_level_uncertainty`.
- International Electrotechnical Commission. (2016). *Electroacoustics —
  Octave-band and fractional-octave-band filters — Part 3: Periodic tests*
  (IEC 61260-3:2016). [IEC webstore](https://webstore.iec.ch/en/publication/24561).
  The periodic tests whose results `verify_filter_periodic` grades, with the
  Formulas (1) and (2) of `periodic_test_frequencies` and the Table 1 of
  `PERIODIC_TEST_ATTENUATION_LIMITS_DB`.

## Standards

IEC 61260-1:2014, *Electroacoustics — Octave-band and
fractional-octave-band filters — Part 1: Specifications*: the Table 1
class 1 / class 2 acceptance limits (with the fractional-octave breakpoint
mapping and log-frequency interpolation) verified in §1, and the effective
bandwidth (5.12), time-invariant operation (5.14), summation of output signals
(5.16) and conformance rule of Annex B graded in §1b, §1c and §3b.
IEC 61260-2:2016, *Pattern-evaluation tests*: Formulas (1) to (3) and the swept
test of 7.4 with Annexes A and B, in §1b and §1c.
IEC 61260-3:2016, *Periodic tests*: the clauses graded in §3b, Formulas (1) and
(2), Table 1 and the statements of Clause 14.
IEC 61260:1995 and ANSI S1.11-2004, *Octave-Band and Fractional-Octave-Band …
Filters*: the withdrawn edition's Table 1 (identical between the two)
supplies the stricter class 0 mask offered by ``edition="1995"`` and
verified in §2, with the filter integrated response of its 4.5.3 and the
summation of output signals of its 4.9; its 4.8 and IEC 61260-1:2014 5.15,
which bound the alias images of a decimated band, in §1.

**Not covered.** The tests themselves on a physical filter: the specimens,
climate, immunity, overload and linearity tests of **IEC 61260-2:2016** and the
measurements of **IEC 61260-3:2016** are a laboratory's to run, and
`verify_filter_periodic` grades the numbers it returns without producing them.
Of those, the self-generated noise of IEC 61260-3 Clause 12 and the overload
indications of 11.5 and 11.8 are not graded, nor, in the 1995 edition, the
requirements a physical instrument answers (linearity, real-time operation,
environment). Near Nyquist the bilinear transform warps the frequency axis and
the bank carries no correction for it, and above half the input rate a digital
bank has no frequency to respond at, so the stopband mask beyond it is reported
as `range_limited` rather than verified.
