Skip to content

IIR high-pass pole transform uses the wrong real part #256

Description

@gabrielfrasantos

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions