feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
// 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}
|
||||
}
|
||||
Reference in New Issue
Block a user