2026-09-03 10:00:00 +02:00
|
|
|
|
// 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)
|
2026-09-28 21:36:09 +02:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
2026-09-03 10:00:00 +02:00
|
|
|
|
for i := range b {
|
|
|
|
|
|
b[i] *= scale
|
|
|
|
|
|
}
|
2026-09-28 21:36:09 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-03 10:00:00 +02:00
|
|
|
|
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}
|
|
|
|
|
|
}
|