636 lines
23 KiB
Go
636 lines
23 KiB
Go
// 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)
|
||
for i := range b {
|
||
b[i] *= scale
|
||
}
|
||
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}
|
||
}
|