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
11struct LowPass
12{
13};
14
15struct HighPass
16{
17};
18
19// Template parameters:
20// T - floating-point scalar type
21// N - filter order (>= 1)
22// Method - discretization method (Tustin default), ZOH, or MatchedZ
23// FilterType - LowPass (default) or HighPass
24template <typename T, consteig::Size N, typename Method = Tustin,
25 typename FilterType = LowPass>
26class Butterworth : public AnalogFilter<T, N, Method>
27{
28 static_assert(N >= 1u, "Butterworth order must be at least 1");
29
30 public:
31 // Construct from filter specification; all math is constexpr.
32 constexpr Butterworth(T cutoff_hz, T sample_rate_hz)
33 : AnalogFilter<T, N, Method>(compute_continuous_tf(cutoff_hz),
34 sample_rate_hz)
35 {
36 }
37
38 private:
39 // Computes the continuous-time Butterworth transfer function.
40 static constexpr TransferFunction<T, N + 1u, N + 1u> compute_continuous_tf(
41 T cutoff_hz)
42 {
43 const T wc = static_cast<T>(2) * static_cast<T>(GCEM_PI) * cutoff_hz;
44 TransferFunction<T, N + 1u, N + 1u> tf{};
45 continuous_tf(wc, tf.b, tf.a, FilterType{});
46 return tf;
47 }
48
49 // Low-pass
50 //
51 // Numerator: b[N] = wc^N, all other b[k] = 0 (DC gain = 1)
52 //
53 // Denominator: a[k] = p[k] * wc^k where p[] are the normalized (wc=1)
54 // Butterworth polynomial coefficients in descending order (p[0] = 1).
55 static constexpr void continuous_tf(T wc, T (&b)[N + 1u], T (&a)[N + 1u],
56 LowPass)
57 {
58 b[N] = gcem::pow(wc, static_cast<int>(N));
59
60 T p[N + 1u]{};
61 butterworth_poly_coeffs(p);
62 for (consteig::Size k = 0; k <= N; ++k)
63 {
64 a[k] = p[k] * gcem::pow(wc, static_cast<int>(k));
65 }
66 }
67
68 // High-pass
69 //
70 // Derived from the LPF via the LP-to-HP frequency transformation s -> wc/s.
71 //
72 // Numerator: b[0] = 1, all other b[k] = 0 (high-frequency gain = 1)
73 //
74 // Denominator: a[k] = p[N-k] * wc^k. The normalized Butterworth
75 // coefficients appear in reversed order, each scaled by wc^k (p[0] = 1
76 // so a[0] = 1, keeping the denominator monic).
77 static constexpr void continuous_tf(T wc, T (&b)[N + 1u], T (&a)[N + 1u],
78 HighPass)
79 {
80 b[0] = static_cast<T>(1);
81
82 T p[N + 1u]{};
83 butterworth_poly_coeffs(p);
84 for (consteig::Size k = 0; k <= N; ++k)
85 {
86 a[k] = p[N - k] * gcem::pow(wc, static_cast<int>(k));
87 }
88 }
89
90 // Normalized Butterworth denominator coefficients (wc=1, monic).
91 // Fills result in descending power order: [1, p[N-1], ..., p[0]]
92 //
93 // Computed by multiplying out (s - p_k) for each Butterworth pole:
94 // p_k = cos(theta_k) + j*sin(theta_k), theta_k = pi*(2k+N-1)/(2N)
95 // k = 1..N
96 // Coefficients are real by construction (poles come in conjugate pairs,
97 // or are real for odd N).
98 static constexpr void butterworth_poly_coeffs(T (&result)[N + 1u])
99 {
100 using Complex = consteig::Complex<T>;
101
102 // poly[i] holds the coefficient of s^i (ascending order).
103 // Start with the constant polynomial 1.
104 Complex poly[N + 1u]{};
105 poly[0] = Complex{static_cast<T>(1), static_cast<T>(0)};
106
107 // Multiply by (s - p_k) for k = 1..N.
108 for (consteig::Size k = 1u; k <= N; ++k)
109 {
110 const T theta = static_cast<T>(GCEM_PI) *
111 static_cast<T>(2u * k + N - 1u) /
112 static_cast<T>(2u * N);
113 const Complex pole{gcem::cos(theta), gcem::sin(theta)};
114
115 // In-place multiply by (s - pole), traversing backwards
116 // to avoid overwriting values still needed this iteration.
117 for (consteig::Size j = k; j > 0u; --j)
118 {
119 poly[j] = poly[j - 1u] - pole * poly[j];
120 }
121 poly[0] =
122 Complex{static_cast<T>(0), static_cast<T>(0)} - pole * poly[0];
123 }
124
125 // Convert ascending-order complex to descending-order real.
126 // The imaginary parts are zero (or near-zero numerical noise).
127 for (consteig::Size i = 0u; i <= N; ++i)
128 {
129 result[i] = poly[N - i].real;
130 }
131 }
132};
133
134// Convenience aliases for first-order RC-equivalent filters.
135template <typename T, typename Method = Tustin>
136using FirstOrderLowPass = Butterworth<T, 1u, Method, LowPass>;
137
138template <typename T, typename Method = Tustin>
139using FirstOrderHighPass = Butterworth<T, 1u, Method, HighPass>;
140
141} // namespace constfilt
142
143#endif // CONSTFILT_BUTTERWORTH_HPP