1#ifndef CONSTFILT_BUTTERWORTH_HPP
2#define CONSTFILT_BUTTERWORTH_HPP
4#include "analog_filter.hpp"
5#include "vendor/consteig/consteig.hpp"
6#include "vendor/gcem_wrapper.hpp"
16template <
typename T, consteig::Size N,
typename Method = TustinPW,
17 typename FilterType = LowPass>
19 :
public AnalogFilter<T, N, typename bind_method<T, Method>::type>
21 static_assert(N >= 1u,
"Butterworth order must be at least 1");
23 using BoundMethod =
typename bind_method<T, Method>::type;
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{}))
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{}))
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");
56 static constexpr TransferFunction<T, N + 1u, N + 1u> compute_continuous_tf(
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{});
71 static constexpr void continuous_tf(T wc, T (&b)[N + 1u], T (&a)[N + 1u],
74 b[N] = gcem::pow(wc,
static_cast<int>(N));
77 butterworth_poly_coeffs(p);
78 for (consteig::Size k = 0; k <= N; ++k)
80 a[k] = p[k] * gcem::pow(wc,
static_cast<int>(k));
93 static constexpr void continuous_tf(T wc, T (&b)[N + 1u], T (&a)[N + 1u],
96 b[0] =
static_cast<T
>(1);
99 butterworth_poly_coeffs(p);
100 for (consteig::Size k = 0; k <= N; ++k)
102 a[k] = p[N - k] * gcem::pow(wc,
static_cast<int>(k));
107 static constexpr FactoredTF<T, N> compute_factored_tf(T cutoff_hz, LowPass)
109 const T wc =
static_cast<T
>(2) *
static_cast<T
>(GCEM_PI) * cutoff_hz;
110 FactoredTF<T, N> factored_tf{};
112 factored_tf.gain = gcem::pow(wc,
static_cast<int>(N));
113 for (consteig::Size k = 1u; k <= N; ++k)
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)};
125 static constexpr FactoredTF<T, N> compute_factored_tf(T cutoff_hz, HighPass)
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{};
131 factored_tf.gain =
static_cast<T
>(1);
132 for (consteig::Size k = 1u; k <= N; ++k)
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)};
153 static constexpr void butterworth_poly_coeffs(T (&result)[N + 1u])
155 using Complex = consteig::Complex<T>;
159 Complex poly[N + 1u]{};
160 poly[0] = Complex{
static_cast<T
>(1),
static_cast<T
>(0)};
163 for (consteig::Size k = 1u; k <= N; ++k)
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)};
172 for (consteig::Size j = k; j > 0u; --j)
174 poly[j] = poly[j - 1u] - pole * poly[j];
177 Complex{
static_cast<T
>(0),
static_cast<T
>(0)} - pole * poly[0];
182 for (consteig::Size i = 0u; i <= N; ++i)
184 result[i] = poly[N - i].real;
192 static constexpr void butterworth_poly_coeffs_zeta(T (&result)[N + 1u],
197 poly[0] =
static_cast<T
>(1);
198 for (consteig::Size pair = 0u; pair < N / 2u; ++pair)
200 const consteig::Size cur = 2u * pair;
201 for (consteig::Size j = cur + 2u; j > 1u; --j)
204 static_cast<T
>(2) * zeta * poly[j - 1u] + poly[j - 2u];
206 poly[1] +=
static_cast<T
>(2) * zeta * poly[0];
210 for (consteig::Size j = N; j > 0u; --j)
212 poly[j] += poly[j - 1u];
215 for (consteig::Size i = 0u; i <= N; ++i)
217 result[i] = poly[N - i];
221 static constexpr void continuous_tf_zeta(T wc, T zeta, T (&b)[N + 1u],
222 T (&a)[N + 1u], LowPass)
224 b[N] = gcem::pow(wc,
static_cast<int>(N));
226 butterworth_poly_coeffs_zeta(p, zeta);
227 for (consteig::Size k = 0; k <= N; ++k)
229 a[k] = p[k] * gcem::pow(wc,
static_cast<int>(k));
233 static constexpr void continuous_tf_zeta(T wc, T zeta, T (&b)[N + 1u],
234 T (&a)[N + 1u], HighPass)
236 b[0] =
static_cast<T
>(1);
238 butterworth_poly_coeffs_zeta(p, zeta);
239 for (consteig::Size k = 0; k <= N; ++k)
241 a[k] = p[N - k] * gcem::pow(wc,
static_cast<int>(k));
245 static constexpr TransferFunction<T, N + 1u, N + 1u>
246 compute_continuous_tf_zeta(T cutoff_hz, T zeta)
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{});
256template <
typename T,
typename Method = TustinPW>
257using FirstOrderLowPass = Butterworth<T, 1u, Method, LowPass>;
259template <
typename T,
typename Method = TustinPW>
260using FirstOrderHighPass = Butterworth<T, 1u, Method, HighPass>;