fix(signal): refuse filter designs whose coefficients overflow
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -54,6 +54,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `WriteSVG` keeps extreme but finite data and axis ranges drawable:
|
||||
the padding, projection and tick arithmetic fall back to forms whose
|
||||
terms stay in range, so the file never carries a NaN coordinate.
|
||||
- Filter design refuses an order whose coefficient arithmetic
|
||||
overflows the float64 range instead of shipping a numerator of
|
||||
zeros or NaN.
|
||||
|
||||
## [1.0.0] - 2026-09-03
|
||||
|
||||
|
||||
+3
-1
@@ -1054,7 +1054,9 @@ Each design returns the direct-form coefficients `b` (numerator) and `a`
|
||||
band edges prewarped to the bilinear axis and the answer exact at the mapped
|
||||
frequencies. All of them refuse an order below 1, a non-positive or infinite `fs`,
|
||||
and an edge outside `(0, fs/2)`; the band forms additionally require
|
||||
`0 < edge1 < edge2 < fs/2`.
|
||||
`0 < edge1 < edge2 < fs/2`. An order whose coefficient arithmetic
|
||||
overflows the float64 range is refused as well, never returned as a
|
||||
numerator of zeros or NaN.
|
||||
|
||||
| Call | What it does |
|
||||
|---|---|
|
||||
|
||||
@@ -360,6 +360,9 @@ func butterworth(order int, fs, cutoff float64, highpass bool) (b, a []float64,
|
||||
}
|
||||
}
|
||||
k := polyEvalAtMinusOne(a) / math.Pow(2, float64(order))
|
||||
if err := designCoefficientsFinite(name, order, k, b, a); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i := range b {
|
||||
b[i] *= k
|
||||
}
|
||||
@@ -367,12 +370,35 @@ func butterworth(order int, fs, cutoff float64, highpass bool) (b, a []float64,
|
||||
}
|
||||
b = binomialCoeffs(order)
|
||||
k := polyEvalAtOne(a) / math.Pow(2, float64(order))
|
||||
if err := designCoefficientsFinite(name, order, k, b, a); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i := range b {
|
||||
b[i] *= k
|
||||
}
|
||||
return b, a, nil
|
||||
}
|
||||
|
||||
// designCoefficientsFinite refuses a design whose arithmetic left the
|
||||
// float64 range. Past an order of about a thousand the gain divides by
|
||||
// an infinite 2^order and the binomial numerator overflows with it,
|
||||
// answers that would otherwise ship as a numerator of zeros or NaN
|
||||
// presented as a filter: the gain must be finite and non-zero, and
|
||||
// every coefficient of both polynomials finite.
|
||||
func designCoefficientsFinite(name string, order int, gain float64, b, a []float64) error {
|
||||
if math.IsNaN(gain) || math.IsInf(gain, 0) || gain == 0 {
|
||||
return base.Errf("%s: order %d overflows the coefficient arithmetic; use a lower order", name, order)
|
||||
}
|
||||
for _, poly := range [2][]float64{b, a} {
|
||||
for _, v := range poly {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return base.Errf("%s: order %d overflows the coefficient arithmetic; use a lower order", name, order)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mulPolyReal multiplies two real polynomials in u = z^{-1} (index m
|
||||
// is the coefficient of u^m).
|
||||
func mulPolyReal(p, q []float64) []float64 {
|
||||
|
||||
@@ -286,9 +286,22 @@ func design(sh shape, proto prototype, w1, w2 float64) (b, a []float64, err erro
|
||||
// reference follows from the roots and is no business of the
|
||||
// scaling.
|
||||
scale := proto.gain / cmplx.Abs(hd)
|
||||
// An extreme order leaves the float64 range here as it does in the
|
||||
// Butterworth pair: an infinite or vanished scale, or a coefficient
|
||||
// past the range, is a refusal rather than a filter of zeros or NaN.
|
||||
if math.IsNaN(scale) || math.IsInf(scale, 0) || scale == 0 {
|
||||
return nil, nil, base.Errf("%s: the order overflows the coefficient arithmetic; use a lower order", name)
|
||||
}
|
||||
for i := range b {
|
||||
b[i] *= scale
|
||||
}
|
||||
for _, poly := range [2][]float64{b, a} {
|
||||
for _, v := range poly {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return nil, nil, base.Errf("%s: the order overflows the coefficient arithmetic; use a lower order", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return b, a, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,52 @@ func TestFilterDesignsAgainstReferenceValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtremeFilterOrders pins the designs against the extreme-but-valid
|
||||
// order corner: the entry gates ask only for order ≥ 1, and past a few
|
||||
// hundred the coefficient arithmetic leaves the float64 range (the
|
||||
// Butterworth gain divides by 2^order, the binomial numerator and the
|
||||
// assembled polynomials overflow with it). An order the arithmetic
|
||||
// cannot carry must come back as an error, never as a numerator of
|
||||
// zeros or NaN presented as a filter; an order it does carry must
|
||||
// answer with finite coefficients and a response whose numerator did
|
||||
// not vanish.
|
||||
func TestExtremeFilterOrders(t *testing.T) {
|
||||
const fs = 1000.0
|
||||
designs := []struct {
|
||||
name string
|
||||
run func() ([]float64, []float64, error)
|
||||
probe float64
|
||||
}{
|
||||
{"butterworth-low-1024", func() ([]float64, []float64, error) { return ButterworthLowPass(1024, fs, 100) }, 10},
|
||||
{"butterworth-high-1024", func() ([]float64, []float64, error) { return ButterworthHighPass(1024, fs, 100) }, 490},
|
||||
{"butterworth-band-pass-513", func() ([]float64, []float64, error) { return ButterworthBandPass(513, fs, 100, 300) }, 200},
|
||||
{"chebyshev1-low-1024", func() ([]float64, []float64, error) { return ChebyshevLowPass(1024, fs, 100, 1) }, 10},
|
||||
{"chebyshev2-low-1024", func() ([]float64, []float64, error) { return InverseChebyshevLowPass(1024, fs, 100, 40) }, 10},
|
||||
{"cauer-low-512", func() ([]float64, []float64, error) { return CauerLowPass(512, fs, 100, 1, 60) }, 10},
|
||||
}
|
||||
for _, d := range designs {
|
||||
b, a, err := d.run()
|
||||
if err != nil {
|
||||
// A refusal naming the overflow is the honest answer.
|
||||
continue
|
||||
}
|
||||
for _, v := range b {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
t.Fatalf("%s: the numerator holds the non-finite coefficient %g", d.name, v)
|
||||
}
|
||||
}
|
||||
for _, v := range a {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
t.Fatalf("%s: the denominator holds the non-finite coefficient %g", d.name, v)
|
||||
}
|
||||
}
|
||||
g := designResponse(b, a, d.probe, fs)
|
||||
if math.IsNaN(g) || g == 0 {
|
||||
t.Fatalf("%s: the response at %g Hz is %.12g, the arithmetic lost the design", d.name, d.probe, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterDesignStability checks that every design's poles sit
|
||||
// inside the unit circle, the property direct-form filtering lives
|
||||
// and dies by.
|
||||
|
||||
Reference in New Issue
Block a user