Severity: high
Domain: filters
Status: VERIFIED — read + hand-traced against a210d34 on 2026-08-10
Suggested labels: bug, numerical-correctness, filters
Summary
HighPassRoots() maps the analog prototype pole with wc / p.Real() for the real part while
correctly using |p|² for the imaginary part. The low-pass→high-pass transform requires
ωc · Re(p) / |p|². The asymmetry between the two components makes this a clear transcription slip.
Location
numerical/filters/passive/IirFilterDesign.hpp
Evidence
for (std::size_t k{ 0 }; k < order; ++k)
{
const T mag2{ protoPoles[k].Real() * protoPoles[k].Real()
+ protoPoles[k].Imaginary() * protoPoles[k].Imaginary() };
const ComplexT hpPole{ wc / protoPoles[k].Real(), // WRONG
-wc * protoPoles[k].Imaginary() / mag2 }; // correct
az[k] = BilinearS2Z(hpPole, fs);
bz[k] = { T{ 1 }, T{ 0 } };
}
Expected
The transform s → ωc/s maps a pole p to
ωc / p = ωc · conj(p) / |p|² = ( ωc·Re(p)/|p|² , −ωc·Im(p)/|p|² )
so the real part must also divide by mag2.
Trace
Butterworth prototype pole p = −0.7071 + 0.7071j, |p|² = 1:
correct: ωc·(−0.7071)/1 = −0.7071·ωc
code: ωc/(−0.7071) = −1.4142·ωc
The pole is placed at twice the correct radius, so both the cutoff frequency and the Q of the
resulting section are wrong.
Suggested fix
const ComplexT hpPole{ wc * protoPoles[k].Real() / mag2,
-wc * protoPoles[k].Imaginary() / mag2 };
Add a test asserting the −3 dB point of a designed high-pass lands at the requested cutoff.
Notes
The band-pass/band-stop paths in the same file have separate, unverified concerns about section
storage sizing and single-cutoff parameterisation — tracked separately in the leads list.
Severity: high
Domain: filters
Status: VERIFIED — read + hand-traced against
a210d34on 2026-08-10Suggested labels:
bug,numerical-correctness,filtersSummary
HighPassRoots()maps the analog prototype pole withwc / p.Real()for the real part whilecorrectly using
|p|²for the imaginary part. The low-pass→high-pass transform requiresωc · Re(p) / |p|². The asymmetry between the two components makes this a clear transcription slip.Location
numerical/filters/passive/IirFilterDesign.hpp
Evidence
Expected
The transform
s → ωc/smaps a poleptoso the real part must also divide by
mag2.Trace
Butterworth prototype pole
p = −0.7071 + 0.7071j,|p|² = 1:The pole is placed at twice the correct radius, so both the cutoff frequency and the Q of the
resulting section are wrong.
Suggested fix
Add a test asserting the −3 dB point of a designed high-pass lands at the requested cutoff.
Notes
The band-pass/band-stop paths in the same file have separate, unverified concerns about section
storage sizing and single-cutoff parameterisation — tracked separately in the leads list.