Skip to content

Instantly share code, notes, and snippets.

@endolith
Last active July 12, 2016 00:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save endolith/3f74c4ec9ea623812cca to your computer and use it in GitHub Desktop.
Save endolith/3f74c4ec9ea623812cca to your computer and use it in GitHub Desktop.
Python design of Bessel filters of any order, with 3 common frequency normalizations
# -*- coding: utf-8 -*-
"""
Generate Bessel filters directly, using mpmath
References:
http://en.wikipedia.org/wiki/Bessel_filter#Example
Thomson, W.E., "Delay Networks having Maximally Flat Frequency
Characteristics", Proceedings of the Institution of Electrical Engineers,
Part III, November 1949, Vol. 96, No. 44, pp. 487–490.
Bond, C.R., Bessel Filters Polynomials, Poles and Circuit Elements, 2003
www.crbond.com/papers/bsf2.pdf
Bond, C.R., Bessel Filter Constants
www.crbond.com/papers/bsf.pdf
Miller, Ray - A Bessel Filter Crossover, and Its Relation to Others
http://www.rane.com/pdf/ranenotes/Bessel_Filter_Crossover_&_its_Relation_to_Others.pdf
"""
from __future__ import division, print_function
from numpy import asarray
import numpy as np
from scipy.misc import factorial
try:
from mpmath import mp
mpmath_available = True
except ImportError:
mpmath_available = False
def bessel_poly(n, reverse=False):
"""
Return the Bessel polynomial of degree `n`
If `reverse` is true, a reverse Bessel polynomial is output.
Output is a list of coefficients:
[1] = 1
[1, 1] = 1*s + 1
[1, 3, 3] = 1*s^2 + 3*s + 3
[1, 6, 15, 15] = 1*s^3 + 6*s^2 + 15*s + 15
[1, 10, 45, 105, 105] = 1*s^4 + 10*s^3 + 45*s^2 + 105*s + 105
etc.
Output is a Python list of arbitrary precision long ints, so n is only
limited by your hardware's memory.
Sequence is http://oeis.org/A001498 , and output can be confirmed to
match http://oeis.org/A001498/b001498.txt :
i = 0
for n in xrange(51):
for x in bessel_poly(n, reverse=True):
print i, x
i += 1
"""
out = []
for k in xrange(n + 1):
num = factorial(2*n - k, exact=True)
den = 2**(n - k) * (factorial(k, exact=True) *
factorial(n - k, exact=True))
out.append(num // den)
if reverse:
return list(reversed(out))
else:
return out
def normfactor(a):
"""
Frequency shift to apply to delay-normalized filter such that -3 dB point
is at 1 rad/sec.
First 10 values are listed in
"Bessel Filters Polynomials, Poles and Circuit Elements 2003, C. Bond."
"""
def G(w):
"""
Gain of filter
"""
return abs(a[-1]/mp.polyval(a, 1j*w))
def cutoff(w):
"""
When gain = -3 dB, return 0
"""
return G(w) - 1/mp.sqrt(2)
return mp.findroot(cutoff, 1.5)
"""
Some values:
1: 1,
2: 1.3616541287161305209588868444454448432347103729901,
3: 1.7556723686812106492036418865538548622632538627204,
4: 2.1139176749042158430196891286338507228884592920213,
5: 2.4274107021526281376663058876113906340172394775656,
6: 2.70339506120292187639578951630254258339151785719,
7: 2.9517221470387227719289052270107816308339931866838,
8: 3.179617237510651330501649153152461962340386280172,
9: 3.3916931389116601011122541953570649508638576678231,
10: 3.5909805945691634827892977108436993654835433664576,
11: 3.7796074164396200928908480462973530897057513767288,
12: 3.9591508211442853155392550478018629152262858202629,
13: 4.1308254993835359808364524050964109423143665202459,
14: 4.2955934095336375649772937469630970087603802703228,
15: 4.4542330216243774943378240757836612531623748693155,
16: 4.6073854654726479174415758515725454199893109030358,
17: 4.7555865489611477270354413366716975695016018946715,
18: 4.8992896772844880074416221966614330534420251746993,
19: 5.0388826814882076058089530669453332343792794904578,
20: 5.1747004417427074232967413801802587948461890472997,
21: 5.3070345313609172742698274606247947796240516171721,
22: 5.4361407032500359996281001872606472534988925645919,
23: 5.5622447837878781965332371305558405356769995361022,
24: 5.6855473712959635219603072771407213415136770804137,
25: 5.8062276237754185419638409233382918905704274993629,
"""
def _roots(a):
"""
Find the roots of a polynomial, using mpmath.polyroots if available,
or numpy.roots if not
"""
N = (len(a) - 1)//2 # Order of the filter
if mpmath_available:
# Overkill: "The user may have to manually set the working precision
# higher than the desired accuracy for the result, possibly much
# higher."
mp.dps = 150
"""
bessel:
with dps 50 and maxsteps = 100, it can do 39 order filters
with dps 150 and maxsteps = 1000, it can do 70 order filters
TODO: How many digits are necessary for float equivalence? Does it
vary with order?
with dps 50 and maxsteps = 100, it can do __ order filters
with dps 150 and maxsteps = 1000, it can do __>=40__ order filters
"""
p, err = mp.polyroots(a, maxsteps=1000, error=True)
if err > 1e-32:
raise ValueError("Filter cannot be accurately computed "
"for order %s" % N)
p = asarray(p).astype(complex)
else:
p = np.roots(a)
if N > 25:
# Bessel and Legendre filters seem to fail above N = 25
raise ValueError("Filter cannot be accurately computed "
"for order %s" % N)
return p
def besselap(N, norm='phase'):
"""
Return (z,p,k) zero, pole, gain for analog prototype of an Nth order
Bessel filter, normalized to a frequency of 1.
`norm` is the frequency normalization:
'phase': (default)
The filter is normalized such that the filter asymptotes are the same
as a Butterworth filter of the same order with an angular (e.g. rad/s)
cutoff frequency of 1.
The phase reaches its midpoint at this cutoff frequency for both
low-pass and high-pass filters, so this is the "phase-matched" case.
This is Matlab's normalization
'delay':
The filter is normalized such that the group delay in the passband
is 1 (e.g. 1 second). This is the "natural" type you get when you
solve Bessel polynomials
'mag':
The filter is normalized such that the gain magnitude is -3 dB at
frequency 1. This is called "frequency normalization" by Bond
"""
if N == 0:
return asarray([]), asarray([]), 1
# Find delay-normalized Bessel filter poles
a = bessel_poly(N, reverse=True)
p = _roots(a)
if norm == 'delay':
# Normalized for group delay of 1
k = a[-1]
elif norm == 'phase':
# Phase-matched (1/2 max phase shift at 1 rad/sec)
# Asymptotes are same as Butterworth filter
p *= 1 / a[-1]**(1/N)
k = 1
elif norm == 'mag':
# -3 dB magnitude point is at 1 rad/sec
p *= 1 / normfactor(a)
k = float(normfactor(a)**-N * a[-1])
else:
raise ValueError('normalization not understood')
z = []
p = p.astype(complex)
return asarray(z), asarray(p), k
def tests():
from scipy import signal
from numpy.testing import (assert_allclose, assert_array_equal,
assert_array_almost_equal)
from sos_stuff import cplxreal
wikipedia_bessels = {
0: [1],
1: [1, 1],
2: [3, 3, 1],
3: [15, 15, 6, 1],
4: [105, 105, 45, 10, 1],
5: [945, 945, 420, 105, 15, 1]
}
for N in range(6):
assert_array_equal(wikipedia_bessels[N], bessel_poly(N))
assert_array_equal(wikipedia_bessels[N][::-1],
bessel_poly(N, reverse=True))
a = bessel_poly(2, reverse=True)
# Compare with symbolic result
assert_allclose(float(normfactor(a)),
(np.sqrt(3)*np.sqrt(np.sqrt(5)-1)) / np.sqrt(2))
# Values from Bond PDF
Bond = {2: 1.36165412871613,
3: 1.75567236868121,
4: 2.11391767490422,
5: 2.42741070215263,
6: 2.70339506120292,
7: 2.95172214703872,
8: 3.17961723751065,
9: 3.39169313891166,
10: 3.59098059456916,
}
for N in range(2, 11):
a = bessel_poly(N, reverse=True)
assert_allclose(float(normfactor(a)), Bond[N])
# 6th order
bond_b = 10395
bond_a = [1, 21, 210, 1260, 4725, 10395, 10395]
b, a = zpk2tf(*besselap(6, 'delay'))
assert_allclose(bond_b, b)
assert_allclose(bond_a, a)
bond_poles = {
1: [-1.0000000000],
2: [-1.5000000000 + 0.8660254038j],
3: [-1.8389073227 + 1.7543809598j, -2.3221853546],
4: [-2.1037893972 + 2.6574180419j, -2.8962106028 + 0.8672341289j],
5: [-2.3246743032 + 3.5710229203j, -3.3519563992 + 1.7426614162j,
-3.6467385953],
6: [-2.5159322478 + 4.4926729537j, -3.7357083563 + 2.6262723114j,
-4.2483593959 + 0.8675096732j],
7: [-2.6856768789 + 5.4206941307j, -4.0701391636 + 3.5171740477j,
-4.7582905282 + 1.7392860611j, -4.9717868585],
8: [-2.8389839489 + 6.3539112986j, -4.3682892172 + 4.4144425005j,
-5.2048407906 + 2.6161751526j, -5.5878860433 + 0.8676144454j],
9: [-2.9792607982 + 7.2914636883j, -4.6384398872 + 5.3172716754j,
-5.6044218195 + 3.4981569179j, -6.1293679043 + 1.7378483835j,
-6.2970191817],
10: [-3.1089162336 + 8.2326994591j, -4.8862195669 + 6.2249854825j,
-5.9675283286 + 4.3849471889j, -6.6152909655 + 2.6115679208j,
-6.9220449054 + 0.8676651955j]
}
for N in range(1, 11):
p1 = np.sort(bond_poles[N])
p2 = np.sort(np.concatenate(cplxreal(besselap(N, 'delay')[1])))
assert_array_almost_equal(p1, p2, decimal=10)
bond_poles = {
1: [-1.0000000000],
2: [-1.1016013306 + 0.6360098248j],
3: [-1.0474091610 + 0.9992644363j, -1.3226757999],
4: [-0.9952087644 + 1.2571057395j, -1.3700678306 + 0.4102497175j],
5: [-0.9576765486 + 1.4711243207j, -1.3808773259 + 0.7179095876j,
-1.5023162714],
6: [-0.9306565229 + 1.6618632689j, -1.3818580976 + 0.9714718907j,
-1.5714904036 + 0.3208963742j],
7: [-0.9098677806 + 1.8364513530j, -1.3789032168 + 1.1915667778j,
-1.6120387662 + 0.5892445069j, -1.6843681793],
8: [-0.8928697188 + 1.9983258436j, -1.3738412176 + 1.3883565759j,
-1.6369394181 + 0.8227956251j, -1.7574084004 + 0.2728675751j],
9: [-0.8783992762 + 2.1498005243j, -1.3675883098 + 1.5677337122j,
-1.6523964846 + 1.0313895670j, -1.8071705350 + 0.5123837306j,
-1.8566005012],
10: [-0.8657569017 + 2.2926048310j, -1.3606922784 + 1.7335057427j,
-1.6618102414 + 1.2211002186j, -1.8421962445 + 0.7272575978j,
-1.9276196914 + 0.2416234710j]
}
for N in range(1, 11):
p1 = np.sort(bond_poles[N])
p2 = np.sort(np.concatenate(cplxreal(besselap(N, 'mag')[1])))
assert_array_almost_equal(p1, p2, decimal=10)
for N in range(26):
assert_allclose(sorted(signal.besselap(N)[1]), sorted(besselap(N)[1]))
# Rane note examples
a = [1, 1, 1/3]
b2, a2 = zpk2tf(*besselap(2, 'delay'))
assert_allclose(a[::-1], a2/b2)
a = [1, 1, 2/5, 1/15]
b2, a2 = zpk2tf(*besselap(3, 'delay'))
assert_allclose(a[::-1], a2/b2)
a = [1, 1, 9/21, 2/21, 1/105]
b2, a2 = zpk2tf(*besselap(4, 'delay'))
assert_allclose(a[::-1], a2/b2)
a = [1, np.sqrt(3), 1]
b2, a2 = zpk2tf(*besselap(2, 'phase'))
assert_allclose(a[::-1], a2/b2)
# TODO: Why so inaccurate?
a = [1, 2.481, 2.463, 1.018]
b2, a2 = zpk2tf(*besselap(3, 'phase'))
assert_array_almost_equal(a[::-1], a2/b2, decimal=1)
# TODO: Why so inaccurate?
a = [1, 3.240, 4.5, 3.240, 1.050]
b2, a2 = zpk2tf(*besselap(4, 'phase'))
assert_array_almost_equal(a[::-1], a2/b2, decimal=1)
# Phase to -3 dB factors:
N, scale = 2, 1.272
scale2 = besselap(N, 'mag')[1] / besselap(N, 'phase')[1]
assert_array_almost_equal(scale, scale2, decimal=3)
# TODO: Why so inaccurate?
N, scale = 3, 1.413
scale2 = besselap(N, 'mag')[1] / besselap(N, 'phase')[1]
assert_array_almost_equal(scale, scale2, decimal=2)
# TODO: Why so inaccurate?
N, scale = 4, 1.533
scale2 = besselap(N, 'mag')[1] / besselap(N, 'phase')[1]
assert_array_almost_equal(scale, scale2, decimal=1)
if __name__ == "__main__":
"""
run in Examples of besselap??
"""
from scipy.signal import freqs, zpk2tf
import matplotlib.pyplot as plt
for norm in ('delay', 'phase', 'mag'):
plt.figure(norm)
plt.suptitle(norm + ' normalized')
for N in (1, 2, 3, 4):
z, p, k = besselap(N, norm)
b, a = zpk2tf(z, p, k)
w, h = freqs(b, a, np.logspace(-3, 3, 1000))
plt.subplot(3, 1, 1)
plt.semilogx(w, 20*np.log10(h))
plt.subplot(3, 1, 2)
plt.semilogx(w[1:], -np.diff(np.unwrap(np.angle(h)))/np.diff(w))
plt.subplot(3, 1, 3)
plt.semilogx(w, np.unwrap(np.angle(h)))
plt.subplot(3, 1, 1)
plt.title('Magnitude')
plt.ylabel('dB')
plt.axvline(1, color='red')
plt.ylim(-40, 3)
plt.grid(True, color='0.7', linestyle='-', which='major')
plt.grid(True, color='0.9', linestyle='-', which='minor')
plt.subplot(3, 1, 2)
plt.title('Group delay')
plt.ylabel('seconds')
plt.axvline(1, color='red')
plt.ylim(0, 3.5)
plt.grid(True, color='0.7', linestyle='-', which='major')
plt.grid(True, color='0.9', linestyle='-', which='minor')
plt.subplot(3, 1, 3)
plt.title('Phase')
plt.ylabel('radians')
plt.axvline(1, color='red')
plt.ylim(-2*np.pi, 0)
plt.grid(True, color='0.7', linestyle='-', which='major')
plt.grid(True, color='0.9', linestyle='-', which='minor')
plt.figure('mag')
plt.subplot(3, 1, 1)
plt.axhline(-3.01, color='red', alpha=0.5)
plt.figure('delay')
plt.subplot(3, 1, 2)
plt.axhline(1, color='red', alpha=0.5)
plt.figure('phase')
plt.subplot(3, 1, 3)
plt.axhline(-np.pi, color='red', alpha=0.5)
plt.axhline(-3*np.pi/4, color='red', alpha=0.5)
plt.axhline(-np.pi/2, color='red', alpha=0.5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment