constfilt
Compile-time IIR digital filter design for C++17
Loading...
Searching...
No Matches
butterworth.hpp
1#ifndef CONSTFILT_BUTTERWORTH_HPP
2#define CONSTFILT_BUTTERWORTH_HPP
3
4#include "analog_filter.hpp"
5#include "vendor/consteig/consteig.hpp"
6#include "vendor/gcem_wrapper.hpp"
7
8namespace constfilt
9{
10
11// Template parameters:
12// T - floating-point scalar type
13// N - filter order (>= 1)
14// Method - TustinPW (default), TustinNW, ZOH, or MatchedZ
15// FilterType - LowPass (default) or HighPass
16template <typename T, consteig::Size N, typename Method = TustinPW,
17 typename FilterType = LowPass>
18class Butterworth
19 : public AnalogFilter<T, N, typename bind_method<T, Method>::type>
20{
21 static_assert(N >= 1u, "Butterworth order must be at least 1");
22
23 using BoundMethod = typename bind_method<T, Method>::type;
24
25 public:
26 // Construct from filter specification; all math is constexpr.
27 constexpr Butterworth(T cutoff_hz, T sample_rate_hz)
28 : AnalogFilter<T, N, BoundMethod>(
29 compute_continuous_tf(cutoff_hz),
30 compute_factored_tf(cutoff_hz, FilterType{}), sample_rate_hz,
31 make_tustin_tag(cutoff_hz, BoundMethod{}))
32 {
33 }
34
35 // Construct with uniform damping ratio zeta across all complex pole pairs.
36 // Routes through generic eigendecomposition (no FactoredTF) to avoid the
37 // Vandermonde singularity that arises when N>=4 produces identical pole
38 // pairs.
39 constexpr Butterworth(T cutoff_hz, T sample_rate_hz, T zeta)
40 : AnalogFilter<T, N, BoundMethod>(
41 compute_continuous_tf_zeta(cutoff_hz, zeta), sample_rate_hz,
42 make_tustin_tag(cutoff_hz, BoundMethod{}))
43 {
44 // Uniform zeta produces repeated complex eigenvalue pairs for N>=4.
45 // consteig's QR iteration cannot split a defective eigenvalue block,
46 // so matrix_exp returns wrong results. MatchedZ and Tustin are fine.
47 static_assert(
48 !is_zoh_tag<Method>::value || N <= 3u,
49 "Butterworth zeta constructor: ZOH is unreliable for N>=4 due to "
50 "repeated eigenvalues. Use MatchedZ or TustinPW instead. "
51 "See https://github.com/MitchellThompkins/constfilt/issues/56");
52 }
53
54 private:
55 // Computes the continuous-time Butterworth transfer function.
56 static constexpr TransferFunction<T, N + 1u, N + 1u> compute_continuous_tf(
57 T cutoff_hz)
58 {
59 const T wc = static_cast<T>(2) * static_cast<T>(GCEM_PI) * cutoff_hz;
60 TransferFunction<T, N + 1u, N + 1u> tf{};
61 continuous_tf(wc, tf.b, tf.a, FilterType{});
62 return tf;
63 }
64
65 // Low-pass
66 //
67 // Numerator: b[N] = wc^N, all other b[k] = 0 (DC gain = 1)
68 //
69 // Denominator: a[k] = p[k] * wc^k where p[] are the normalized (wc=1)
70 // Butterworth polynomial coefficients in descending order (p[0] = 1).
71 static constexpr void continuous_tf(T wc, T (&b)[N + 1u], T (&a)[N + 1u],
72 LowPass)
73 {
74 b[N] = gcem::pow(wc, static_cast<int>(N));
75
76 T p[N + 1u]{};
77 butterworth_poly_coeffs(p);
78 for (consteig::Size k = 0; k <= N; ++k)
79 {
80 a[k] = p[k] * gcem::pow(wc, static_cast<int>(k));
81 }
82 }
83
84 // High-pass
85 //
86 // Derived from the LPF via the LP-to-HP frequency transformation s -> wc/s.
87 //
88 // Numerator: b[0] = 1, all other b[k] = 0 (high-frequency gain = 1)
89 //
90 // Denominator: a[k] = p[N-k] * wc^k. The normalized Butterworth
91 // coefficients appear in reversed order, each scaled by wc^k (p[0] = 1
92 // so a[0] = 1, keeping the denominator monic).
93 static constexpr void continuous_tf(T wc, T (&b)[N + 1u], T (&a)[N + 1u],
94 HighPass)
95 {
96 b[0] = static_cast<T>(1);
97
98 T p[N + 1u]{};
99 butterworth_poly_coeffs(p);
100 for (consteig::Size k = 0; k <= N; ++k)
101 {
102 a[k] = p[N - k] * gcem::pow(wc, static_cast<int>(k));
103 }
104 }
105
106 // LP: poles at wc*exp(j*theta_k), no finite zeros, gain = wc^N.
107 static constexpr FactoredTF<T, N> compute_factored_tf(T cutoff_hz, LowPass)
108 {
109 const T wc = static_cast<T>(2) * static_cast<T>(GCEM_PI) * cutoff_hz;
110 FactoredTF<T, N> factored_tf{};
111 factored_tf.nz = 0;
112 factored_tf.gain = gcem::pow(wc, static_cast<int>(N));
113 for (consteig::Size k = 1u; k <= N; ++k)
114 {
115 const T theta = static_cast<T>(GCEM_PI) *
116 static_cast<T>(2u * k + N - 1u) /
117 static_cast<T>(2u * N);
118 factored_tf.poles[k - 1u] = {wc * gcem::cos(theta),
119 wc * gcem::sin(theta)};
120 }
121 return factored_tf;
122 }
123
124 // HP: poles at wc*exp(-j*theta_k) (magnitude wc), N zeros at s=0, gain=1.
125 static constexpr FactoredTF<T, N> compute_factored_tf(T cutoff_hz, HighPass)
126 {
127 using Complex = consteig::Complex<T>;
128 const T wc = static_cast<T>(2) * static_cast<T>(GCEM_PI) * cutoff_hz;
129 FactoredTF<T, N> factored_tf{};
130 factored_tf.nz = N;
131 factored_tf.gain = static_cast<T>(1);
132 for (consteig::Size k = 1u; k <= N; ++k)
133 {
134 const T theta = static_cast<T>(GCEM_PI) *
135 static_cast<T>(2u * k + N - 1u) /
136 static_cast<T>(2u * N);
137 factored_tf.poles[k - 1u] = {wc * gcem::cos(theta),
138 -wc * gcem::sin(theta)};
139 factored_tf.zeros[k - 1u] =
140 Complex{static_cast<T>(0), static_cast<T>(0)};
141 }
142 return factored_tf;
143 }
144
145 // Normalized Butterworth denominator coefficients (wc=1, monic).
146 // Fills result in descending power order: [1, p[N-1], ..., p[0]]
147 //
148 // Computed by multiplying out (s - p_k) for each Butterworth pole:
149 // p_k = cos(theta_k) + j*sin(theta_k), theta_k = pi*(2k+N-1)/(2N)
150 // k = 1..N
151 // Coefficients are real by construction (poles come in conjugate pairs,
152 // or are real for odd N).
153 static constexpr void butterworth_poly_coeffs(T (&result)[N + 1u])
154 {
155 using Complex = consteig::Complex<T>;
156
157 // poly[i] holds the coefficient of s^i (ascending order).
158 // Start with the constant polynomial 1.
159 Complex poly[N + 1u]{};
160 poly[0] = Complex{static_cast<T>(1), static_cast<T>(0)};
161
162 // Multiply by (s - p_k) for k = 1..N.
163 for (consteig::Size k = 1u; k <= N; ++k)
164 {
165 const T theta = static_cast<T>(GCEM_PI) *
166 static_cast<T>(2u * k + N - 1u) /
167 static_cast<T>(2u * N);
168 const Complex pole{gcem::cos(theta), gcem::sin(theta)};
169
170 // In-place multiply by (s - pole), traversing backwards
171 // to avoid overwriting values still needed this iteration.
172 for (consteig::Size j = k; j > 0u; --j)
173 {
174 poly[j] = poly[j - 1u] - pole * poly[j];
175 }
176 poly[0] =
177 Complex{static_cast<T>(0), static_cast<T>(0)} - pole * poly[0];
178 }
179
180 // Convert ascending-order complex to descending-order real.
181 // The imaginary parts are zero (or near-zero numerical noise).
182 for (consteig::Size i = 0u; i <= N; ++i)
183 {
184 result[i] = poly[N - i].real;
185 }
186 }
187
188 // Normalized Butterworth denominator coefficients for uniform damping
189 // ratio. Each complex pair contributes a quadratic factor (s^2 + 2*zeta*s +
190 // 1); an odd-order real pole contributes (s + 1). Pure real arithmetic, no
191 // complex types. Fills result in descending power order: [1, ..., 1].
192 static constexpr void butterworth_poly_coeffs_zeta(T (&result)[N + 1u],
193 T zeta)
194 {
195 // poly[i] holds the coefficient of s^i (ascending order).
196 T poly[N + 1u]{};
197 poly[0] = static_cast<T>(1);
198 for (consteig::Size pair = 0u; pair < N / 2u; ++pair)
199 {
200 const consteig::Size cur = 2u * pair;
201 for (consteig::Size j = cur + 2u; j > 1u; --j)
202 {
203 poly[j] +=
204 static_cast<T>(2) * zeta * poly[j - 1u] + poly[j - 2u];
205 }
206 poly[1] += static_cast<T>(2) * zeta * poly[0];
207 }
208 if (N % 2u == 1u)
209 {
210 for (consteig::Size j = N; j > 0u; --j)
211 {
212 poly[j] += poly[j - 1u];
213 }
214 }
215 for (consteig::Size i = 0u; i <= N; ++i)
216 {
217 result[i] = poly[N - i];
218 }
219 }
220
221 static constexpr void continuous_tf_zeta(T wc, T zeta, T (&b)[N + 1u],
222 T (&a)[N + 1u], LowPass)
223 {
224 b[N] = gcem::pow(wc, static_cast<int>(N));
225 T p[N + 1u]{};
226 butterworth_poly_coeffs_zeta(p, zeta);
227 for (consteig::Size k = 0; k <= N; ++k)
228 {
229 a[k] = p[k] * gcem::pow(wc, static_cast<int>(k));
230 }
231 }
232
233 static constexpr void continuous_tf_zeta(T wc, T zeta, T (&b)[N + 1u],
234 T (&a)[N + 1u], HighPass)
235 {
236 b[0] = static_cast<T>(1);
237 T p[N + 1u]{};
238 butterworth_poly_coeffs_zeta(p, zeta);
239 for (consteig::Size k = 0; k <= N; ++k)
240 {
241 a[k] = p[N - k] * gcem::pow(wc, static_cast<int>(k));
242 }
243 }
244
245 static constexpr TransferFunction<T, N + 1u, N + 1u>
246 compute_continuous_tf_zeta(T cutoff_hz, T zeta)
247 {
248 const T wc = static_cast<T>(2) * static_cast<T>(GCEM_PI) * cutoff_hz;
249 TransferFunction<T, N + 1u, N + 1u> tf{};
250 continuous_tf_zeta(wc, zeta, tf.b, tf.a, FilterType{});
251 return tf;
252 }
253};
254
255// Convenience aliases for first-order RC-equivalent filters.
256template <typename T, typename Method = TustinPW>
257using FirstOrderLowPass = Butterworth<T, 1u, Method, LowPass>;
258
259template <typename T, typename Method = TustinPW>
260using FirstOrderHighPass = Butterworth<T, 1u, Method, HighPass>;
261
262} // namespace constfilt
263
264#endif // CONSTFILT_BUTTERWORTH_HPP