Files
tensor/signal/filterdesign.go
T
2026-09-28 21:36:09 +02:00

649 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package signal
import (
"math"
"math/cmplx"
"slices"
"sourcedock.dev/petrbalvin/tensor/internal/base"
"sourcedock.dev/petrbalvin/tensor/internal/core"
)
// IIR designs beyond the Butterworth pair: the Chebyshev equiripple
// family, the inverse Chebyshev, the elliptic (Cauer) designs, and
// the band shapes for every prototype. Each design walks the same
// road: an analog low-pass prototype at unit passband edge, the
// shape transformation of its pole-zero set with both band edges
// prewarped to the bilinear axis, and the bilinear mapping of each
// pole and zero. The mapping is exact, so the prototype's passband
// peak and edge attenuations land on the digital side at the mapped
// frequencies; that is what the tests pin.
//
// The coefficients come back in the same u = z⁻¹ convention as the
// Butterworth pair, with the denominator leading a one. Direct-form
// filtering loses digits as the order climbs, so past roughly order
// eight a design should be split into second-order sections by the
// caller; where that line sits is deliberately left to taste.
// prototype is one analog low-pass at unit passband edge. Poles and
// finite zeroes carry exact conjugate symmetry; the zeroes a low-pass
// holds at infinity are counted, not listed.
type prototype struct {
poles []complex128
zeros []complex128
zerosAtInfinity int
gain float64
}
// chebyshev1 returns the type I prototype: the Butterworth circle
// squashed by the ripple's hyperbolic factor, so the passband swings
// between one and 1/sqrt(1+eps²), the peak normalised to one. The
// edge sits where the gain first drops to -rippleDB.
func chebyshev1(order int, rippleDB float64) prototype {
eps := math.Sqrt(math.Pow(10, rippleDB/10) - 1)
mu := math.Asinh(1/eps) / float64(order)
poles := make([]complex128, 0, order)
for k := range order {
theta := math.Pi * float64(2*k+1) / float64(2*order)
// One Sincos serves the pole's both factors: the pair is the
// one the separate Sin and Cos calls produced, so the pole
// keeps its bits.
sinT, cosT := math.Sincos(theta)
if math.Abs(cosT) < 1e-15 {
// The odd order's real pole, built exactly.
poles = append(poles, complex(-math.Sinh(mu), 0))
continue
}
if cosT < 0 {
continue // the mirror angle's conjugate, already stored
}
p := complex(-math.Sinh(mu)*sinT, math.Cosh(mu)*cosT)
poles = append(poles, p, cmplx.Conj(p))
}
gain := 1.0
if order%2 == 0 {
gain = 1 / math.Sqrt(1+eps*eps)
}
return prototype{poles: poles, zerosAtInfinity: order, gain: gain}
}
// chebyshev2 returns the inverse Chebyshev: zeroes on the imaginary
// axis at the reciprocals of the type I ripple points, poles at the
// reciprocals of type I poles built with the stopband's factor, so
// the stopband floor is exactly the demanded attenuation. The root
// lattice runs over the integers of one parity between −(n−1) and
// n−1: for odd orders it passes through zero, holding the real pole
// there, and the zero at infinity keeps m = 0 out of the zero list.
func chebyshev2(order int, stopbandDB float64) prototype {
de := 1 / math.Sqrt(math.Pow(10, stopbandDB/10)-1)
mu := math.Asinh(1/de) / float64(order)
poles := make([]complex128, 0, order)
zeros := make([]complex128, 0, order)
for m := -order + 1; m <= order-1; m += 2 {
theta := math.Pi * float64(m) / float64(2*order)
if m >= 0 {
p := -1 / cmplx.Sinh(complex(mu, theta))
if m == 0 {
poles = append(poles, p)
} else {
poles = append(poles, p, cmplx.Conj(p))
}
if m > 0 {
z := complex(0, 1/math.Sin(theta))
zeros = append(zeros, z, cmplx.Conj(z))
}
}
}
return prototype{poles: poles, zeros: zeros,
zerosAtInfinity: order - len(zeros), gain: 1}
}
// cauer returns the elliptic prototype. The zeroes are cd of the
// quarter-period fractions, read through the library's Jacobi
// functions; the poles ride the addition theorem through the
// amplitude v0 solved from the stopband's discrimination; and the
// degree equation closes through the nome, so ripple and attenuation
// both land on spec at the first transition edge.
func cauer(order int, rippleDB, stopbandDB float64) prototype {
epsSq := math.Pow(10, rippleDB/10) - 1
eps := math.Sqrt(epsSq)
m1 := epsSq / (math.Pow(10, stopbandDB/10) - 1)
m := ellipdeg(order, m1)
capk := core.EllipticKScalar(m)
// Amplitudes along the quarter period: u_j = j·K/n for odd j when
// the order is even, even j when it is odd, where u = 0 holds the
// zero at infinity.
zeroes := make([]complex128, 0, order)
amplitudes := make([][3]float64, 0, order)
for j := 1 - order%2; j < order; j += 2 {
u := float64(j) * capk / float64(order)
s := jacobiScalar(core.JacobiSN, u, m)
c := jacobiScalar(core.JacobiCN, u, m)
d := jacobiScalar(core.JacobiDN, u, m)
if math.Abs(s) > 1e-12 {
z := complex(0, 1/(math.Sqrt(m)*s))
zeroes = append(zeroes, z, cmplx.Conj(z))
}
amplitudes = append(amplitudes, [3]float64{s, c, d})
}
// v0: the amplitude solving sc(v0, 1−m1) = 1/ε on the
// complementary parameter, through the identity sc(u, m) =
// tan(am(u, m)): v0 = F(atan(1/ε), 1−m1), scaled by the nome
// ratio the degree equation provides. The pole formula then reads
// its Jacobi amplitudes at v0 on the prototype's own parameter.
v0 := capk * core.EllipticFScalar(math.Atan(1/eps), 1-m1) /
(float64(order) * core.EllipticKScalar(m1))
sv := jacobiScalar(core.JacobiSN, v0, 1-m)
cv := jacobiScalar(core.JacobiCN, v0, 1-m)
dv := jacobiScalar(core.JacobiDN, v0, 1-m)
poles := make([]complex128, 0, order)
for _, a := range amplitudes {
s, c, d := a[0], a[1], a[2]
p := -complex(c*d*sv*cv, s*dv) / (1 - complex((d*sv)*(d*sv), 0))
if math.Abs(imag(p)) < 1e-10 {
poles = append(poles, complex(real(p), 0))
continue
}
poles = append(poles, p, cmplx.Conj(p))
}
gain := 1.0
if order%2 == 0 {
gain = 1 / math.Sqrt(1+epsSq)
}
return prototype{poles: poles, zeros: zeroes,
zerosAtInfinity: order - len(zeroes), gain: gain}
}
// ellipdeg solves the degree equation n·K(m)/K'(m) = K(m1)/K'(m1)
// for m through the nome q = exp(−π·K'/K), whose theta product is
// accurate to double precision within the first eight powers.
func ellipdeg(n int, m1 float64) float64 {
k1 := core.EllipticKScalar(m1)
k1p := core.EllipticKScalar(1 - m1)
q := math.Pow(math.Exp(-math.Pi*k1p/k1), 1.0/float64(n))
num, den := 1.0, 1.0
for k := 1; k <= 7; k++ {
num += math.Pow(q, float64(k*(k+1)))
}
for k := 1; k <= 8; k++ {
den += 2 * math.Pow(q, float64(k*k))
}
return 16 * q * math.Pow(num/den, 4)
}
// jacobiScalar reads one Jacobi function at one point through the
// array implementation, which inverts the amplitude by bracketed
// Newton against Carlson's incomplete integral.
func jacobiScalar(f func(u *core.Array, m float64) (*core.Array, error), u, m float64) float64 {
arr, err := core.FromFloats([]float64{u}, 1)
if err != nil {
return math.NaN()
}
out, err := f(arr, m)
if err != nil {
return math.NaN()
}
return out.FloatAt(0)
}
// mapEdge transforms one analog root of the unit-edge prototype into
// the prewarped target band. Band roots come back as the pair of a
// quadratic, the pairing surviving because the map sends conjugate
// pairs to conjugate pairs.
func mapEdge(s complex128, sh shape, w1, w2 float64) []complex128 {
switch sh {
case lowPass:
return []complex128{s * complex(w1, 0)}
case highPass:
return []complex128{complex(w1, 0) / s}
case bandPass:
// Scale to the half bandwidth, then split about the centre:
// the pair solves s² − BW·root·s + w0² = 0.
half := s * complex((w2-w1)/2, 0)
disc := cmplx.Sqrt(half*half - complex(w1*w2, 0))
return []complex128{half + disc, half - disc}
default: // bandStop: invert to the half-bandwidth high-pass, then
// split about the centre the same way.
half := complex((w2-w1)/2, 0) / s
disc := cmplx.Sqrt(half*half - complex(w1*w2, 0))
return []complex128{half + disc, half - disc}
}
}
// shape picks the band the design passes.
type shape int
const (
lowPass shape = iota
highPass
bandPass
bandStop
)
// design runs the shared road from prototype to coefficients: shape
// transformation of every root, bilinear mapping, assembly into real
// u = z⁻¹ polynomials, and the gain taken from the prototype itself
// at its DC, which every shape reaches through the mapping (the
// low-pass at DC, the high-pass at Nyquist, the band pair at the
// band centre and DC respectively).
func design(sh shape, proto prototype, w1, w2 float64) (b, a []float64, err error) {
const name = "filter design"
// Denominator roots: every pole, shape-transformed and mapped.
var poles []complex128
for _, p := range proto.poles {
for _, s := range mapEdge(p, sh, w1, w2) {
if real(s) > 1e-7*(1+cmplx.Abs(s)) {
return nil, nil, base.Errf("%s: a pole escaped the left half-plane", name)
}
poles = append(poles, bilinear(s))
}
}
// Numerator roots: every finite zero's image, then the zeroes at
// infinity: (1+u) factors for the low-pass, whose infinity maps
// to u = −1; (1−u) pairs for the band-pass, whose infinity maps
// to s = 0, u = 1; and one ±j·w0 conjugate pair per zero for the
// band-stop. The infinity factors are digital already; only the
// finite zeroes pass through the bilinear map.
var zeros []complex128
for _, z := range proto.zeros {
zeros = append(zeros, mapEdge(z, sh, w1, w2)...)
}
zeros = bilinearAll(zeros)
zeros = append(zeros, infinityFactors(sh, proto.zerosAtInfinity, math.Sqrt(w1*w2))...)
b, err = assembleRoots(zeros)
if err != nil {
return nil, nil, err
}
a, err = assembleRoots(poles)
if err != nil {
return nil, nil, err
}
// Gain: the prototype's gain field is the response the design
// promises at its reference point (the passband peak, one or
// 1/sqrt(1+eps²) by order parity), so the numerator scales until
// the digital response at the mapped reference equals it.
var uRef complex128
switch sh {
case lowPass, bandStop:
uRef = 1
case highPass:
uRef = -1
default: // bandPass: the band centre, the geometric mean edge; the
// conjugate side keeps the polynomial evaluation real.
uRef = cmplx.Conj(bilinear(complex(0, math.Sqrt(w1*w2))))
}
hd := polyEvalC(b, uRef) / polyEvalC(a, uRef)
if hd == 0 {
return nil, nil, base.Errf("%s: the reference point carries no gain", name)
}
// The magnitude is what the gain field promises; the phase at the
// 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
}
// infinityFactors names the numerator roots the prototype's zeroes at
// infinity turn into after the shape transformation: the low-pass
// zeroes at s = ∞ land at u = −1; the high-pass zeroes at s = 0 land
// at u = +1; the band-pass substitution squares its frequency, so
// every infinity zero becomes a double zero at s = 0, two (1−u)
// factors; and the band-stop turns every infinity zero into the
// conjugate pair ±j·w0, the roots of s² + w0² = 0.
func infinityFactors(sh shape, atInfinity int, w0 float64) []complex128 {
switch sh {
case lowPass:
roots := make([]complex128, 0, atInfinity)
for range atInfinity {
roots = append(roots, -1)
}
return roots
case bandPass:
// The substitution's s = (1−u)/(1+u) leaves the numerator as
// (1−u²)^N: half the roots at u = 1, half at u = −1.
roots := make([]complex128, 0, 2*atInfinity)
for range atInfinity {
roots = append(roots, 1, -1)
}
return roots
case bandStop:
roots := make([]complex128, 0, 2*atInfinity)
for range atInfinity {
d := bilinear(complex(0, w0))
roots = append(roots, d, cmplx.Conj(d))
}
return roots
default: // highPass
roots := make([]complex128, 0, atInfinity)
for range atInfinity {
roots = append(roots, 1)
}
return roots
}
}
// assembleRoots factors digital roots into a real polynomial in u =
// z⁻¹: conjugate pairs become the real quadratic
// (1 − z·u)(1 − z̄·u) = 1 − 2Re(z)·u + |z|²·u², real roots the linear
// factor (1 − z·u). The pairing matches each root against the
// remaining roots' conjugates, so it does not depend on the order
// the roots arrived in.
func assembleRoots(roots []complex128) ([]float64, error) {
const name = "filter design"
poly := []float64{1}
used := make([]bool, len(roots))
for i, r := range roots {
if used[i] {
continue
}
if math.Abs(imag(r)) < 1e-9 {
used[i] = true
poly = mulPolyReal(poly, []float64{1, -real(r)})
continue
}
// The nearest conjugate partner among the unused roots.
partner := -1
best := math.Inf(1)
for j := i + 1; j < len(roots); j++ {
if used[j] {
continue
}
if d := cmplx.Abs(roots[j] - cmplx.Conj(r)); d < best {
best, partner = d, j
}
}
if partner < 0 || best > 1e-6*cmplx.Abs(r) {
return nil, base.Errf("%s: the roots lost their conjugate symmetry", name)
}
used[partner] = true
poly = mulPolyReal(poly, []float64{1, -2 * real(r), real(r * cmplx.Conj(r))})
}
return poly, nil
}
// bilinearAll maps a root list through the bilinear transform.
func bilinearAll(roots []complex128) []complex128 {
out := make([]complex128, len(roots))
for i, s := range roots {
out[i] = bilinear(s)
}
return out
}
// bilinear maps an analog root to its digital image, the T = 2
// sampling the prewarp assumes.
func bilinear(s complex128) complex128 {
return (1 + s) / (1 - s)
}
// polyEvalC evaluates a u = z⁻¹ polynomial at one complex point.
func polyEvalC(poly []float64, u complex128) complex128 {
total := complex(0, 0)
for _, p := range slices.Backward(poly) {
total = total*u + complex(p, 0)
}
return total
}
// The public designs. Each validates its arguments, prewarps the
// edges, and hands the shared road its prototype.
// designArgs bundles the validated prewarped edges for one call.
func designArgs(name string, order int, fs, edge1, edge2 float64, sh shape) (w1, w2 float64, err error) {
if order < 1 {
return 0, 0, base.Errf("%s: the order must be at least 1, got %d", name, order)
}
if !(fs > 0) || math.IsInf(fs, 0) {
return 0, 0, base.Errf("%s: fs must be positive and finite, got %g", name, fs)
}
if !(edge1 > 0) || edge1 >= fs/2 {
return 0, 0, base.Errf("%s: the edge must lie in (0, fs/2), got %g for fs %g", name, edge1, fs)
}
w1 = math.Tan(math.Pi * edge1 / fs)
if sh == bandPass || sh == bandStop {
if !(edge2 > edge1) || edge2 >= fs/2 {
return 0, 0, base.Errf("%s: the band must span (edge1, edge2) inside (0, fs/2), got %g, %g for fs %g",
name, edge1, edge2, fs)
}
w2 = math.Tan(math.Pi * edge2 / fs)
}
return w1, w2, nil
}
// ChebyshevLowPass designs an order-N type I Chebyshev low-pass at fs
// hertz with its ripple in decibels: the passband oscillates between
// 0 and -rippleDB, the edge is the last touch of -rippleDB, and the
// stopband rolls off as fast as that budget allows.
func ChebyshevLowPass(order int, fs, cutoff, rippleDB float64) (b, a []float64, err error) {
const name = "ChebyshevLowPass"
if rippleDB <= 0 {
return nil, nil, base.Errf("%s: the ripple must be positive decibels, got %g", name, rippleDB)
}
w, _, err := designArgs(name, order, fs, cutoff, 0, lowPass)
if err != nil {
return nil, nil, err
}
return design(lowPass, chebyshev1(order, rippleDB), w, 0)
}
// ChebyshevHighPass is the type I mirror: the same equiripple
// passband above the edge, rolling off below it.
func ChebyshevHighPass(order int, fs, cutoff, rippleDB float64) (b, a []float64, err error) {
const name = "ChebyshevHighPass"
if rippleDB <= 0 {
return nil, nil, base.Errf("%s: the ripple must be positive decibels, got %g", name, rippleDB)
}
w, _, err := designArgs(name, order, fs, cutoff, 0, highPass)
if err != nil {
return nil, nil, err
}
return design(highPass, chebyshev1(order, rippleDB), w, 0)
}
// InverseChebyshevLowPass designs the type II low-pass: a flat
// passband through the edge, with the stopband bottoming out at
// -stopbandDB and equiripple beyond it.
func InverseChebyshevLowPass(order int, fs, cutoff, stopbandDB float64) (b, a []float64, err error) {
const name = "InverseChebyshevLowPass"
if stopbandDB <= 0 {
return nil, nil, base.Errf("%s: the stopband attenuation must be positive decibels, got %g", name, stopbandDB)
}
w, _, err := designArgs(name, order, fs, cutoff, 0, lowPass)
if err != nil {
return nil, nil, err
}
return design(lowPass, chebyshev2(order, stopbandDB), w, 0)
}
// InverseChebyshevHighPass is the type II mirror above the edge.
func InverseChebyshevHighPass(order int, fs, cutoff, stopbandDB float64) (b, a []float64, err error) {
const name = "InverseChebyshevHighPass"
if stopbandDB <= 0 {
return nil, nil, base.Errf("%s: the stopband attenuation must be positive decibels, got %g", name, stopbandDB)
}
w, _, err := designArgs(name, order, fs, cutoff, 0, highPass)
if err != nil {
return nil, nil, err
}
return design(highPass, chebyshev2(order, stopbandDB), w, 0)
}
// CauerLowPass designs the elliptic low-pass: equiripple in the
// passband within rippleDB and equiripple stopband not above
// -stopbandDB, with the narrowest transition of any design at the
// order. The zeroes sit in the stopband, finite and on the unit
// circle after mapping.
func CauerLowPass(order int, fs, cutoff, rippleDB, stopbandDB float64) (b, a []float64, err error) {
const name = "CauerLowPass"
if rippleDB <= 0 || stopbandDB <= rippleDB {
return nil, nil, base.Errf("%s: the ripple must be positive and the attenuation larger, got %g and %g",
name, rippleDB, stopbandDB)
}
w, _, err := designArgs(name, order, fs, cutoff, 0, lowPass)
if err != nil {
return nil, nil, err
}
return design(lowPass, cauer(order, rippleDB, stopbandDB), w, 0)
}
// CauerHighPass is the elliptic mirror above the edge.
func CauerHighPass(order int, fs, cutoff, rippleDB, stopbandDB float64) (b, a []float64, err error) {
const name = "CauerHighPass"
if rippleDB <= 0 || stopbandDB <= rippleDB {
return nil, nil, base.Errf("%s: the ripple must be positive and the attenuation larger, got %g and %g",
name, rippleDB, stopbandDB)
}
w, _, err := designArgs(name, order, fs, cutoff, 0, highPass)
if err != nil {
return nil, nil, err
}
return design(highPass, cauer(order, rippleDB, stopbandDB), w, 0)
}
// ChebyshevBandPass designs the type I band-pass spanning edge1 to
// edge2: the prototype's order doubles through the band move.
func ChebyshevBandPass(order int, fs, edge1, edge2, rippleDB float64) (b, a []float64, err error) {
const name = "ChebyshevBandPass"
if rippleDB <= 0 {
return nil, nil, base.Errf("%s: the ripple must be positive decibels, got %g", name, rippleDB)
}
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandPass)
if err != nil {
return nil, nil, err
}
return design(bandPass, chebyshev1(order, rippleDB), w1, w2)
}
// ChebyshevBandStop designs the type I band-stop.
func ChebyshevBandStop(order int, fs, edge1, edge2, rippleDB float64) (b, a []float64, err error) {
const name = "ChebyshevBandStop"
if rippleDB <= 0 {
return nil, nil, base.Errf("%s: the ripple must be positive decibels, got %g", name, rippleDB)
}
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandStop)
if err != nil {
return nil, nil, err
}
return design(bandStop, chebyshev1(order, rippleDB), w1, w2)
}
// InverseChebyshevBandPass designs the type II band-pass.
func InverseChebyshevBandPass(order int, fs, edge1, edge2, stopbandDB float64) (b, a []float64, err error) {
const name = "InverseChebyshevBandPass"
if stopbandDB <= 0 {
return nil, nil, base.Errf("%s: the stopband attenuation must be positive decibels, got %g", name, stopbandDB)
}
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandPass)
if err != nil {
return nil, nil, err
}
return design(bandPass, chebyshev2(order, stopbandDB), w1, w2)
}
// InverseChebyshevBandStop designs the type II band-stop.
func InverseChebyshevBandStop(order int, fs, edge1, edge2, stopbandDB float64) (b, a []float64, err error) {
const name = "InverseChebyshevBandStop"
if stopbandDB <= 0 {
return nil, nil, base.Errf("%s: the stopband attenuation must be positive decibels, got %g", name, stopbandDB)
}
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandStop)
if err != nil {
return nil, nil, err
}
return design(bandStop, chebyshev2(order, stopbandDB), w1, w2)
}
// CauerBandPass designs the elliptic band-pass.
func CauerBandPass(order int, fs, edge1, edge2, rippleDB, stopbandDB float64) (b, a []float64, err error) {
const name = "CauerBandPass"
if rippleDB <= 0 || stopbandDB <= rippleDB {
return nil, nil, base.Errf("%s: the ripple must be positive and the attenuation larger, got %g and %g",
name, rippleDB, stopbandDB)
}
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandPass)
if err != nil {
return nil, nil, err
}
return design(bandPass, cauer(order, rippleDB, stopbandDB), w1, w2)
}
// CauerBandStop designs the elliptic band-stop.
func CauerBandStop(order int, fs, edge1, edge2, rippleDB, stopbandDB float64) (b, a []float64, err error) {
const name = "CauerBandStop"
if rippleDB <= 0 || stopbandDB <= rippleDB {
return nil, nil, base.Errf("%s: the ripple must be positive and the attenuation larger, got %g and %g",
name, rippleDB, stopbandDB)
}
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandStop)
if err != nil {
return nil, nil, err
}
return design(bandStop, cauer(order, rippleDB, stopbandDB), w1, w2)
}
// ButterworthBandPass designs the maximally flat band-pass.
func ButterworthBandPass(order int, fs, edge1, edge2 float64) (b, a []float64, err error) {
const name = "ButterworthBandPass"
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandPass)
if err != nil {
return nil, nil, err
}
return design(bandPass, butterworthPrototype(order), w1, w2)
}
// ButterworthBandStop designs the maximally flat band-stop.
func ButterworthBandStop(order int, fs, edge1, edge2 float64) (b, a []float64, err error) {
const name = "ButterworthBandStop"
w1, w2, err := designArgs(name, order, fs, edge1, edge2, bandStop)
if err != nil {
return nil, nil, err
}
return design(bandStop, butterworthPrototype(order), w1, w2)
}
// butterworthPrototype rebuilds the maximally flat poles in the
// prototype shape, so the band shapes share the same road; the
// existing ButterworthLowPass and ButterworthHighPass keep their own
// pinned implementations untouched.
func butterworthPrototype(order int) prototype {
poles := make([]complex128, 0, order)
for k := range order {
theta := math.Pi * float64(2*k+1) / float64(2*order)
// One Sincos serves the pole's both factors, the same pair the
// separate calls produced.
sinT, cosT := math.Sincos(theta)
if math.Abs(cosT) < 1e-15 {
poles = append(poles, complex(-1, 0))
continue
}
if cosT < 0 {
continue // the mirror angle's conjugate, already stored
}
p := complex(-sinT, cosT)
poles = append(poles, p, cmplx.Conj(p))
}
return prototype{poles: poles, zerosAtInfinity: order, gain: 1}
}