constfilt
Compile-time IIR digital filter design for C++17
Loading...
Searching...
No Matches
discretize.hpp
1#ifndef CONSTFILT_DISCRETIZE_HPP
2#define CONSTFILT_DISCRETIZE_HPP
3
4#include "vendor/consteig/consteig.hpp"
5#include "vendor/gcem_wrapper.hpp"
6
7namespace constfilt
8{
9
10// Tag types
11
12struct ZOH
13{
14};
15
16struct MatchedZ
17{
18};
19
20struct Tustin // Bilinear
21{
22};
23
24// Data structures
25
26template <typename T, consteig::Size N> struct StateSpace
27{
28 consteig::Matrix<T, N, N> A{};
29 consteig::Matrix<T, N, 1> B{};
30 consteig::Matrix<T, 1, N> C{};
31 T D{};
32};
33
34template <typename T, consteig::Size NB, consteig::Size NA>
35struct TransferFunction
36{
37 T b[NB]{};
38 T a[NA]{};
39};
40
41// Matrix exponential
42
43// matrix_exp(A) via eigendecomposition:
44// Ad = V * diag(exp(lam_i)) * V^{-1}
45// where V = eigenvectors, lam_i = eigenvalues (complex).
46// Real part extracted at the end (imaginary parts cancel for real A).
47template <typename T, consteig::Size N>
48constexpr consteig::Matrix<T, N, N> matrix_exp(
49 const consteig::Matrix<T, N, N> &A)
50{
51 using Complex = consteig::Complex<T>;
52 using ComplexMat_NN = consteig::Matrix<Complex, N, N>;
53 using ComplexMat_N1 = consteig::Matrix<Complex, N, 1>;
54
55 // 1. Eigenvalues and eigenvectors
56 const auto evals = consteig::eigenvalues(A); // Matrix<Complex, N, 1>
57 const auto V = consteig::eigenvectors(A, evals); // Matrix<Complex, N, N>
58
59 // 2. Invert V column-by-column via LU
60 const auto lu_V = consteig::lu(V);
61
62 ComplexMat_NN V_inv{};
63 for (consteig::Size col = 0; col < N; ++col)
64 {
65 ComplexMat_N1 e_col{};
66 e_col(col, 0) = Complex{static_cast<T>(1), static_cast<T>(0)};
67 auto col_vec = consteig::lu_solve(lu_V, e_col);
68 for (consteig::Size row = 0; row < N; ++row)
69 {
70 V_inv(row, col) = col_vec(row, 0);
71 }
72 }
73
74 // 3. Accumulate: matrix_exp = sum_i exp(lam_i) * v_i * w_i^T
75 // where v_i = column i of V, w_i^T = row i of V_inv
76 ComplexMat_NN result_c{};
77 for (consteig::Size i = 0; i < N; ++i)
78 {
79 Complex exp_lambda = consteig::exp(evals(i, 0));
80 for (consteig::Size r = 0; r < N; ++r)
81 {
82 for (consteig::Size c = 0; c < N; ++c)
83 {
84 result_c(r, c) =
85 result_c(r, c) + exp_lambda * V(r, i) * V_inv(i, c);
86 }
87 }
88 }
89
90 // 4. Extract real part
91 consteig::Matrix<T, N, N> result{};
92 for (consteig::Size r = 0; r < N; ++r)
93 {
94 for (consteig::Size c = 0; c < N; ++c)
95 {
96 result(r, c) = result_c(r, c).real;
97 }
98 }
99 return result;
100}
101
102// ZOH discretization
103
104// ZOH: Ad = matrix_exp(Ac*Ts), Bd = Ac^{-1} * (Ad - I) * Bc
105// Solve Ac * Bd = (Ad - I) * Bc via LU.
106// Cc and Dc are unchanged.
107// Returns StateSpace, as opposed to TransferFunction to preserve access to the
108// discrete matrices for callers that need them (e.g. state estimation, observer
109// design).
110template <typename T, consteig::Size N>
111constexpr StateSpace<T, N> zoh_discretize(const StateSpace<T, N> &sys_c, T Ts,
112 ZOH /*tag*/)
113{
114 const auto &Ac = sys_c.A;
115 const auto &Bc = sys_c.B;
116
117 // Ac * Ts
118 const consteig::Matrix<T, N, N> Ad = matrix_exp(Ts * Ac);
119
120 // (Ad - I)
121 const consteig::Matrix<T, N, N> AdmI = Ad - consteig::eye<T, N>();
122
123 // (Ad - I) * Bc
124 const consteig::Matrix<T, N, 1> rhs = AdmI * Bc;
125
126 // Bd = Ac^{-1} * rhs -> solve Ac * Bd = rhs
127 const auto lu_Ac = consteig::lu(Ac);
128 const auto Bd = consteig::lu_solve(lu_Ac, rhs);
129
130 StateSpace<T, N> sys_d{};
131 sys_d.A = Ad;
132 sys_d.B = Bd;
133 sys_d.C = sys_c.C;
134 sys_d.D = sys_c.D;
135 return sys_d;
136}
137
138// Characteristic polynomial
139
140// Fills monic characteristic polynomial of Ad:
141// [1, c_1, c_2, ..., c_N] (N+1 coefficients)
142// Uses consteig::eigenvalues to obtain lam_1..lam_N, then builds
143// (z - lam_1)(z - lam_2)...(z - lam_N) in complex arithmetic.
144// Real parts are extracted at the end (imaginary parts cancel for real A).
145template <typename T, consteig::Size N>
146constexpr void char_poly(const consteig::Matrix<T, N, N> &Ad,
147 T (&coeffs)[N + 1u])
148{
149 using Complex = consteig::Complex<T>;
150
151 const auto evals = consteig::eigenvalues(Ad); // Matrix<Complex, N, 1>
152
153 // p[0..k] holds the monic polynomial of degree k after k iterations.
154 Complex p[N + 1u]{};
155 p[0] = Complex{static_cast<T>(1), static_cast<T>(0)};
156
157 for (consteig::Size k = 0; k < N; ++k)
158 {
159 const Complex lam = evals(k, 0);
160 // Multiply degree-k poly by (z - lam), working high-to-low in-place.
161 p[k + 1u] = Complex{static_cast<T>(0), static_cast<T>(0)} - lam * p[k];
162 for (consteig::Size i = k; i > 0u; --i)
163 {
164 p[i] = p[i] - lam * p[i - 1u];
165 }
166 // p[0] is unchanged (stays 1)
167 }
168
169 for (consteig::Size i = 0; i <= N; ++i)
170 {
171 coeffs[i] = p[i].real;
172 }
173}
174
175// Markov numerator
176
177// Computes numerator polynomial b from Markov parameters and denominator a.
178// h[0] = D
179// h[k] = C * A^{k-1} * B for k = 1..N
180// b[k] = sum_{j=0}^{k} a[j] * h[k-j]
181template <typename T, consteig::Size N>
182constexpr void markov_numerator(const StateSpace<T, N> &sys_d,
183 const T (&a_coeffs)[N + 1u], T (&b)[N + 1u])
184{
185 const auto &Ad = sys_d.A;
186 const auto &Bd = sys_d.B;
187 const auto &Cd = sys_d.C;
188
189 // Compute Markov parameters h[0..N]
190 T h[N + 1u]{};
191 h[0] = sys_d.D;
192
193 // A_pow = A^{k-1}, starting with A^0 = I
194 consteig::Matrix<T, N, N> A_pow = consteig::eye<T, N>();
195
196 for (consteig::Size k = 1u; k <= N; ++k)
197 {
198 // h[k] = C * A_pow * B (scalar)
199 T val = static_cast<T>(0);
200 for (consteig::Size c = 0; c < N; ++c)
201 {
202 // (A_pow * B)[c] = sum_col A_pow(c,col)*B(col,0)
203 T AB_c = static_cast<T>(0);
204 for (consteig::Size col = 0; col < N; ++col)
205 {
206 AB_c += A_pow(c, col) * Bd(col, 0);
207 }
208 val += Cd(0, c) * AB_c;
209 }
210 h[k] = val;
211
212 // Advance: A_pow = A_pow * Ad
213 A_pow = A_pow * Ad;
214 }
215
216 // Convolve: b[k] = sum_{j=0}^{k} a[j] * h[k-j]
217 for (consteig::Size k = 0; k <= N; ++k)
218 {
219 T sum = static_cast<T>(0);
220 for (consteig::Size jj = 0; jj <= k; ++jj)
221 {
222 sum += a_coeffs[jj] * h[k - jj];
223 }
224 b[k] = sum;
225 }
226}
227
228// ss_to_tf
229
230// Full pipeline: discrete state-space -> (b, a) transfer function.
231template <typename T, consteig::Size N>
232constexpr TransferFunction<T, N + 1u, N + 1u> ss_to_tf(
233 const StateSpace<T, N> &sys_d)
234{
235 TransferFunction<T, N + 1u, N + 1u> tf{};
236 char_poly(sys_d.A, tf.a);
237 markov_numerator(sys_d, tf.a, tf.b);
238 return tf;
239}
240
241// tf_to_ss
242
243// Build controllable canonical form continuous-time state-space from
244// analog transfer function coefficients in descending power order:
245//
246// H(s) = (b[0]*s^N + b[1]*s^{N-1} + ... + b[N])
247// / (a[0]*s^N + a[1]*s^{N-1} + ... + a[N])
248//
249// Handles proper (deg b = deg a) and strictly proper (b[0]=0) cases.
250// a[0] must be non-zero; the denominator is normalized to monic form
251// internally.
252//
253// Resulting state-space (controllable canonical form):
254// A: super-diagonal = 1; last row = [-a_N, -a_{N-1}, ..., -a_1] / a_0
255// B: [0, ..., 0, 1]^T
256// C: [e_N, e_{N-1}, ..., e_1] where e_k = b_k/a_0 - D*(a_k/a_0)
257// D: b[0] / a[0]
258template <typename T, consteig::Size N>
259constexpr StateSpace<T, N> tf_to_ss(const T (&b)[N + 1u], const T (&a)[N + 1u])
260{
261 StateSpace<T, N> sys{};
262
263 const T inv_a0 = static_cast<T>(1) / a[0];
264
265 sys.D = b[0] * inv_a0;
266
267 // Numerator residual: e[k] = b[k]/a[0] - D*(a[k]/a[0]) for k=1..N
268 T e[N + 1u]{};
269 for (consteig::Size k = 1u; k <= N; ++k)
270 {
271 e[k] = b[k] * inv_a0 - sys.D * (a[k] * inv_a0);
272 }
273
274 // A: super-diagonal
275 for (consteig::Size row = 0; row < N - 1u; ++row)
276 {
277 sys.A(row, row + 1u) = static_cast<T>(1);
278 }
279
280 // A: last row = -a[N-k]/a[0] for k=0..N-1
281 for (consteig::Size k = 0; k < N; ++k)
282 {
283 sys.A(N - 1u, k) = -(a[N - k] * inv_a0);
284 }
285
286 // B: last entry = 1
287 sys.B(N - 1u, 0) = static_cast<T>(1);
288
289 // C: C[0][k] = e[N-k]
290 for (consteig::Size k = 0; k < N; ++k)
291 {
292 sys.C(0, k) = e[N - k];
293 }
294
295 return sys;
296}
297
298// Matched-Z discretization (TF entry point)
299
300// Full matched-Z from analog transfer function coefficients.
301// Maps each finite analog zero via z = exp(s*Ts), pads with zeros at z = -1
302// for strictly proper systems, and matches gain at a test frequency w_c.
303// Reference: Octave control pkg @tf/__c2d__.m, lines 32-66.
304template <typename T, consteig::Size N>
305constexpr TransferFunction<T, N + 1u, N + 1u> matched_z_discretize_tf(
306 const T (&b_c)[N + 1u], const T (&a_c)[N + 1u], T Ts, MatchedZ /*tag*/)
307{
308 using Complex = consteig::Complex<T>;
309
310 // Step 1: count leading zeros in b_c; derive nz (finite analog zero count).
311 consteig::Size d_b = 0;
312 while (d_b <= N && b_c[d_b] == static_cast<T>(0))
313 {
314 ++d_b;
315 }
316 const consteig::Size nz = (d_b > N) ? 0u : N - d_b;
317
318 // Step 2: continuous leading-coefficient gain.
319 const T k_c = (d_b > N) ? static_cast<T>(0) : b_c[d_b] / a_c[0];
320
321 // Step 3: find analog poles (roots of a_c) via companion matrix.
322 // consteig exposes eigenvalues, not polynomial roots; a companion matrix
323 // has eigenvalues equal to the polynomial roots by construction.
324 consteig::Matrix<T, N, N> A_pole{};
325 for (consteig::Size i = 0; i < N - 1u; ++i)
326 A_pole(i + 1u, i) = static_cast<T>(1);
327 for (consteig::Size i = 0; i < N; ++i)
328 A_pole(i, N - 1u) = -(a_c[N - i] / a_c[0]);
329 const auto p_c_evals = consteig::eigenvalues(A_pole);
330
331 // Step 4: map poles to z-domain; build monic denominator polynomial.
332 Complex p_d_vals[N]{};
333 Complex pole_poly[N + 1u]{};
334 pole_poly[0] = Complex{static_cast<T>(1), static_cast<T>(0)};
335 for (consteig::Size k = 0; k < N; ++k)
336 {
337 p_d_vals[k] =
338 consteig::exp(p_c_evals(k, 0) * Complex{Ts, static_cast<T>(0)});
339 const Complex zk = p_d_vals[k];
340 pole_poly[k + 1u] =
341 Complex{static_cast<T>(0), static_cast<T>(0)} - zk * pole_poly[k];
342 for (consteig::Size i = k; i > 0u; --i)
343 {
344 pole_poly[i] = pole_poly[i] - zk * pole_poly[i - 1u];
345 }
346 }
347
348 // Steps 5-7: find analog zeros (roots of b_c) via companion matrix,
349 // same trick as above. Embedded in NxN so consteig::eigenvalues can be
350 // called with a fixed size; the top-left nz x nz block holds the actual
351 // companion, the remaining entries are 0, producing d_b spurious
352 // eigenvalues at the origin that are discarded afterward.
353 Complex z_c_finite[N]{};
354 Complex z_d_finite[N]{};
355 Complex zero_poly[N + 1u]{};
356 zero_poly[0] = Complex{static_cast<T>(1), static_cast<T>(0)};
357
358 if (nz > 0u)
359 {
360 consteig::Matrix<T, N, N> A_zero{};
361 for (consteig::Size i = 0; i + 1u < nz; ++i)
362 A_zero(i + 1u, i) = static_cast<T>(1);
363 for (consteig::Size i = 0; i < nz; ++i)
364 A_zero(i, nz - 1u) = -(b_c[d_b + (nz - i)] / b_c[d_b]);
365 const auto z_c_evals = consteig::eigenvalues(A_zero);
366
367 // Mark d_b spurious eigenvalues (smallest magnitude = from zero block).
368 bool spurious[N]{};
369 for (consteig::Size m = 0; m < d_b; ++m)
370 {
371 consteig::Size min_idx = 0;
372 T min_mag_sq = static_cast<T>(-1);
373 for (consteig::Size i = 0; i < N; ++i)
374 {
375 if (!spurious[i])
376 {
377 const T mag_sq =
378 z_c_evals(i, 0).real * z_c_evals(i, 0).real +
379 z_c_evals(i, 0).imag * z_c_evals(i, 0).imag;
380 if (min_mag_sq < static_cast<T>(0) || mag_sq < min_mag_sq)
381 {
382 min_mag_sq = mag_sq;
383 min_idx = i;
384 }
385 }
386 }
387 spurious[min_idx] = true;
388 }
389
390 // Collect finite zeros and build zero polynomial prod(z - z_d_k).
391 consteig::Size nz_cnt = 0u;
392 for (consteig::Size k = 0; k < N; ++k)
393 {
394 if (!spurious[k])
395 {
396 z_c_finite[nz_cnt] = z_c_evals(k, 0);
397 z_d_finite[nz_cnt] = consteig::exp(
398 z_c_evals(k, 0) * Complex{Ts, static_cast<T>(0)});
399 const Complex zk = z_d_finite[nz_cnt];
400 zero_poly[nz_cnt + 1u] =
401 Complex{static_cast<T>(0), static_cast<T>(0)} -
402 zk * zero_poly[nz_cnt];
403 for (consteig::Size i = nz_cnt; i > 0u; --i)
404 {
405 zero_poly[i] = zero_poly[i] - zk * zero_poly[i - 1u];
406 }
407 ++nz_cnt;
408 }
409 }
410 }
411
412 // Step 8: pad with zeros at z = -1 to reach numerator degree N-1.
413 const consteig::Size n_extra = (nz + 1u < N) ? (N - nz - 1u) : 0u;
414 for (consteig::Size e = 0; e < n_extra; ++e)
415 {
416 const consteig::Size cur_deg = nz + e;
417 zero_poly[cur_deg + 1u] = zero_poly[cur_deg + 1u] + zero_poly[cur_deg];
418 for (consteig::Size i = cur_deg; i > 0u; --i)
419 {
420 zero_poly[i] = zero_poly[i] + zero_poly[i - 1u];
421 }
422 }
423 const consteig::Size num_deg = nz + n_extra;
424
425 // Step 9: find matching frequency w_c (avoid collision with poles/zeros).
426 const T tol = gcem::sqrt(consteig::epsilon<T>());
427 T w_c = static_cast<T>(0);
428 for (consteig::Size attempt = 0; attempt < 1000u; ++attempt)
429 {
430 bool collision = false;
431 for (consteig::Size i = 0; i < N && !collision; ++i)
432 {
433 const T dr = w_c - p_c_evals(i, 0).real;
434 const T di = p_c_evals(i, 0).imag;
435 if (dr * dr + di * di < tol * tol)
436 {
437 collision = true;
438 }
439 }
440 for (consteig::Size i = 0; i < nz && !collision; ++i)
441 {
442 const T dr = w_c - z_c_finite[i].real;
443 const T di = z_c_finite[i].imag;
444 if (dr * dr + di * di < tol * tol)
445 {
446 collision = true;
447 }
448 }
449 if (!collision)
450 {
451 break;
452 }
453 w_c += static_cast<T>(0.1) / Ts;
454 }
455
456 // Step 10: compute discrete gain k_d matching H_d(w_d) = H_c(w_c).
457 const Complex w_c_cx{w_c, static_cast<T>(0)};
458 const Complex w_d_cx =
459 consteig::exp(w_c_cx * Complex{Ts, static_cast<T>(0)});
460
461 Complex num_c_cx{static_cast<T>(1), static_cast<T>(0)};
462 for (consteig::Size i = 0; i < nz; ++i)
463 {
464 num_c_cx = num_c_cx * (w_c_cx - z_c_finite[i]);
465 }
466
467 Complex den_c_cx{static_cast<T>(1), static_cast<T>(0)};
468 for (consteig::Size i = 0; i < N; ++i)
469 {
470 den_c_cx = den_c_cx * (w_c_cx - p_c_evals(i, 0));
471 }
472
473 Complex num_d_cx{static_cast<T>(1), static_cast<T>(0)};
474 for (consteig::Size i = 0; i < N; ++i)
475 {
476 num_d_cx = num_d_cx * (w_d_cx - p_d_vals[i]);
477 }
478
479 Complex den_d_cx{static_cast<T>(1), static_cast<T>(0)};
480 for (consteig::Size i = 0; i < nz; ++i)
481 {
482 den_d_cx = den_d_cx * (w_d_cx - z_d_finite[i]);
483 }
484 for (consteig::Size e = 0; e < n_extra; ++e)
485 {
486 den_d_cx = den_d_cx *
487 (w_d_cx - Complex{static_cast<T>(-1), static_cast<T>(0)});
488 }
489
490 const Complex gain_num =
491 Complex{k_c, static_cast<T>(0)} * num_c_cx * num_d_cx;
492 const Complex gain_den = den_c_cx * den_d_cx;
493 const T gain_den_sq =
494 gain_den.real * gain_den.real + gain_den.imag * gain_den.imag;
495 const T k_d =
496 (gain_num.real * gain_den.real + gain_num.imag * gain_den.imag) /
497 gain_den_sq;
498
499 // Step 11: assemble output TF.
500 TransferFunction<T, N + 1u, N + 1u> tf{};
501 for (consteig::Size i = 0; i <= N; ++i)
502 {
503 tf.a[i] = pole_poly[i].real;
504 }
505 const consteig::Size pad = N - num_deg;
506 for (consteig::Size i = 0; i <= num_deg; ++i)
507 {
508 tf.b[pad + i] = k_d * zero_poly[i].real;
509 }
510
511 return tf;
512}
513
514// Tustin discretization
515//
516// With alpha = 2/Ts:
517// M = I - (1/alpha)*Ac
518// P = I + (1/alpha)*Ac
519// Ad = P * M^{-1} (right-solve: M^T * Ad^T = P^T)
520// Bd = (1/alpha) * (Ad + I) * Bc
521// Cd = Cc * M^{-1} (right-solve: M^T * Cd^T = Cc^T)
522// Dd = Dc + (1/alpha) * Cd * Bc
523//
524// M is LU-factorized once and reused for both Ad and Cd.
525// See https://dsp.stackexchange.com/questions/45042
526// Returns StateSpace (not TransferFunction) to preserve access to the discrete
527// matrices for callers that need them (e.g. state estimation, observer design).
528template <typename T, consteig::Size N>
529constexpr StateSpace<T, N> tustin_discretize(const StateSpace<T, N> &sys_c,
530 T Ts, Tustin /*tag*/)
531{
532 const auto &Ac = sys_c.A;
533 const auto &Bc = sys_c.B;
534 const auto &Cc = sys_c.C;
535
536 const T alpha = static_cast<T>(2) / Ts;
537 const T inv_alpha = static_cast<T>(1) / alpha;
538
539 // M = I - (1/alpha)*Ac, P = I + (1/alpha)*Ac
540 const consteig::Matrix<T, N, N> M = consteig::eye<T, N>() - inv_alpha * Ac;
541 const consteig::Matrix<T, N, N> P = consteig::eye<T, N>() + inv_alpha * Ac;
542
543 const auto lu_M{consteig::lu(M)};
544
545 // M^{-1} column by column via LU solve (lu_solve only accepts a single rhs)
546 consteig::Matrix<T, N, N> M_inv{};
547 for (consteig::Size col = 0; col < N; ++col)
548 {
549 consteig::Matrix<T, N, 1> e_col{};
550 e_col(col, 0) = static_cast<T>(1);
551 const auto col_vec{consteig::lu_solve(lu_M, e_col)};
552 for (consteig::Size row = 0; row < N; ++row)
553 {
554 M_inv(row, col) = col_vec(row, 0);
555 }
556 }
557
558 // Ad = P * M^{-1}
559 const consteig::Matrix<T, N, N> Ad{P * M_inv};
560
561 // Bd = (1/alpha) * (Ad + I) * Bc
562 const consteig::Matrix<T, N, 1> Bd{inv_alpha *
563 (Ad + consteig::eye<T, N>()) * Bc};
564
565 // Cd = Cc * M^{-1}
566 const consteig::Matrix<T, 1, N> Cd{Cc * M_inv};
567
568 // Dd = Dc + (1/alpha) * Cd * Bc
569 const T Dd{sys_c.D + inv_alpha * (Cd * Bc)(0, 0)};
570
571 StateSpace<T, N> sys_d{};
572 sys_d.A = Ad;
573 sys_d.B = Bd;
574 sys_d.C = Cd;
575 sys_d.D = Dd;
576 return sys_d;
577}
578
579// Backward-compatible wrapper: recovers (b_c, a_c) from SS and delegates.
580template <typename T, consteig::Size N>
581constexpr TransferFunction<T, N + 1u, N + 1u> matched_z_discretize(
582 const StateSpace<T, N> &sys_c, T Ts, MatchedZ /*tag*/)
583{
584 T a_c[N + 1u]{};
585 char_poly(sys_c.A, a_c);
586 T b_c[N + 1u]{};
587 markov_numerator(sys_c, a_c, b_c);
588 return matched_z_discretize_tf<T, N>(b_c, a_c, Ts, MatchedZ{});
589}
590
591// analog_to_digital (state-space overloads)
592
593template <typename T, consteig::Size N>
594constexpr TransferFunction<T, N + 1u, N + 1u> analog_to_digital(
595 const StateSpace<T, N> &sys_c, T Ts, ZOH)
596{
597 return ss_to_tf(zoh_discretize(sys_c, Ts, ZOH{}));
598}
599
600template <typename T, consteig::Size N>
601constexpr TransferFunction<T, N + 1u, N + 1u> analog_to_digital(
602 const StateSpace<T, N> &sys_c, T Ts, MatchedZ)
603{
604 return matched_z_discretize(sys_c, Ts, MatchedZ{});
605}
606
607template <typename T, consteig::Size N>
608constexpr TransferFunction<T, N + 1u, N + 1u> analog_to_digital(
609 const StateSpace<T, N> &sys_c, T Ts, Tustin)
610{
611 return ss_to_tf(tustin_discretize(sys_c, Ts, Tustin{}));
612}
613
614// analog_to_digital (TF overloads)
615
616template <typename T, consteig::Size N>
617constexpr TransferFunction<T, N + 1u, N + 1u> analog_to_digital(
618 const T (&b_c)[N + 1u], const T (&a_c)[N + 1u], T Ts, ZOH)
619{
620 return ss_to_tf(zoh_discretize(tf_to_ss<T, N>(b_c, a_c), Ts, ZOH{}));
621}
622
623template <typename T, consteig::Size N>
624constexpr TransferFunction<T, N + 1u, N + 1u> analog_to_digital(
625 const T (&b_c)[N + 1u], const T (&a_c)[N + 1u], T Ts, MatchedZ)
626{
627 return matched_z_discretize_tf<T, N>(b_c, a_c, Ts, MatchedZ{});
628}
629
630template <typename T, consteig::Size N>
631constexpr TransferFunction<T, N + 1u, N + 1u> analog_to_digital(
632 const T (&b_c)[N + 1u], const T (&a_c)[N + 1u], T Ts, Tustin)
633{
634 return ss_to_tf(tustin_discretize(tf_to_ss<T, N>(b_c, a_c), Ts, Tustin{}));
635}
636
637} // namespace constfilt
638
639#endif // CONSTFILT_DISCRETIZE_HPP