Aleph-w 3.0
A C++ Library for Data Structures and Algorithms
Loading...
Searching...
No Matches
fft_test.cc
Go to the documentation of this file.
1/*
2 Aleph_w
3
4 Data structures & Algorithms
5 version 2.0.0b
6 https://github.com/lrleon/Aleph-w
7
8 This file is part of Aleph-w library
9
10 Copyright (c) 2002-2026 Leandro Rabindranath Leon
11
12 Permission is hereby granted, free of charge, to any person obtaining a copy
13 of this software and associated documentation files (the "Software"), to deal
14 in the Software without restriction, including without limitation the rights
15 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 copies of the Software, and to permit persons to whom the Software is
17 furnished to do so, subject to the following conditions:
18
19 The above copyright notice and this permission notice shall be included in all
20 copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 SOFTWARE.
29*/
30
36# include <gtest/gtest.h>
37
38# include <algorithm>
39# include <cmath>
40# include <complex>
41# include <numbers>
42# include <random>
43# include <stdexcept>
44# include <vector>
45
46# include <fft.H>
47# include <tpl_array.H>
48
49using namespace Aleph;
50
51namespace
52{
53 using FFTD = FFT<double>;
54 using Complex = FFTD::Complex;
55
56 constexpr double eps = 1e-9;
57
58 void expect_complex_near(const Complex & lhs, const Complex & rhs,
59 const double tol = eps)
60 {
61 EXPECT_NEAR(lhs.real(), rhs.real(), tol);
62 EXPECT_NEAR(lhs.imag(), rhs.imag(), tol);
63 }
64
66 const Array<Complex> & rhs,
67 const double tol = eps)
68 {
69 ASSERT_EQ(lhs.size(), rhs.size());
70 for (size_t i = 0; i < lhs.size(); ++i)
71 expect_complex_near(lhs[i], rhs[i], tol);
72 }
73
75 const Array<double> & rhs,
76 const double tol = eps)
77 {
78 ASSERT_EQ(lhs.size(), rhs.size());
79 for (size_t i = 0; i < lhs.size(); ++i)
80 EXPECT_NEAR(lhs[i], rhs[i], tol);
81 }
82
83 size_t half_spectrum_index(const double frequency,
84 const double sample_rate,
85 const size_t num_points)
86 {
87 const double scaled =
88 frequency / (sample_rate / 2.0) * static_cast<double>(num_points - 1);
89 const long long rounded = std::llround(scaled);
90 if (rounded < 0)
91 return 0;
92 const size_t idx = static_cast<size_t>(rounded);
93 return std::min(idx, num_points - 1);
94 }
95
96 double response_magnitude_at(const FFTD::FrequencyResponse & response,
97 const double frequency,
98 const double sample_rate)
99 {
100 return std::abs(response.response[half_spectrum_index(frequency,
102 response.response.size())]);
103 }
104
106 const Array<Array<Complex>> & rhs,
107 const double tol = eps)
108 {
109 ASSERT_EQ(lhs.size(), rhs.size());
110 for (size_t i = 0; i < lhs.size(); ++i)
111 expect_complex_array_near(lhs[i], rhs[i], tol);
112 }
113
115 const Array<Array<Complex>> & rhs,
116 const double tol = eps)
117 {
118 ASSERT_EQ(lhs.size(), rhs.size());
119 for (size_t i = 0; i < lhs.size(); ++i)
120 expect_complex_array_near(lhs[i], rhs[i], tol);
121 }
122
124 const Array<Array<Array<Complex>>> & rhs,
125 const double tol = eps)
126 {
127 ASSERT_EQ(lhs.size(), rhs.size());
128 for (size_t i = 0; i < lhs.size(); ++i)
129 expect_matrix_near(lhs[i], rhs[i], tol);
130 }
131
133 const Array<Array<double>> & rhs,
134 const double tol = eps)
135 {
136 ASSERT_EQ(lhs.size(), rhs.size());
137 for (size_t i = 0; i < lhs.size(); ++i)
138 expect_real_array_near(lhs[i], rhs[i], tol);
139 }
140
142 const Array<Array<Complex>> & src)
143 {
144 for (size_t i = 0; i < src.size(); ++i)
145 dst.append(src[i]);
146 }
147
149 const Array<double> & src)
150 {
151 for (size_t i = 0; i < src.size(); ++i)
152 dst.append(src[i]);
153 }
154
156 const Array<Complex> & rhs,
157 const double tol = eps)
158 {
159 ASSERT_EQ(lhs.size(), rhs.size());
160
161 std::vector<bool> matched(rhs.size(), false);
162 for (size_t i = 0; i < lhs.size(); ++i)
163 {
164 bool found = false;
165 for (size_t j = 0; j < rhs.size(); ++j)
166 {
167 if (matched[j])
168 continue;
169
170 if (std::abs(lhs[i] - rhs[j]) <= tol)
171 {
172 matched[j] = true;
173 found = true;
174 break;
175 }
176 }
177
178 EXPECT_TRUE(found) << "Unexpected complex root " << lhs[i];
179 }
180 }
181
182 Array<double> real_polynomial_from_roots(const Array<Complex> & roots)
183 {
185 coeffs(0) = Complex(1.0, 0.0);
186
187 for (size_t i = 0; i < roots.size(); ++i)
188 {
190 for (size_t j = 0; j < next.size(); ++j)
191 next(j) = Complex(0.0, 0.0);
192
193 for (size_t j = 0; j < coeffs.size(); ++j)
194 {
195 next(j) += coeffs[j];
196 next(j + 1) -= coeffs[j] * roots[i];
197 }
198
199 coeffs = std::move(next);
200 }
201
203 for (size_t i = 0; i < coeffs.size(); ++i)
204 {
205 EXPECT_NEAR(coeffs[i].imag(), 0.0, 1e-10);
206 output(i) = coeffs[i].real();
207 }
208 return output;
209 }
210
211 Array<Complex> lift_real_input(const Array<double> & input)
212 {
214 lifted.reserve(input.size());
215 for (size_t i = 0; i < input.size(); ++i)
216 lifted.append(Complex(input[i], 0.0));
217 return lifted;
218 }
219
221 {
222 const size_t n = input.size();
224 output.reserve(n);
225
226 const double sign = invert ? 1.0 : -1.0;
227 for (size_t k = 0; k < n; ++k)
228 {
229 Complex sum(0.0, 0.0);
230 for (size_t t = 0; t < n; ++t)
231 {
232 const double angle = sign * 2.0 * std::numbers::pi *
233 static_cast<double>(k * t) /
234 static_cast<double>(n);
235 sum += input[t] * std::polar(1.0, angle);
236 }
237
238 if (invert)
239 sum /= static_cast<double>(n);
240
241 output.append(sum);
242 }
243
244 return output;
245 }
246
248 const Array<Complex> & b)
249 {
250 if (a.is_empty() or b.is_empty())
251 return {};
252
254 for (size_t i = 0; i < output.size(); ++i)
255 output(i) = Complex(0.0, 0.0);
256
257 for (size_t i = 0; i < a.size(); ++i)
258 for (size_t j = 0; j < b.size(); ++j)
259 output(i + j) += a[i] * b[j];
260
261 return output;
262 }
263
265 const Array<double> & b)
266 {
267 if (a.is_empty() or b.is_empty())
268 return {};
269
271 for (size_t i = 0; i < output.size(); ++i)
272 output(i) = 0.0;
273
274 for (size_t i = 0; i < a.size(); ++i)
275 for (size_t j = 0; j < b.size(); ++j)
276 output(i + j) += a[i] * b[j];
277
278 return output;
279 }
280
282 const Array<double> & coeffs,
283 const size_t up,
284 const size_t down)
285 {
286 if (signal.is_empty() or coeffs.is_empty())
287 return {};
288
290 for (size_t i = 0; i < upsampled.size(); ++i)
291 upsampled(i) = 0.0;
292 for (size_t i = 0; i < signal.size(); ++i)
293 upsampled(i * up) = signal[i];
294
296 const size_t output_size = (filtered.size() + down - 1) / down;
298 for (size_t i = 0; i < output.size(); ++i)
299 output(i) = filtered[i * down];
300 return output;
301 }
302
303 Array<double> sine_signal(const size_t n,
304 const double sample_rate,
305 const double frequency,
306 const double amplitude = 1.0,
307 const double phase = 0.0)
308 {
309 Array<double> signal;
310 signal.reserve(n);
311 for (size_t i = 0; i < n; ++i)
312 signal.append(amplitude
313 * std::sin(2.0 * std::numbers::pi * frequency
314 * static_cast<double>(i) / sample_rate
315 + phase));
316 return signal;
317 }
318
319 size_t max_index(const Array<double> & input)
320 {
321 EXPECT_FALSE(input.is_empty());
322 size_t index = 0;
323 for (size_t i = 1; i < input.size(); ++i)
324 if (input[i] > input[index])
325 index = i;
326 return index;
327 }
328
330 {
332 output.reserve(input.size());
333 for (size_t i = input.size(); i > 0; --i)
334 output.append(input[i - 1]);
335 return output;
336 }
337
339 const Array<double> & coeffs)
340 {
341 if (signal.is_empty() or coeffs.is_empty())
342 return {};
343
345 for (size_t i = 0; i < signal.size(); ++i)
346 {
347 double sum = 0.0;
348 const size_t limit = std::min(i + 1, coeffs.size());
349 for (size_t k = 0; k < limit; ++k)
350 sum += coeffs[k] * signal[i - k];
351 output(i) = sum;
352 }
353
354 return output;
355 }
356
357 size_t naive_filtfilt_pad_length(const size_t signal_size,
358 const size_t coeff_size)
359 {
360 if (signal_size <= 1 or coeff_size <= 1)
361 return 0;
362
363 const size_t taps_minus_one = coeff_size - 1;
364 const size_t suggested = taps_minus_one > std::numeric_limits<size_t>::max() / 3
365 ? std::numeric_limits<size_t>::max()
366 : taps_minus_one * 3;
367 return std::min(suggested, signal_size - 1);
368 }
369
371 const size_t pad_len)
372 {
373 if (pad_len == 0)
374 return signal;
375
377 output.reserve(signal.size() + 2 * pad_len);
378
379 for (size_t i = 0; i < pad_len; ++i)
380 output.append(signal[pad_len - i]);
381 for (size_t i = 0; i < signal.size(); ++i)
382 output.append(signal[i]);
383 for (size_t i = 0; i < pad_len; ++i)
384 output.append(signal[signal.size() - 2 - i]);
385
386 return output;
387 }
388
390 const size_t offset,
391 const size_t length)
392 {
394 output.reserve(length);
395 for (size_t i = 0; i < length; ++i)
396 output.append(input[offset + i]);
397 return output;
398 }
399
401 const Array<double> & coeffs)
402 {
403 if (signal.is_empty() or coeffs.is_empty())
404 return {};
405
406 if (coeffs.size() == 1)
407 {
409 const double gain = coeffs[0] * coeffs[0];
410 for (size_t i = 0; i < signal.size(); ++i)
411 output(i) = signal[i] * gain;
412 return output;
413 }
414
415 const size_t pad_len = naive_filtfilt_pad_length(signal.size(), coeffs.size());
417 const Array<double> forward = naive_prefix_filter(padded, coeffs);
418 const Array<double> backward = naive_prefix_filter(reverse_real_array(forward), coeffs);
419 return slice_real_array(reverse_real_array(backward), pad_len, signal.size());
420 }
421
422 struct RefIIRCoefficients
423 {
424 Array<double> numerator;
425 Array<double> denominator;
426 };
427
429 {
430 double max_value = 0.0;
431 for (size_t i = 0; i < input.size(); ++i)
432 max_value = std::max(max_value, std::abs(input[i]));
433 return max_value;
434 }
435
437 {
438 if (input.is_empty())
439 return 0;
440
441 const double tol = (max_abs_real_array(input) + 1.0) * 64.0
442 * std::numeric_limits<double>::epsilon();
443 size_t n = input.size();
444 while (n > 1 and std::abs(input[n - 1]) <= tol)
445 --n;
446 return n;
447 }
448
449 RefIIRCoefficients normalize_iir_reference(const Array<double> & numerator,
450 const Array<double> & denominator)
451 {
452 const size_t num_length = effective_coeff_length_ref(numerator);
453 const size_t den_length = effective_coeff_length_ref(denominator);
456
457 const double a0 = denominator[0];
458 const double tol = (max_abs_real_array(denominator) + 1.0) * 64.0
459 * std::numeric_limits<double>::epsilon();
460 EXPECT_GT(std::abs(a0), tol);
461
462 const size_t order = std::max(num_length, den_length) - 1;
463 RefIIRCoefficients coeffs;
464 coeffs.numerator = Array<double>::create(order + 1);
465 coeffs.denominator = Array<double>::create(order + 1);
466 for (size_t i = 0; i <= order; ++i)
467 {
468 coeffs.numerator(i) = 0.0;
469 coeffs.denominator(i) = 0.0;
470 }
471
472 for (size_t i = 0; i < num_length; ++i)
473 coeffs.numerator(i) = numerator[i] / a0;
474 for (size_t i = 0; i < den_length; ++i)
475 coeffs.denominator(i) = denominator[i] / a0;
476 coeffs.denominator(0) = 1.0;
477 return coeffs;
478 }
479
482 const size_t n)
483 {
484 if (n == 0)
485 return {};
486
487 auto coeff = [&matrix, n](const size_t row, const size_t col) -> double &
488 {
489 return matrix(row * n + col);
490 };
491
492 double max_entry = 0.0;
493 for (size_t i = 0; i < matrix.size(); ++i)
494 max_entry = std::max(max_entry, std::abs(matrix[i]));
495 for (size_t i = 0; i < rhs.size(); ++i)
496 max_entry = std::max(max_entry, std::abs(rhs[i]));
497
498 const double tol = (max_entry + 1.0) * 128.0
499 * std::numeric_limits<double>::epsilon();
500
501 for (size_t col = 0; col < n; ++col)
502 {
503 size_t pivot_row = col;
504 double pivot_abs = std::abs(coeff(col, col));
505 for (size_t row = col + 1; row < n; ++row)
506 {
507 const double candidate = std::abs(coeff(row, col));
508 if (candidate > pivot_abs)
509 {
511 pivot_row = row;
512 }
513 }
514
515 EXPECT_GT(pivot_abs, tol);
516
517 if (pivot_row != col)
518 {
519 for (size_t k = col; k < n; ++k)
520 std::swap(coeff(col, k), coeff(pivot_row, k));
521 std::swap(rhs(col), rhs(pivot_row));
522 }
523
524 const double pivot = coeff(col, col);
525 for (size_t row = col + 1; row < n; ++row)
526 {
527 const double factor = coeff(row, col) / pivot;
528 coeff(row, col) = 0.0;
529 for (size_t k = col + 1; k < n; ++k)
530 coeff(row, k) -= factor * coeff(col, k);
531 rhs(row) -= factor * rhs[col];
532 }
533 }
534
536 for (size_t row = n; row > 0; --row)
537 {
538 const size_t i = row - 1;
539 double sum = rhs[i];
540 for (size_t col = i + 1; col < n; ++col)
541 sum -= coeff(i, col) * solution[col];
542 solution(i) = sum / coeff(i, i);
543 }
544
545 return solution;
546 }
547
549 const Array<double> & denominator)
550 {
551 const size_t order = denominator.size() - 1;
552 if (order == 0)
553 return {};
554
556 for (size_t i = 0; i < system.size(); ++i)
557 system(i) = 0.0;
558
559 auto coeff = [&system, order](const size_t row, const size_t col) -> double &
560 {
561 return system(row * order + col);
562 };
563
564 for (size_t i = 0; i < order; ++i)
565 coeff(i, i) = 1.0;
566 for (size_t i = 0; i < order; ++i)
567 {
568 coeff(i, 0) += denominator[i + 1];
569 if (i + 1 < order)
570 coeff(i, i + 1) -= 1.0;
571 }
572
574 for (size_t i = 0; i < order; ++i)
575 rhs(i) = numerator[i + 1] - denominator[i + 1] * numerator[0];
576 return solve_dense_system_ref(system, rhs, order);
577 }
578
579 Array<double> scale_real_array(const Array<double> & input, const double factor)
580 {
582 for (size_t i = 0; i < input.size(); ++i)
583 output(i) = input[i] * factor;
584 return output;
585 }
586
588 const Array<double> & numerator,
589 const Array<double> & denominator,
590 const Array<double> & initial_state = {})
591 {
592 if (signal.is_empty())
593 return {};
594
595 const size_t order = denominator.size() - 1;
597 if (order == 0)
598 {
599 for (size_t i = 0; i < signal.size(); ++i)
600 output(i) = numerator[0] * signal[i];
601 return output;
602 }
603
605 for (size_t i = 0; i < order; ++i)
606 state(i) = initial_state.is_empty() ? 0.0 : initial_state[i];
607
608 for (size_t n = 0; n < signal.size(); ++n)
609 {
610 const double x = signal[n];
611 const double y = numerator[0] * x + state[0];
612 output(n) = y;
613 for (size_t i = 0; i + 1 < order; ++i)
614 state(i) = state[i + 1] + numerator[i + 1] * x
615 - denominator[i + 1] * y;
616 state(order - 1) = numerator[order] * x - denominator[order] * y;
617 }
618
619 return output;
620 }
621
623 const Array<double> & numerator,
624 const Array<double> & denominator)
625 {
626 if (signal.is_empty() or numerator.is_empty() or denominator.is_empty())
627 return {};
628
629 const auto coeffs = normalize_iir_reference(numerator, denominator);
630 const size_t order = coeffs.denominator.size() - 1;
631
632 if (order == 0)
633 {
635 const double gain = coeffs.numerator[0] * coeffs.numerator[0];
636 for (size_t i = 0; i < signal.size(); ++i)
637 output(i) = signal[i] * gain;
638 return output;
639 }
640
641 const size_t pad_len = naive_filtfilt_pad_length(signal.size(),
642 coeffs.denominator.size());
644 const Array<double> zi = iir_steady_state_ref(coeffs.numerator,
645 coeffs.denominator);
646
647 const Array<double> forward = naive_iir_filter(padded,
648 coeffs.numerator,
649 coeffs.denominator,
651 const Array<double> reversed = reverse_real_array(forward);
652 const Array<double> backward = naive_iir_filter(reversed,
653 coeffs.numerator,
654 coeffs.denominator,
655 scale_real_array(zi, reversed[0]));
656 return slice_real_array(reverse_real_array(backward), pad_len, signal.size());
657 }
658
661 {
662 if (signal.is_empty() or sections.is_empty())
663 return {};
664
667 size_t total_order = 0;
668 for (size_t i = 0; i < sections.size(); ++i)
669 {
670 const auto coeffs = normalize_iir_reference(sections[i].numerator(),
671 sections[i].denominator());
672 total_order += coeffs.denominator.size() - 1;
673 normalized_sections.append(coeffs);
674 }
675
678
679 for (size_t i = 0; i < normalized_sections.size(); ++i)
680 {
681 const auto & coeffs = normalized_sections[i];
683 coeffs.numerator,
684 coeffs.denominator,
685 scale_real_array(iir_steady_state_ref(coeffs.numerator,
686 coeffs.denominator),
687 stage[0]));
688 }
689
691 for (size_t i = 0; i < normalized_sections.size(); ++i)
692 {
693 const auto & coeffs = normalized_sections[i];
695 coeffs.numerator,
696 coeffs.denominator,
697 scale_real_array(iir_steady_state_ref(coeffs.numerator,
698 coeffs.denominator),
699 stage[0]));
700 }
701
702 const size_t pad_len = naive_filtfilt_pad_length(signal.size(), total_order + 1);
704 }
705
706 template <std::floating_point Real>
707 void expect_typed_complex_array_near(const Array<std::complex<Real>> & lhs,
708 const Array<std::complex<Real>> & rhs,
709 const Real tol)
710 {
711 ASSERT_EQ(lhs.size(), rhs.size());
712 for (size_t i = 0; i < lhs.size(); ++i)
713 EXPECT_LE(std::abs(lhs[i] - rhs[i]), tol);
714 }
715}
716
718{
719 Array<Complex> empty;
720 EXPECT_THROW(FFTD::transform(empty, false), std::invalid_argument);
721}
722
724{
725 EXPECT_FALSE(FFTD::is_power_of_two(0));
726 EXPECT_TRUE(FFTD::is_power_of_two(1));
727 EXPECT_TRUE(FFTD::is_power_of_two(8));
728 EXPECT_FALSE(FFTD::is_power_of_two(12));
729}
730
732{
733 const std::string backend = FFTD::simd_backend_name();
734 const std::string batch_backend = FFTD::batched_plan_simd_backend_name();
735 const std::string detected = FFTD::detected_simd_backend_name();
736 const std::string preference = FFTD::simd_preference_name();
737 EXPECT_TRUE(backend == "scalar" or backend == "avx2" or backend == "neon");
738 EXPECT_TRUE(batch_backend == "scalar" or batch_backend == "avx2"
739 or batch_backend == "neon");
740 EXPECT_TRUE(detected == "scalar" or detected == "avx2" or detected == "neon");
741 EXPECT_TRUE(preference == "auto" or preference == "scalar"
742 or preference == "avx2" or preference == "neon");
743 EXPECT_FALSE(FFTD::avx2_runtime_available() and FFTD::neon_runtime_available());
744 EXPECT_FALSE(FFTD::avx2_runtime_available() and not FFTD::avx2_kernel_compiled());
745 EXPECT_FALSE(FFTD::neon_runtime_available() and not FFTD::neon_kernel_compiled());
746 EXPECT_EQ(FFTD::avx2_runtime_available(), detected == "avx2");
747 EXPECT_EQ(FFTD::neon_runtime_available(), detected == "neon");
748}
749
751{
752 Array<Complex> signal = {
753 Complex(1.5, -0.5),
754 Complex(-2.0, 3.0),
755 Complex(0.25, 4.5),
756 Complex(7.0, -1.0),
757 Complex(-3.5, 2.25),
758 Complex(1.0, 0.0),
759 Complex(2.0, -2.0),
760 Complex(-1.25, 0.75)
761 };
762
763 Array<Complex> work = signal;
764 FFTD::transform(work, false);
765 FFTD::transform(work, true);
766
767 expect_complex_array_near(work, signal, 1e-8);
768}
769
771{
772 std::mt19937_64 rng(20260305);
773 std::uniform_real_distribution<double> dist(-5.0, 5.0);
774
775 for (const size_t n : {size_t(2), size_t(3), size_t(4), size_t(5),
776 size_t(6), size_t(8), size_t(10), size_t(15),
777 size_t(16), size_t(17)})
778 {
779 Array<Complex> signal;
780 signal.reserve(n);
781 for (size_t i = 0; i < n; ++i)
782 signal.append(Complex(dist(rng), dist(rng)));
783
784 const auto expected = naive_dft(signal, false);
785 const auto obtained = FFTD::transformed(signal, false);
787 }
788}
789
791{
792 Array<double> signal = {1.0, 2.0, -1.0, 0.5, 3.0, -2.0, 4.0, -0.75, 1.25};
793
794 const auto real_spectrum = FFTD::transform(signal);
795 const auto complex_spectrum = FFTD::transformed(lift_real_input(signal), false);
796
798}
799
801{
802 std::mt19937_64 rng(20260306);
803 std::uniform_real_distribution<double> dist(-4.0, 4.0);
804
805 for (const size_t n : {size_t(3), size_t(5), size_t(6), size_t(10),
806 size_t(12), size_t(15), size_t(17)})
807 {
808 Array<Complex> signal;
809 signal.reserve(n);
810 for (size_t i = 0; i < n; ++i)
811 signal.append(Complex(dist(rng), dist(rng)));
812
813 Array<Complex> work = signal;
814 FFTD::transform(work, false);
815 FFTD::transform(work, true);
816 expect_complex_array_near(work, signal, 1e-8);
817 }
818}
819
821{
822 std::vector<Complex> signal = {
823 Complex(1.5, -0.5),
824 Complex(-2.0, 3.0),
825 Complex(0.25, 4.5),
826 Complex(7.0, -1.0),
827 Complex(-3.5, 2.25),
828 Complex(1.0, 0.0),
829 Complex(2.0, -2.0),
830 Complex(-1.25, 0.75)
831 };
832
833 const Array<Complex> as_array(signal.begin(), signal.end());
834 const auto spectrum_from_vector = FFTD::transform(signal);
835 const auto spectrum_from_array = FFTD::transformed(as_array, false);
837
838 std::vector<Complex> spectrum_vector(spectrum_from_vector.begin(),
840 const auto restored = FFTD::inverse_transform(spectrum_vector);
842}
843
845{
846 std::vector<double> signal = {1.0, 2.0, -1.0, 0.5, 3.0, -2.0, 4.0, -0.75};
847 Array<double> as_array(signal.begin(), signal.end());
848
849 const auto spectrum_from_vector = FFTD::transform(signal);
850 const auto spectrum_from_array = FFTD::transform(as_array);
852
853 const auto restored = FFTD::inverse_transform_real(spectrum_from_vector);
855}
856
858{
859 ThreadPool pool(4);
860
861 Array<double> real_signal = {1.0, -0.5, 3.0, 2.5, -1.0, 4.0, 0.75, -2.25};
862 const auto expected_real = FFTD::transform(real_signal);
863 const auto obtained_real = FFTD::spectrum(real_signal);
865
866 const auto expected_real_parallel = FFTD::ptransform(pool, real_signal);
867 const auto obtained_real_parallel = FFTD::pspectrum(pool, real_signal);
869
870 Array<Complex> complex_signal = lift_real_input(real_signal);
871 const auto expected_complex = FFTD::transformed(complex_signal, false);
872 const auto obtained_complex = FFTD::spectrum(complex_signal);
874
875 std::vector<Complex> complex_vector(complex_signal.begin(), complex_signal.end());
876 const auto expected_complex_parallel = FFTD::ptransformed(pool, complex_vector, false);
877 const auto obtained_complex_parallel = FFTD::pspectrum(pool, complex_vector);
879}
880
882{
884 Complex(3.0, 4.0),
885 Complex(-1.0, 0.0),
886 Complex(0.0, -2.0),
887 Complex(0.5, -0.5)
888 };
889
890 const auto magnitude = FFTD::magnitude_spectrum(bins);
891 const auto power = FFTD::power_spectrum(bins);
892
893 Array<double> expected_magnitude = {5.0, 1.0, 2.0, std::sqrt(0.5)};
894 Array<double> expected_power = {25.0, 1.0, 4.0, 0.5};
895
898
899 std::vector<Complex> bins_vector(bins.begin(), bins.end());
900 expect_real_array_near(FFTD::magnitude_spectrum(bins_vector), expected_magnitude, 1e-12);
901 expect_real_array_near(FFTD::power_spectrum(bins_vector), expected_power, 1e-12);
902}
903
905{
907 Complex(1.0, 0.0),
908 Complex(0.0, 1.0),
909 Complex(-1.0, 0.0),
910 Complex(0.0, -1.0)
911 };
912
913 const auto phase = FFTD::phase_spectrum(bins);
914 ASSERT_EQ(phase.size(), 4u);
915 EXPECT_NEAR(phase[0], 0.0, 1e-12);
916 EXPECT_NEAR(phase[1], std::numbers::pi / 2.0, 1e-12);
917 EXPECT_NEAR(phase[2], std::numbers::pi, 1e-12);
918 EXPECT_NEAR(phase[3], -std::numbers::pi / 2.0, 1e-12);
919
920 std::vector<Complex> bins_vector(bins.begin(), bins.end());
921 expect_real_array_near(FFTD::phase_spectrum(bins_vector), phase, 1e-12);
922}
923
925{
926 std::vector<double> signal = {1.0, -0.5, 2.0, 0.25, -1.0, 3.0};
927 Array<double> padded(signal.begin(), signal.end());
928 padded.append(0.0);
929 padded.append(0.0);
930
931 const auto obtained = FFTD::transform_padded(signal);
932 const auto expected = FFTD::transform(padded);
933
934 ASSERT_EQ(obtained.size(), 8u);
936}
937
939{
940 ThreadPool pool(4);
941 Array<Complex> signal = {
942 Complex(1.5, -0.5),
943 Complex(-2.0, 3.0),
944 Complex(0.25, 4.5),
945 Complex(7.0, -1.0),
946 Complex(-3.5, 2.25),
947 Complex(1.0, 0.0),
948 Complex(2.0, -2.0),
949 Complex(-1.25, 0.75)
950 };
951
952 const auto expected = FFTD::transformed(signal, false);
953 const auto obtained = FFTD::ptransformed(pool, signal, false);
955
956 Array<Complex> work = signal;
957 FFTD::ptransform(pool, work, false);
959}
960
962{
963 ThreadPool pool(4);
964 Array<Complex> signal = {
965 Complex(1.5, -0.5),
966 Complex(-2.0, 3.0),
967 Complex(0.25, 4.5),
968 Complex(7.0, -1.0),
969 Complex(-3.5, 2.25),
970 Complex(1.0, 0.0),
971 Complex(2.0, -2.0),
972 Complex(-1.25, 0.75),
973 Complex(0.5, -0.25),
974 Complex(-0.75, 1.5)
975 };
976
977 const auto expected = FFTD::transformed(signal, false);
978 const auto obtained = FFTD::ptransformed(pool, signal, false);
980}
981
983{
984 ThreadPool pool(4);
985 Array<double> signal = {2.0, -1.0, 0.5, 3.0, -2.5, 1.25, 4.0, -0.75};
986
987 const auto expected = FFTD::transform(signal);
988 const auto obtained = FFTD::ptransform(pool, signal);
990}
991
993{
994 Array<double> signal = {0.5, -1.25, 3.5, 2.0, -0.75, 4.25, 1.0, -2.0, 0.25};
995
996 const auto spectrum = FFTD::transform(signal);
997 const auto restored = FFTD::inverse_transform_real(spectrum);
998
999 expect_real_array_near(restored, signal, 1e-8);
1000}
1001
1003{
1005 Complex(10.0, 0.0),
1006 Complex(1.0, 2.0),
1007 Complex(-3.0, 0.0),
1008 Complex(1.5, -0.5)
1009 };
1010
1011 EXPECT_THROW(FFTD::inverse_transform_real(invalid), std::domain_error);
1012}
1013
1015{
1016 Array<double> signal = {2.0, -1.0, 0.5, 3.0, -2.5, 1.25, 4.0, -0.75};
1017 const auto spectrum = FFTD::transform(signal);
1018
1019 ASSERT_EQ(spectrum.size(), 8u);
1020 expect_complex_near(spectrum[0], std::conj(spectrum[0]), 1e-8);
1021 expect_complex_near(spectrum[4], std::conj(spectrum[4]), 1e-8);
1022
1023 for (size_t k = 1; k < 4; ++k)
1024 expect_complex_near(spectrum[k], std::conj(spectrum[8 - k]), 1e-8);
1025}
1026
1028{
1029 Array<double> even_signal = {1.0, -2.0, 0.5, 3.5, -1.0, 4.0};
1030 Array<double> odd_signal = {0.25, -1.5, 2.0, 0.0, -0.75, 1.25, 3.0};
1031
1032 const auto even_full = FFTD::transform(even_signal);
1033 const auto even_compact = FFTD::rfft(even_signal);
1034 ASSERT_EQ(even_compact.size(), even_signal.size() / 2 + 1);
1035 for (size_t i = 0; i < even_compact.size(); ++i)
1037
1038 const auto odd_full = FFTD::transform(odd_signal);
1039 const auto odd_compact = FFTD::rfft(odd_signal);
1040 ASSERT_EQ(odd_compact.size(), odd_signal.size() / 2 + 1);
1041 for (size_t i = 0; i < odd_compact.size(); ++i)
1043}
1044
1046{
1047 ThreadPool pool(4);
1048
1049 Array<double> even_signal = {1.0, -2.0, 0.5, 3.5, -1.0, 4.0};
1050 const auto even_compact = FFTD::rfft(even_signal);
1051 expect_real_array_near(FFTD::irfft(even_compact), even_signal, 1e-8);
1052 expect_real_array_near(FFTD::pirfft(pool, even_compact), even_signal, 1e-8);
1053
1054 Array<double> odd_signal = {0.25, -1.5, 2.0, 0.0, -0.75, 1.25, 3.0};
1055 const auto odd_compact = FFTD::rfft(odd_signal);
1056 expect_real_array_near(FFTD::irfft(odd_compact, odd_signal.size()), odd_signal, 1e-8);
1057 expect_real_array_near(FFTD::pirfft(pool,
1059 odd_signal.size()),
1060 odd_signal,
1061 1e-8);
1062}
1063
1065{
1067 Complex(1.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0),
1068 Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0), Complex(0.0, 0.0)
1069 };
1070 const auto impulse_spectrum = FFTD::transformed(impulse, false);
1071 for (size_t i = 0; i < impulse_spectrum.size(); ++i)
1072 expect_complex_near(impulse_spectrum[i], Complex(1.0, 0.0), 1e-8);
1073
1074 Array<double> alternating = {1.0, -1.0, 1.0, -1.0};
1075 const auto alternating_spectrum = FFTD::transform(alternating);
1076 ASSERT_EQ(alternating_spectrum.size(), 4u);
1077 expect_complex_near(alternating_spectrum[0], Complex(0.0, 0.0), 1e-8);
1078 expect_complex_near(alternating_spectrum[1], Complex(0.0, 0.0), 1e-8);
1079 expect_complex_near(alternating_spectrum[2], Complex(4.0, 0.0), 1e-8);
1080 expect_complex_near(alternating_spectrum[3], Complex(0.0, 0.0), 1e-8);
1081}
1082
1084{
1085 Array<Complex> a = {
1086 Complex(1.0, 2.0),
1087 Complex(-3.0, 0.5),
1088 Complex(2.5, -1.5)
1089 };
1090 Array<Complex> b = {
1091 Complex(0.5, -1.0),
1092 Complex(4.0, 2.0)
1093 };
1094
1095 const auto expected = naive_convolution(a, b);
1096 const auto obtained = FFTD::multiply(a, b);
1098}
1099
1101{
1102 ThreadPool pool(4);
1103 Array<Complex> a = {
1104 Complex(1.0, 2.0),
1105 Complex(-3.0, 0.5),
1106 Complex(2.5, -1.5)
1107 };
1108 Array<Complex> b = {
1109 Complex(0.5, -1.0),
1110 Complex(4.0, 2.0)
1111 };
1112
1113 const auto expected = FFTD::multiply(a, b);
1114 const auto obtained = FFTD::pmultiply(pool, a, b);
1116}
1117
1119{
1120 std::vector<Complex> a = {
1121 Complex(1.0, 2.0),
1122 Complex(-3.0, 0.5),
1123 Complex(2.5, -1.5)
1124 };
1125 Array<Complex> b = {
1126 Complex(0.5, -1.0),
1127 Complex(4.0, 2.0)
1128 };
1129
1130 const Array<Complex> a_array(a.begin(), a.end());
1131 const auto expected = FFTD::multiply(a_array, b);
1132 const auto obtained = FFTD::multiply(a, b);
1134}
1135
1137{
1138 Array<double> a = {1.0, 2.0, 3.0, -1.0};
1139 Array<double> b = {4.0, -1.0, 0.5};
1140
1141 const auto expected = naive_convolution(a, b);
1142 const auto obtained = FFTD::multiply(a, b);
1144}
1145
1147{
1148 ThreadPool pool(4);
1149 Array<double> a = {1.0, 2.0, 3.0, -1.0};
1150 Array<double> b = {4.0, -1.0, 0.5};
1151
1152 const auto expected = FFTD::multiply(a, b);
1153 const auto obtained = FFTD::pmultiply(pool, a, b);
1155}
1156
1158{
1159 std::vector<double> a = {1.0, 2.0, 3.0, -1.0};
1160 Array<double> b = {4.0, -1.0, 0.5};
1161
1162 const Array<double> a_array(a.begin(), a.end());
1163 const auto expected = FFTD::multiply(a_array, b);
1164 const auto obtained = FFTD::multiply(a, b);
1166}
1167
1169{
1170 std::mt19937_64 rng(424242);
1171 std::uniform_real_distribution<double> dist(-3.0, 3.0);
1172
1173 for (int trial = 0; trial < 20; ++trial)
1174 {
1175 const size_t n = static_cast<size_t>(trial % 6 + 1);
1176 const size_t m = static_cast<size_t>((trial * 3) % 5 + 1);
1177
1180 ra.reserve(n);
1181 rb.reserve(m);
1182 for (size_t i = 0; i < n; ++i)
1183 ra.append(dist(rng));
1184 for (size_t i = 0; i < m; ++i)
1185 rb.append(dist(rng));
1186
1187 const auto expected_real = naive_convolution(ra, rb);
1188 const auto obtained_real = FFTD::multiply(ra, rb);
1190
1193 ca.reserve(n);
1194 cb.reserve(m);
1195 for (size_t i = 0; i < n; ++i)
1196 ca.append(Complex(dist(rng), dist(rng)));
1197 for (size_t i = 0; i < m; ++i)
1198 cb.append(Complex(dist(rng), dist(rng)));
1199
1200 const auto expected_complex = naive_convolution(ca, cb);
1201 const auto obtained_complex = FFTD::multiply(ca, cb);
1203 }
1204}
1205
1207{
1208 EXPECT_TRUE(FFTD::multiply(Array<double>(), Array<double>({1.0, 2.0})).is_empty());
1209 EXPECT_TRUE(FFTD::multiply(Array<Complex>(), Array<Complex>({Complex(1.0, 0.0)})).is_empty());
1210}
1211
1213{
1214 using FFTF = FFT<float>;
1215 using ComplexF = FFTF::Complex;
1216
1217 Array<ComplexF> signal = {
1218 ComplexF(1.5f, -0.5f),
1219 ComplexF(-2.0f, 3.0f),
1220 ComplexF(0.25f, 4.5f),
1221 ComplexF(7.0f, -1.0f),
1222 ComplexF(-3.5f, 2.25f),
1223 ComplexF(1.0f, 0.0f),
1224 ComplexF(2.0f, -2.0f),
1225 ComplexF(-1.25f, 0.75f)
1226 };
1227
1228 Array<ComplexF> work = signal;
1229 FFTF::transform(work, false);
1230 FFTF::transform(work, true);
1231
1232 expect_typed_complex_array_near(work, signal, 2e-4f);
1233}
1234
1236{
1237 using FFTL = FFT<long double>;
1238 using ComplexL = FFTL::Complex;
1239
1240 Array<ComplexL> signal = {
1241 ComplexL(1.5L, -0.5L),
1242 ComplexL(-2.0L, 3.0L),
1243 ComplexL(0.25L, 4.5L),
1244 ComplexL(7.0L, -1.0L),
1245 ComplexL(-3.5L, 2.25L),
1246 ComplexL(1.0L, 0.0L),
1247 ComplexL(2.0L, -2.0L),
1248 ComplexL(-1.25L, 0.75L)
1249 };
1250
1251 Array<ComplexL> work = signal;
1252 FFTL::transform(work, false);
1253 FFTL::transform(work, true);
1254
1255 expect_typed_complex_array_near(work, signal, 1e-12L);
1256}
1257
1258// ---------------------------------------------------------------------------
1259// Plan tests
1260// ---------------------------------------------------------------------------
1261
1263{
1264 EXPECT_THROW(FFTD::Plan(0), std::invalid_argument);
1265 EXPECT_NO_THROW(FFTD::Plan(1));
1266 EXPECT_NO_THROW(FFTD::Plan(2));
1267 EXPECT_NO_THROW(FFTD::Plan(3));
1268 EXPECT_NO_THROW(FFTD::Plan(6));
1269 EXPECT_NO_THROW(FFTD::Plan(8));
1270 EXPECT_NO_THROW(FFTD::Plan(15));
1271 EXPECT_NO_THROW(FFTD::Plan(17));
1272 EXPECT_NO_THROW(FFTD::Plan(1024));
1273}
1274
1276{
1277 FFTD::Plan p1(1);
1278 EXPECT_EQ(p1.size(), 1u);
1279
1280 FFTD::Plan p8(8);
1281 EXPECT_EQ(p8.size(), 8u);
1282
1283 FFTD::Plan p1024(1024);
1284 EXPECT_EQ(p1024.size(), 1024u);
1285
1286 FFTD::Plan empty;
1287 EXPECT_EQ(empty.size(), 0u);
1288}
1289
1291{
1292 FFTD::Plan plan(8);
1293 Array<Complex> wrong_size = {Complex(1.0, 0.0), Complex(2.0, 0.0)};
1294 EXPECT_THROW(plan.transform(wrong_size, false), std::invalid_argument);
1295}
1296
1298{
1299 std::mt19937_64 rng(99887766);
1300 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1301
1302 for (const size_t n : {size_t(1), size_t(2), size_t(3), size_t(4),
1303 size_t(5), size_t(6), size_t(8), size_t(10),
1304 size_t(15), size_t(16), size_t(17), size_t(64),
1305 size_t(256), size_t(1024)})
1306 {
1307 FFTD::Plan plan(n);
1308 Array<Complex> signal;
1309 signal.reserve(n);
1310 for (size_t i = 0; i < n; ++i)
1311 signal.append(Complex(dist(rng), dist(rng)));
1312
1313 auto from_static = FFTD::transformed(signal, false);
1314 auto from_plan = plan.transformed(signal, false);
1316 }
1317}
1318
1320{
1321 std::mt19937_64 rng(55443322);
1322 std::uniform_real_distribution<double> dist(-10.0, 10.0);
1323
1324 for (const size_t n : {size_t(2), size_t(3), size_t(5), size_t(8),
1325 size_t(15), size_t(17), size_t(64), size_t(512)})
1326 {
1327 FFTD::Plan plan(n);
1328 Array<Complex> signal;
1329 signal.reserve(n);
1330 for (size_t i = 0; i < n; ++i)
1331 signal.append(Complex(dist(rng), dist(rng)));
1332
1333 Array<Complex> work = signal;
1334 plan.transform(work, false);
1335 plan.transform(work, true);
1336
1337 expect_complex_array_near(work, signal, 1e-8);
1338 }
1339}
1340
1342{
1343 std::mt19937_64 rng(11223344);
1344 std::uniform_real_distribution<double> dist(-3.0, 3.0);
1345
1346 FFTD::Plan plan(256);
1347
1348 for (int trial = 0; trial < 10; ++trial)
1349 {
1350 Array<Complex> signal;
1351 signal.reserve(256);
1352 for (size_t i = 0; i < 256; ++i)
1353 signal.append(Complex(dist(rng), dist(rng)));
1354
1355 Array<Complex> work = signal;
1356 plan.transform(work, false);
1357 plan.transform(work, true);
1358
1359 expect_complex_array_near(work, signal, 1e-8);
1360 }
1361}
1362
1364{
1365 Array<double> signal = {0.5, -1.25, 3.5, 2.0, -0.75, 4.25, 1.0, -2.0, 0.25};
1367 lifted.reserve(signal.size());
1368 for (size_t i = 0; i < signal.size(); ++i)
1369 lifted.append(Complex(signal[i], 0.0));
1370
1371 FFTD::Plan plan(signal.size());
1372 auto spectrum = plan.transformed(lifted, false);
1373 auto restored = plan.inverse_transform_real(spectrum);
1374
1375 expect_real_array_near(restored, signal, 1e-8);
1376}
1377
1379{
1380 ThreadPool pool(4);
1381 std::mt19937_64 rng(31415926);
1382 std::uniform_real_distribution<double> dist(-4.0, 4.0);
1383
1384 const size_t n = 256;
1385 Array<double> signal;
1386 signal.reserve(n);
1387 for (size_t i = 0; i < n; ++i)
1388 signal.append(dist(rng));
1389
1390 FFTD::Plan plan(n);
1391 const auto spectrum = FFTD::transform(signal);
1392 const auto sequential = plan.inverse_transform_real(spectrum);
1393 const auto parallel = plan.pinverse_transform_real(pool, spectrum);
1394
1395 expect_real_array_near(parallel, sequential, 1e-8);
1396 expect_real_array_near(parallel, signal, 1e-8);
1397}
1398
1400{
1401 std::mt19937_64 rng(77665544);
1402 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1403
1404 const size_t n = 64;
1405 FFTD::Plan plan(n);
1406 Array<Complex> signal;
1407 signal.reserve(n);
1408 for (size_t i = 0; i < n; ++i)
1409 signal.append(Complex(dist(rng), dist(rng)));
1410
1411 const auto expected = naive_dft(signal, false);
1412 const auto obtained = plan.transformed(signal, false);
1414}
1415
1417{
1418 ThreadPool pool(4);
1419 std::mt19937_64 rng(33221100);
1420 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1421
1422 for (const size_t n : {size_t(8), size_t(10), size_t(15),
1423 size_t(17), size_t(64), size_t(256), size_t(1024)})
1424 {
1425 FFTD::Plan plan(n);
1426 Array<Complex> signal;
1427 signal.reserve(n);
1428 for (size_t i = 0; i < n; ++i)
1429 signal.append(Complex(dist(rng), dist(rng)));
1430
1431 const auto sequential = plan.transformed(signal, false);
1432 const auto parallel = plan.ptransformed(pool, signal, false);
1433 expect_complex_array_near(sequential, parallel, 1e-9);
1434
1435 const auto seq_inv = plan.transformed(signal, true);
1436 const auto par_inv = plan.ptransformed(pool, signal, true);
1438 }
1439}
1440
1442{
1443 ThreadPool pool(4);
1444 std::mt19937_64 rng(76543210);
1445 std::uniform_real_distribution<double> dist(-3.0, 3.0);
1446
1447 const size_t n = 17;
1448 FFTD::Plan plan(n);
1450 batch.reserve(6);
1451 for (size_t item = 0; item < 6; ++item)
1452 {
1453 Array<Complex> signal;
1454 signal.reserve(n);
1455 for (size_t i = 0; i < n; ++i)
1456 signal.append(Complex(dist(rng), dist(rng)));
1457 batch.append(signal);
1458 }
1459
1460 const auto sequential_batch = plan.transformed_batch(batch, false);
1461 const auto parallel_batch = plan.ptransformed_batch(pool, batch, false);
1462 ASSERT_EQ(sequential_batch.size(), batch.size());
1463 ASSERT_EQ(parallel_batch.size(), batch.size());
1464 for (size_t i = 0; i < batch.size(); ++i)
1465 {
1466 const auto expected = plan.transformed(batch[i], false);
1469 }
1470
1471 const auto restored = plan.inverse_transform_batch(sequential_batch);
1472 const auto parallel_restored = plan.pinverse_transform_batch(pool, sequential_batch);
1473 for (size_t i = 0; i < batch.size(); ++i)
1474 {
1477 }
1478}
1479
1481{
1482 ThreadPool pool(4);
1483 FFTD::Plan plan(8);
1485 Array<Complex>({Complex(1.0, 0.0), Complex(0.0, 1.0), Complex(-1.0, 0.0),
1486 Complex(0.0, -1.0), Complex(0.5, 0.25), Complex(-0.5, 0.75),
1487 Complex(1.25, -0.5), Complex(-0.25, 0.5)}),
1488 Array<Complex>({Complex(1.0, 0.0), Complex(2.0, 0.0)})
1489 };
1490
1491 EXPECT_THROW(plan.transform_batch(batch, false), std::invalid_argument);
1492 EXPECT_THROW(plan.ptransform_batch(pool, batch, false), std::invalid_argument);
1493 EXPECT_THROW(FFTD::transformed_batch(batch, false), std::invalid_argument);
1494 EXPECT_THROW(FFTD::ptransformed_batch(pool, batch, false), std::invalid_argument);
1495}
1496
1498{
1499 ThreadPool pool(4);
1500 std::mt19937_64 rng(123456789);
1501 std::uniform_real_distribution<double> dist(-2.0, 2.0);
1502
1503 const size_t n = 15;
1504 FFTD::Plan plan(n);
1506 batch.reserve(5);
1507 for (size_t item = 0; item < 5; ++item)
1508 {
1509 Array<double> signal;
1510 signal.reserve(n);
1511 for (size_t i = 0; i < n; ++i)
1512 signal.append(dist(rng));
1513 batch.append(signal);
1514 }
1515
1516 const auto sequential = plan.rfft_batch(batch);
1517 const auto parallel = plan.prfft_batch(pool, batch);
1518 const auto restored = plan.irfft_batch(sequential);
1519 const auto parallel_restored = plan.pirfft_batch(pool, sequential);
1520
1521 ASSERT_EQ(sequential.size(), batch.size());
1522 ASSERT_EQ(parallel.size(), batch.size());
1523 ASSERT_EQ(restored.size(), batch.size());
1524 ASSERT_EQ(parallel_restored.size(), batch.size());
1525 for (size_t i = 0; i < batch.size(); ++i)
1526 {
1527 const auto expected = plan.rfft(batch[i]);
1528 expect_complex_array_near(sequential[i], expected, 1e-8);
1529 expect_complex_array_near(parallel[i], expected, 1e-8);
1532 }
1533}
1534
1536{
1537 ThreadPool pool(4);
1538 std::mt19937_64 rng(987654321);
1539 std::uniform_real_distribution<double> dist(-2.5, 2.5);
1540
1541 const size_t n = 32;
1542 FFTD::Plan plan(n);
1544 spectra.reserve(4);
1546 expected.reserve(4);
1547
1548 for (size_t item = 0; item < 4; ++item)
1549 {
1550 Array<double> signal;
1552 signal.reserve(n);
1553 lifted.reserve(n);
1554 for (size_t i = 0; i < n; ++i)
1555 {
1556 const double sample = dist(rng);
1557 signal.append(sample);
1558 lifted.append(Complex(sample, 0.0));
1559 }
1560 expected.append(signal);
1561 spectra.append(plan.transformed(lifted, false));
1562 }
1563
1564 const auto sequential = plan.inverse_transform_real_batch(spectra);
1565 const auto parallel = plan.pinverse_transform_real_batch(pool, spectra);
1566 ASSERT_EQ(sequential.size(), expected.size());
1567 ASSERT_EQ(parallel.size(), expected.size());
1568 for (size_t i = 0; i < expected.size(); ++i)
1569 {
1570 expect_real_array_near(sequential[i], expected[i], 1e-8);
1571 expect_real_array_near(parallel[i], expected[i], 1e-8);
1572 }
1573}
1574
1576{
1577 ThreadPool pool(4);
1578 Array<double> signal = {1.0, -2.0, 0.5, 3.5, -1.0, 4.0, 0.75};
1579
1580 FFTD::Plan plan(signal.size());
1581 const auto compact = plan.rfft(signal);
1582 const auto parallel_compact = plan.prfft(pool, signal);
1584
1585 const auto restored = plan.irfft(compact);
1586 const auto parallel_restored = plan.pirfft(pool, compact);
1587 expect_real_array_near(restored, signal, 1e-8);
1589}
1590
1592{
1593 using FFTF = FFT<float>;
1594 using ComplexF = FFTF::Complex;
1595
1596 FFTF::Plan plan(8);
1597 Array<ComplexF> signal = {
1598 ComplexF(1.5f, -0.5f), ComplexF(-2.0f, 3.0f),
1599 ComplexF(0.25f, 4.5f), ComplexF(7.0f, -1.0f),
1600 ComplexF(-3.5f, 2.25f), ComplexF(1.0f, 0.0f),
1601 ComplexF(2.0f, -2.0f), ComplexF(-1.25f, 0.75f)
1602 };
1603
1604 Array<ComplexF> work = signal;
1605 plan.transform(work, false);
1606 plan.transform(work, true);
1607
1608 expect_typed_complex_array_near(work, signal, 2e-4f);
1609}
1610
1612{
1613 std::mt19937_64 rng(44556677);
1614 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1615
1616 for (const size_t n : {size_t(8), size_t(64), size_t(256), size_t(1024)})
1617 {
1618 FFTD::Plan plan(n);
1619 Array<Complex> signal;
1620 signal.reserve(n);
1621 for (size_t i = 0; i < n; ++i)
1622 signal.append(Complex(dist(rng), dist(rng)));
1623
1624 double energy_time = 0.0;
1625 for (size_t i = 0; i < n; ++i)
1626 energy_time += std::norm(signal[i]);
1627
1628 const auto spectrum = plan.transformed(signal, false);
1629
1630 double energy_freq = 0.0;
1631 for (size_t i = 0; i < n; ++i)
1632 energy_freq += std::norm(spectrum[i]);
1633
1634 EXPECT_NEAR(energy_time, energy_freq / static_cast<double>(n), 1e-6)
1635 << "Parseval failed for N=" << n;
1636 }
1637}
1638
1640{
1641 std::mt19937_64 rng(66778899);
1642 std::uniform_real_distribution<double> dist(-3.0, 3.0);
1643
1644 const size_t n = 128;
1645 FFTD::Plan plan(n);
1646
1647 Array<Complex> x, y;
1648 x.reserve(n);
1649 y.reserve(n);
1650 for (size_t i = 0; i < n; ++i)
1651 {
1652 x.append(Complex(dist(rng), dist(rng)));
1653 y.append(Complex(dist(rng), dist(rng)));
1654 }
1655
1656 const double alpha = 3.7;
1657 const double beta = -2.1;
1658
1660 z.reserve(n);
1661 for (size_t i = 0; i < n; ++i)
1662 z.append(alpha * x[i] + beta * y[i]);
1663
1664 const auto fft_z = plan.transformed(z, false);
1665 const auto fft_x = plan.transformed(x, false);
1666 const auto fft_y = plan.transformed(y, false);
1667
1669 expected.reserve(n);
1670 for (size_t i = 0; i < n; ++i)
1671 expected.append(alpha * fft_x[i] + beta * fft_y[i]);
1672
1674}
1675
1676// ---------------------------------------------------------------------------
1677// Large-N tests (exercise twiddle refresh, numerical stability at scale)
1678// ---------------------------------------------------------------------------
1679
1681{
1682 std::mt19937_64 rng(10203040);
1683 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1684
1685 for (const size_t n : {size_t(32), size_t(64), size_t(128), size_t(256)})
1686 {
1687 Array<Complex> signal;
1688 signal.reserve(n);
1689 for (size_t i = 0; i < n; ++i)
1690 signal.append(Complex(dist(rng), dist(rng)));
1691
1692 const auto expected = naive_dft(signal, false);
1693 const auto obtained = FFTD::transformed(signal, false);
1694
1695 for (size_t i = 0; i < n; ++i)
1696 {
1697 EXPECT_NEAR(obtained[i].real(), expected[i].real(), 1e-6)
1698 << "N=" << n << " bin=" << i;
1699 EXPECT_NEAR(obtained[i].imag(), expected[i].imag(), 1e-6)
1700 << "N=" << n << " bin=" << i;
1701 }
1702 }
1703}
1704
1706{
1707 std::mt19937_64 rng(50607080);
1708 std::uniform_real_distribution<double> dist(-10.0, 10.0);
1709
1710 for (const size_t n : {size_t(256), size_t(1024), size_t(4096), size_t(16384)})
1711 {
1712 Array<Complex> signal;
1713 signal.reserve(n);
1714 for (size_t i = 0; i < n; ++i)
1715 signal.append(Complex(dist(rng), dist(rng)));
1716
1717 Array<Complex> work = signal;
1718 FFTD::transform(work, false);
1719 FFTD::transform(work, true);
1720
1721 for (size_t i = 0; i < n; ++i)
1722 {
1723 EXPECT_NEAR(work[i].real(), signal[i].real(), 1e-7)
1724 << "N=" << n << " i=" << i;
1725 EXPECT_NEAR(work[i].imag(), signal[i].imag(), 1e-7)
1726 << "N=" << n << " i=" << i;
1727 }
1728 }
1729}
1730
1732{
1733 std::mt19937_64 rng(90807060);
1734 std::uniform_real_distribution<double> dist(-10.0, 10.0);
1735
1736 for (const size_t n : {size_t(256), size_t(1024), size_t(4096)})
1737 {
1738 Array<double> signal;
1739 signal.reserve(n);
1740 for (size_t i = 0; i < n; ++i)
1741 signal.append(dist(rng));
1742
1743 const auto spectrum = FFTD::transform(signal);
1744 const auto restored = FFTD::inverse_transform_real(spectrum);
1745
1746 ASSERT_EQ(restored.size(), n);
1747 for (size_t i = 0; i < n; ++i)
1748 EXPECT_NEAR(restored[i], signal[i], 1e-7)
1749 << "N=" << n << " i=" << i;
1750 }
1751}
1752
1754{
1755 std::mt19937_64 rng(12121212);
1756 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1757
1758 for (const size_t n : {size_t(256), size_t(1024), size_t(4096), size_t(16384)})
1759 {
1760 Array<Complex> signal;
1761 signal.reserve(n);
1762 for (size_t i = 0; i < n; ++i)
1763 signal.append(Complex(dist(rng), dist(rng)));
1764
1765 double energy_time = 0.0;
1766 for (size_t i = 0; i < n; ++i)
1767 energy_time += std::norm(signal[i]);
1768
1769 const auto spectrum = FFTD::transformed(signal, false);
1770
1771 double energy_freq = 0.0;
1772 for (size_t i = 0; i < n; ++i)
1773 energy_freq += std::norm(spectrum[i]);
1774
1775 EXPECT_NEAR(energy_time, energy_freq / static_cast<double>(n), 1e-4)
1776 << "Parseval failed for N=" << n;
1777 }
1778}
1779
1781{
1782 std::mt19937_64 rng(34343434);
1783 std::uniform_real_distribution<double> dist(-3.0, 3.0);
1784
1785 for (const size_t n : {size_t(8), size_t(64), size_t(512), size_t(4096)})
1786 {
1787 Array<Complex> signal;
1788 signal.reserve(n);
1789 Complex sum_signal(0.0, 0.0);
1790 for (size_t i = 0; i < n; ++i)
1791 {
1792 Complex val(dist(rng), dist(rng));
1793 signal.append(val);
1794 sum_signal += val;
1795 }
1796
1797 const auto spectrum = FFTD::transformed(signal, false);
1798 expect_complex_near(spectrum[0], sum_signal, 1e-6);
1799 }
1800}
1801
1803{
1804 std::mt19937_64 rng(56565656);
1805 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1806
1807 for (const size_t n : {size_t(64), size_t(256), size_t(1024)})
1808 {
1809 Array<double> signal;
1810 signal.reserve(n);
1811 for (size_t i = 0; i < n; ++i)
1812 signal.append(dist(rng));
1813
1814 const auto spectrum = FFTD::transform(signal);
1815 ASSERT_EQ(spectrum.size(), n);
1816
1817 EXPECT_NEAR(spectrum[0].imag(), 0.0, 1e-8)
1818 << "DC should be real, N=" << n;
1819 EXPECT_NEAR(spectrum[n / 2].imag(), 0.0, 1e-8)
1820 << "Nyquist should be real, N=" << n;
1821
1822 for (size_t k = 1; k < n / 2; ++k)
1823 {
1824 EXPECT_NEAR(spectrum[k].real(), spectrum[n - k].real(), 1e-8)
1825 << "Re symmetry failed at k=" << k << " N=" << n;
1826 EXPECT_NEAR(spectrum[k].imag(), -spectrum[n - k].imag(), 1e-8)
1827 << "Im symmetry failed at k=" << k << " N=" << n;
1828 }
1829 }
1830}
1831
1833{
1834 std::mt19937_64 rng(78787878);
1835 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1836
1837 for (const size_t n : {size_t(64), size_t(256), size_t(1024)})
1838 {
1839 Array<double> signal;
1840 signal.reserve(n);
1841 for (size_t i = 0; i < n; ++i)
1842 signal.append(dist(rng));
1843
1844 const auto real_spectrum = FFTD::transform(signal);
1845 const auto complex_spectrum = FFTD::transformed(lift_real_input(signal), false);
1847 }
1848}
1849
1851{
1852 ThreadPool pool(4);
1853 std::mt19937_64 rng(11111111);
1854 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1855
1856 for (const size_t n : {size_t(256), size_t(1024), size_t(4096), size_t(8192)})
1857 {
1858 Array<Complex> signal;
1859 signal.reserve(n);
1860 for (size_t i = 0; i < n; ++i)
1861 signal.append(Complex(dist(rng), dist(rng)));
1862
1863 const auto sequential = FFTD::transformed(signal, false);
1864 const auto parallel = FFTD::ptransformed(pool, signal, false);
1865
1866 for (size_t i = 0; i < n; ++i)
1867 {
1868 EXPECT_NEAR(sequential[i].real(), parallel[i].real(), 1e-9)
1869 << "N=" << n << " i=" << i;
1870 EXPECT_NEAR(sequential[i].imag(), parallel[i].imag(), 1e-9)
1871 << "N=" << n << " i=" << i;
1872 }
1873 }
1874}
1875
1877{
1878 ThreadPool pool(4);
1879 std::mt19937_64 rng(22222222);
1880 std::uniform_real_distribution<double> dist(-5.0, 5.0);
1881
1882 for (const size_t n : {size_t(256), size_t(1024), size_t(4096)})
1883 {
1884 Array<double> signal;
1885 signal.reserve(n);
1886 for (size_t i = 0; i < n; ++i)
1887 signal.append(dist(rng));
1888
1889 const auto sequential = FFTD::transform(signal);
1890 const auto parallel = FFTD::ptransform(pool, signal);
1891 expect_complex_array_near(sequential, parallel, 1e-8);
1892 }
1893}
1894
1896{
1897 ThreadPool pool(4);
1898 std::mt19937_64 rng(33333333);
1899 std::uniform_real_distribution<double> dist(-2.0, 2.0);
1900
1901 for (const size_t n : {size_t(128), size_t(512), size_t(2048)})
1902 {
1903 Array<double> a, b;
1904 a.reserve(n);
1905 b.reserve(n);
1906 for (size_t i = 0; i < n; ++i)
1907 {
1908 a.append(dist(rng));
1909 b.append(dist(rng));
1910 }
1911
1912 const auto sequential = FFTD::multiply(a, b);
1913 const auto parallel = FFTD::pmultiply(pool, a, b);
1914 expect_real_array_near(sequential, parallel, 1e-6);
1915 }
1916}
1917
1919{
1920 std::mt19937_64 rng(44444444);
1921 std::uniform_real_distribution<double> dist(-2.0, 2.0);
1922
1923 for (const size_t n : {size_t(50), size_t(100), size_t(200)})
1924 {
1925 const size_t m = n / 2 + 1;
1926 Array<double> a, b;
1927 a.reserve(n);
1928 b.reserve(m);
1929 for (size_t i = 0; i < n; ++i)
1930 a.append(dist(rng));
1931 for (size_t i = 0; i < m; ++i)
1932 b.append(dist(rng));
1933
1934 const auto expected = naive_convolution(a, b);
1935 const auto obtained = FFTD::multiply(a, b);
1937 }
1938}
1939
1940// ---------------------------------------------------------------------------
1941// Edge cases for real transform
1942// ---------------------------------------------------------------------------
1943
1945{
1946 Array<double> signal = {42.0};
1947 const auto spectrum = FFTD::transform(signal);
1948 ASSERT_EQ(spectrum.size(), 1u);
1949 expect_complex_near(spectrum[0], Complex(42.0, 0.0), 1e-12);
1950}
1951
1953{
1954 Array<double> signal = {3.0, 7.0};
1955 const auto spectrum = FFTD::transform(signal);
1956 ASSERT_EQ(spectrum.size(), 2u);
1957 expect_complex_near(spectrum[0], Complex(10.0, 0.0), 1e-12);
1958 expect_complex_near(spectrum[1], Complex(-4.0, 0.0), 1e-12);
1959
1960 const auto restored = FFTD::inverse_transform_real(spectrum);
1961 expect_real_array_near(restored, signal, 1e-12);
1962}
1963
1965{
1966 Array<double> signal = {1.0, 0.0, -1.0, 0.0};
1967 const auto spectrum = FFTD::transform(signal);
1968 ASSERT_EQ(spectrum.size(), 4u);
1969 expect_complex_near(spectrum[0], Complex(0.0, 0.0), 1e-12);
1970 expect_complex_near(spectrum[1], Complex(2.0, 0.0), 1e-12);
1971 expect_complex_near(spectrum[2], Complex(0.0, 0.0), 1e-12);
1972 expect_complex_near(spectrum[3], Complex(2.0, 0.0), 1e-12);
1973}
1974
1975// ---------------------------------------------------------------------------
1976// Float stress tests
1977// ---------------------------------------------------------------------------
1978
1980{
1981 using FFTF = FFT<float>;
1982 using ComplexF = FFTF::Complex;
1983
1984 std::mt19937_64 rng(55555555);
1985 std::uniform_real_distribution<float> dist(-5.0f, 5.0f);
1986
1987 for (const size_t n : {size_t(64), size_t(256), size_t(1024)})
1988 {
1989 Array<ComplexF> signal;
1990 signal.reserve(n);
1991 for (size_t i = 0; i < n; ++i)
1992 signal.append(ComplexF(dist(rng), dist(rng)));
1993
1994 Array<ComplexF> work = signal;
1995 FFTF::transform(work, false);
1996 FFTF::transform(work, true);
1997
1998 const float tol = 5e-3f * std::log2(static_cast<float>(n));
1999 expect_typed_complex_array_near(work, signal, tol);
2000 }
2001}
2002
2004{
2005 using FFTF = FFT<float>;
2006 using ComplexF = FFTF::Complex;
2007
2008 std::mt19937_64 rng(66666666);
2009 std::uniform_real_distribution<float> dist(-3.0f, 3.0f);
2010
2011 for (const size_t n : {size_t(64), size_t(256), size_t(1024)})
2012 {
2013 Array<ComplexF> signal;
2014 signal.reserve(n);
2015 for (size_t i = 0; i < n; ++i)
2016 signal.append(ComplexF(dist(rng), dist(rng)));
2017
2018 float energy_time = 0.0f;
2019 for (size_t i = 0; i < n; ++i)
2020 energy_time += std::norm(signal[i]);
2021
2022 auto spectrum = FFTF::transformed(signal, false);
2023 float energy_freq = 0.0f;
2024 for (size_t i = 0; i < n; ++i)
2025 energy_freq += std::norm(spectrum[i]);
2026
2027 const float tol = energy_time * 1e-3f;
2028 EXPECT_NEAR(energy_time, energy_freq / static_cast<float>(n), tol)
2029 << "Float Parseval failed for N=" << n;
2030 }
2031}
2032
2034{
2035 using FFTF = FFT<float>;
2036
2037 Array<float> a = {1.0f, 2.0f, 3.0f, -1.0f};
2038 Array<float> b = {4.0f, -1.0f, 0.5f};
2039
2040 const auto result = FFTF::multiply(a, b);
2041 ASSERT_EQ(result.size(), 6u);
2042
2043 // 1*4=4, 1*(-1)+2*4=7, 1*0.5+2*(-1)+3*4=10.5,
2044 // 2*0.5+3*(-1)+(-1)*4=-6, 3*0.5+(-1)*(-1)=2.5, (-1)*0.5=-0.5
2045 Array<float> expected = {4.0f, 7.0f, 10.5f, -6.0f, 2.5f, -0.5f};
2046 for (size_t i = 0; i < expected.size(); ++i)
2047 EXPECT_NEAR(result[i], expected[i], 1e-3f) << "i=" << i;
2048}
2049
2051{
2052 // With extreme dynamic range (1e+12 vs 1e-12), FFT butterflies add/subtract
2053 // these values, losing precision on small entries. Tolerance must be
2054 // proportional to the largest magnitude in the signal.
2055 Array<Complex> signal;
2056 signal.reserve(8);
2057 signal.append(Complex(1e+12, 0.0));
2058 signal.append(Complex(1e-12, 0.0));
2059 signal.append(Complex(-1e+12, 0.0));
2060 signal.append(Complex(1e-12, 0.0));
2061 signal.append(Complex(1e+6, 0.0));
2062 signal.append(Complex(-1e+6, 0.0));
2063 signal.append(Complex(1e-6, 0.0));
2064 signal.append(Complex(-1e-6, 0.0));
2065
2066 double max_magnitude = 0.0;
2067 for (size_t i = 0; i < signal.size(); ++i)
2068 max_magnitude = std::max(max_magnitude, std::abs(signal[i]));
2069
2070 Array<Complex> work = signal;
2071 FFTD::transform(work, false);
2072 FFTD::transform(work, true);
2073
2074 // Use Parseval as the correctness check instead of element-wise comparison:
2075 // round-trip preserves total energy even when individual small elements
2076 // are lost to floating-point cancellation.
2077 double energy_original = 0.0;
2078 double energy_restored = 0.0;
2079 for (size_t i = 0; i < signal.size(); ++i)
2080 {
2081 energy_original += std::norm(signal[i]);
2082 energy_restored += std::norm(work[i]);
2083 }
2085
2086 // Elements whose magnitude is comparable to the max should round-trip well
2087 const double tol = max_magnitude * 1e-8;
2088 for (size_t i = 0; i < signal.size(); ++i)
2089 if (std::abs(signal[i]) > max_magnitude * 1e-6)
2090 {
2091 EXPECT_NEAR(work[i].real(), signal[i].real(), tol) << "i=" << i;
2092 EXPECT_NEAR(work[i].imag(), signal[i].imag(), tol) << "i=" << i;
2093 }
2094}
2095
2097{
2098 std::mt19937_64 rng(77777777);
2099
2100 const size_t n = 1024;
2101 Array<Complex> signal;
2102 signal.reserve(n);
2103 for (size_t i = 0; i < n; ++i)
2104 {
2105 const double scale = (i % 3 == 0) ? 1e+10 : ((i % 3 == 1) ? 1e-10 : 1.0);
2106 std::uniform_real_distribution<double> dist(-scale, scale);
2107 signal.append(Complex(dist(rng), dist(rng)));
2108 }
2109
2110 double energy_time = 0.0;
2111 for (size_t i = 0; i < n; ++i)
2112 energy_time += std::norm(signal[i]);
2113
2114 const auto spectrum = FFTD::transformed(signal, false);
2115
2116 double energy_freq = 0.0;
2117 for (size_t i = 0; i < n; ++i)
2118 energy_freq += std::norm(spectrum[i]);
2119
2120 const double rel_error = std::abs(energy_time - energy_freq / static_cast<double>(n))
2121 / energy_time;
2122 EXPECT_LT(rel_error, 1e-6) << "Parseval with high dynamic range, N=" << n;
2123}
2124
2125// ---------------------------------------------------------------------------
2126// Convolution algebraic properties
2127// ---------------------------------------------------------------------------
2128
2130{
2131 std::mt19937_64 rng(88888888);
2132 std::uniform_real_distribution<double> dist(-3.0, 3.0);
2133
2134 for (int trial = 0; trial < 5; ++trial)
2135 {
2136 const size_t na = static_cast<size_t>(trial + 3);
2137 const size_t nb = static_cast<size_t>(trial + 2);
2138
2139 Array<double> a, b;
2140 a.reserve(na);
2141 b.reserve(nb);
2142 for (size_t i = 0; i < na; ++i)
2143 a.append(dist(rng));
2144 for (size_t i = 0; i < nb; ++i)
2145 b.append(dist(rng));
2146
2147 const auto ab = FFTD::multiply(a, b);
2148 const auto ba = FFTD::multiply(b, a);
2149 expect_real_array_near(ab, ba, 1e-9);
2150 }
2151}
2152
2154{
2155 Array<double> a = {1.0, 2.0, 3.0, -1.0, 0.5};
2156 Array<double> identity = {1.0};
2157
2158 const auto result = FFTD::multiply(a, identity);
2159 expect_real_array_near(result, a, 1e-12);
2160}
2161
2163{
2164 Array<double> a = {1.0, 2.0, 3.0, -1.0};
2165 Array<double> delay2 = {0.0, 0.0, 1.0};
2166
2167 const auto result = FFTD::multiply(a, delay2);
2168 ASSERT_EQ(result.size(), 6u);
2169
2170 Array<double> expected = {0.0, 0.0, 1.0, 2.0, 3.0, -1.0};
2171 expect_real_array_near(result, expected, 1e-12);
2172}
2173
2175{
2176 Array<double> a = {1.0, -2.0, 3.0, 0.5};
2177 Array<double> scale = {3.5};
2178
2179 const auto result = FFTD::multiply(a, scale);
2180 ASSERT_EQ(result.size(), 4u);
2181
2182 for (size_t i = 0; i < a.size(); ++i)
2183 EXPECT_NEAR(result[i], a[i] * 3.5, 1e-10) << "i=" << i;
2184}
2185
2187{
2188 std::mt19937_64 rng(99999999);
2189 std::uniform_real_distribution<double> dist(-3.0, 3.0);
2190
2191 Array<Complex> a, b;
2192 for (size_t i = 0; i < 5; ++i)
2193 a.append(Complex(dist(rng), dist(rng)));
2194 for (size_t i = 0; i < 4; ++i)
2195 b.append(Complex(dist(rng), dist(rng)));
2196
2197 const auto ab = FFTD::multiply(a, b);
2198 const auto ba = FFTD::multiply(b, a);
2199 expect_complex_array_near(ab, ba, 1e-9);
2200}
2201
2203{
2204 Array<double> a = {5.0};
2205 Array<double> b = {3.0};
2206 const auto result = FFTD::multiply(a, b);
2207 ASSERT_EQ(result.size(), 1u);
2208 EXPECT_NEAR(result[0], 15.0, 1e-12);
2209}
2210
2212{
2213 std::mt19937_64 rng(12345678);
2214 std::uniform_real_distribution<double> dist(-3.0, 3.0);
2215
2216 const size_t na = 5;
2217 const size_t nb = 4;
2218 const size_t required = na + nb - 1;
2219 const size_t n = 8;
2220
2221 Array<Complex> a, b;
2222 for (size_t i = 0; i < na; ++i)
2223 a.append(Complex(dist(rng), dist(rng)));
2224 for (size_t i = 0; i < nb; ++i)
2225 b.append(Complex(dist(rng), dist(rng)));
2226
2227 auto conv_result = FFTD::multiply(a, b);
2228
2230 a_padded.reserve(n);
2231 b_padded.reserve(n);
2232 for (size_t i = 0; i < na; ++i)
2233 a_padded.append(a[i]);
2234 for (size_t i = na; i < n; ++i)
2235 a_padded.append(Complex(0.0, 0.0));
2236 for (size_t i = 0; i < nb; ++i)
2237 b_padded.append(b[i]);
2238 for (size_t i = nb; i < n; ++i)
2239 b_padded.append(Complex(0.0, 0.0));
2240
2241 auto fa = FFTD::transformed(a_padded, false);
2242 auto fb = FFTD::transformed(b_padded, false);
2243
2245 product.reserve(n);
2246 for (size_t i = 0; i < n; ++i)
2247 product.append(fa[i] * fb[i]);
2248
2249 auto from_theorem = FFTD::transformed(product, true);
2250
2251 for (size_t i = 0; i < required; ++i)
2253}
2254
2256{
2257 const auto hann = FFTD::hann_window(5);
2258 const auto hamming = FFTD::hamming_window(5);
2259 const auto blackman = FFTD::blackman_window(5);
2260
2261 Array<double> expected_hann = {0.0, 0.5, 1.0, 0.5, 0.0};
2262 Array<double> expected_hamming = {0.08, 0.54, 1.0, 0.54, 0.08};
2263 Array<double> expected_blackman = {0.0, 0.34, 1.0, 0.34, 0.0};
2264
2268}
2269
2271{
2272 EXPECT_TRUE(FFTD::hann_window(0).is_empty());
2273 EXPECT_TRUE(FFTD::hamming_window(0).is_empty());
2274 EXPECT_TRUE(FFTD::blackman_window(0).is_empty());
2275
2276 Array<double> singleton = {1.0};
2277 expect_real_array_near(FFTD::hann_window(1), singleton, 1e-12);
2278 expect_real_array_near(FFTD::hamming_window(1), singleton, 1e-12);
2279 expect_real_array_near(FFTD::blackman_window(1), singleton, 1e-12);
2280}
2281
2283{
2284 const auto beta = FFTD::kaiser_beta(60.0);
2285 EXPECT_NEAR(beta, 0.1102 * (60.0 - 8.7), 1e-12);
2286
2287 const auto kaiser = FFTD::kaiser_window(9, beta);
2288 ASSERT_EQ(kaiser.size(), 9u);
2289 EXPECT_NEAR(kaiser[0], kaiser[8], 1e-12);
2290 EXPECT_NEAR(kaiser[1], kaiser[7], 1e-12);
2291 EXPECT_NEAR(kaiser[2], kaiser[6], 1e-12);
2292 EXPECT_NEAR(kaiser[3], kaiser[5], 1e-12);
2293 EXPECT_NEAR(kaiser[4], 1.0, 1e-12);
2294 EXPECT_LT(kaiser[0], kaiser[4]);
2295
2296 constexpr double sample_rate = 8000.0;
2297 constexpr size_t num_points = 2049;
2298
2299 const auto fir_lp = FFTD::firwin_lowpass(65, 900.0, sample_rate, 70.0);
2300 const auto fir_hp = FFTD::firwin_highpass(65, 900.0, sample_rate, 70.0);
2301 const auto fir_bp = FFTD::firwin_bandpass(65, 700.0, 1500.0, sample_rate, 70.0);
2302 const auto fir_bs = FFTD::firwin_bandstop(65, 700.0, 1500.0, sample_rate, 70.0);
2303
2304 const auto fir_lp_response = FFTD::freqz(fir_lp, num_points, false);
2305 const auto fir_hp_response = FFTD::freqz(fir_hp, num_points, false);
2306 const auto fir_bp_response = FFTD::freqz(fir_bp, num_points, false);
2307 const auto fir_bs_response = FFTD::freqz(fir_bs, num_points, false);
2308
2311
2314
2318
2322
2323 EXPECT_THROW(FFTD::kaiser_beta(-1.0), std::invalid_argument);
2324 EXPECT_THROW(FFTD::kaiser_window(9, -1.0), std::invalid_argument);
2325 EXPECT_THROW(FFTD::firwin_lowpass(0, 900.0, sample_rate, 70.0),
2326 std::invalid_argument);
2327}
2328
2330{
2331 constexpr double sample_rate = 8000.0;
2332 constexpr size_t num_points = 4097;
2333
2334 Array<double> bands = {0.0, 900.0, 1200.0, sample_rate / 2.0};
2335 Array<double> desired = {1.0, 1.0, 0.0, 0.0};
2336 Array<double> weighted_stop = {1.0, 20.0};
2337 Array<double> sloped_desired = {1.0, 0.65, 0.0, 0.0};
2338
2339 const auto balanced = FFTD::firls(65, bands, desired, sample_rate);
2340 const auto weighted = FFTD::firls(65, bands, desired, sample_rate, weighted_stop);
2341 const auto sloped = FFTD::firls(65, bands, sloped_desired, sample_rate, weighted_stop);
2342
2343 ASSERT_EQ(weighted.size(), 65u);
2344 for (size_t i = 0; i < weighted.size(); ++i)
2345 EXPECT_NEAR(weighted[i], weighted[weighted.size() - 1 - i], 1e-12);
2346
2347 const auto balanced_response = FFTD::freqz(balanced, num_points, false);
2348 const auto weighted_response = FFTD::freqz(weighted, num_points, false);
2349 const auto sloped_response = FFTD::freqz(sloped, num_points, false);
2350
2352 2600.0,
2353 sample_rate);
2355 2600.0,
2356 sample_rate);
2361
2365 EXPECT_GT(sloped_upper, 0.45);
2367
2368 Array<double> bad_bands = {100.0, 900.0, 1200.0, sample_rate / 2.0};
2369 Array<double> bad_desired = {1.0, 1.0, 0.0};
2370 Array<double> bad_weights = {1.0};
2371
2372 EXPECT_THROW(FFTD::firls(64, bands, desired, sample_rate),
2373 std::invalid_argument);
2374 EXPECT_THROW(FFTD::firls(65, bad_bands, desired, sample_rate),
2375 std::invalid_argument);
2376 EXPECT_THROW(FFTD::firls(65, bands, bad_desired, sample_rate),
2377 std::invalid_argument);
2378 EXPECT_THROW(FFTD::firls(65, bands, desired, sample_rate, bad_weights),
2379 std::invalid_argument);
2380}
2381
2383{
2384 constexpr double sample_rate = 8000.0;
2385 constexpr size_t num_points = 4097;
2386
2387 Array<double> bands = {0.0, 900.0, 1200.0, sample_rate / 2.0};
2388 Array<double> desired = {1.0, 1.0, 0.0, 0.0};
2389 Array<double> weighted_stop = {1.0, 16.0};
2390
2391 const auto firls_reference =
2392 FFTD::firls(65, bands, desired, sample_rate, weighted_stop);
2393 const auto equiripple =
2394 FFTD::remez(65, bands, desired, sample_rate, weighted_stop);
2395
2396 ASSERT_EQ(equiripple.size(), 65u);
2397 for (size_t i = 0; i < equiripple.size(); ++i)
2398 EXPECT_NEAR(equiripple[i], equiripple[equiripple.size() - 1 - i], 1e-12);
2399
2400 const auto firls_response = FFTD::freqz(firls_reference, num_points, false);
2401 const auto equiripple_response = FFTD::freqz(equiripple, num_points, false);
2402
2404 2600.0,
2405 sample_rate);
2407 2600.0,
2408 sample_rate);
2409
2414
2415 Array<double> bad_weights = {1.0};
2416 EXPECT_THROW(FFTD::remez(64, bands, desired, sample_rate),
2417 std::invalid_argument);
2418 EXPECT_THROW(FFTD::remez(65, bands, desired, sample_rate, bad_weights),
2419 std::invalid_argument);
2420 EXPECT_THROW(FFTD::remez(65, bands, desired, sample_rate, weighted_stop, 4),
2421 std::invalid_argument);
2422}
2423
2425{
2426 Array<double> signal = {2.0, -1.0, 0.5, 4.0};
2427 Array<double> window = {0.0, 0.5, 1.0, 0.25};
2428 Array<double> expected_real = {0.0, -0.5, 0.5, 1.0};
2429
2430 const auto windowed_real = FFTD::apply_window(signal, window);
2432
2434 Complex(2.0, -2.0),
2435 Complex(-1.0, 3.0),
2436 Complex(0.5, 0.5),
2437 Complex(4.0, -1.0)
2438 };
2440 Complex(0.0, 0.0),
2441 Complex(-0.5, 1.5),
2442 Complex(0.5, 0.5),
2443 Complex(1.0, -0.25)
2444 };
2445
2446 const auto windowed_complex = FFTD::apply_window(complex_signal, window);
2448
2449 std::vector<double> signal_vec(signal.begin(), signal.end());
2450 std::vector<double> window_vec(window.begin(), window.end());
2451 expect_real_array_near(FFTD::apply_window(signal_vec, window_vec), expected_real, 1e-12);
2452
2453 expect_real_array_near(FFTD::apply_hann_window(signal),
2454 FFTD::apply_window(signal, FFTD::hann_window(signal.size())),
2455 1e-12);
2456 expect_complex_array_near(FFTD::apply_hamming_window(complex_signal),
2457 FFTD::apply_window(complex_signal,
2458 FFTD::hamming_window(complex_signal.size())),
2459 1e-12);
2460}
2461
2463{
2464 Array<double> signal = {1.0, 2.0, 3.0};
2465 Array<double> short_window = {0.5, 1.0};
2467 Complex(1.0, 0.0),
2468 Complex(2.0, 1.0),
2469 Complex(3.0, -1.0)
2470 };
2471
2472 EXPECT_THROW(FFTD::apply_window(signal, short_window), std::invalid_argument);
2473 EXPECT_THROW(FFTD::apply_window(complex_signal, short_window), std::invalid_argument);
2474}
2475
2477{
2478 Array<double> signal = {1.0, -0.5, 2.0, 0.25, -1.0, 3.0, 0.5, -2.0};
2479 const auto window = FFTD::hann_window(signal.size());
2480
2481 const auto manual_real = FFTD::spectrum(FFTD::apply_window(signal, window));
2482 const auto helper_real = FFTD::windowed_spectrum(signal, window);
2484
2485 Array<Complex> complex_signal = lift_real_input(signal);
2486 const auto manual_complex = FFTD::spectrum(FFTD::apply_window(complex_signal, window));
2487 const auto helper_complex = FFTD::windowed_spectrum(complex_signal, window);
2489
2490 std::vector<double> signal_vec(signal.begin(), signal.end());
2491 std::vector<double> window_vec(window.begin(), window.end());
2492 expect_complex_array_near(FFTD::windowed_spectrum(signal_vec, window_vec),
2493 manual_real, 1e-12);
2494}
2495
2497{
2498 Array<double> rectangular = {1.0, 1.0, 1.0, 1.0};
2499 EXPECT_NEAR(FFTD::window_energy(rectangular), 4.0, 1e-12);
2500 EXPECT_NEAR(FFTD::window_coherent_gain(rectangular), 1.0, 1e-12);
2501 EXPECT_NEAR(FFTD::window_enbw(rectangular), 1.0, 1e-12);
2502
2503 const auto padded_offsets = FFTD::frame_offsets(5, 4, 2, true);
2504 ASSERT_EQ(padded_offsets.size(), 3u);
2505 EXPECT_EQ(padded_offsets[0], 0u);
2506 EXPECT_EQ(padded_offsets[1], 2u);
2507 EXPECT_EQ(padded_offsets[2], 4u);
2508
2509 const auto exact_offsets = FFTD::frame_offsets(5, 4, 2, false);
2510 ASSERT_EQ(exact_offsets.size(), 1u);
2511 EXPECT_EQ(exact_offsets[0], 0u);
2512
2513 Array<double> signal = {1.0, 2.0, 3.0, 4.0, 5.0};
2514 const auto frames = FFTD::frame_signal(signal, 4, 2, true);
2515 const auto overlap_added = FFTD::overlap_add_frames(frames, 2);
2517 Array<double>({1.0, 2.0, 6.0, 8.0, 10.0, 0.0, 0.0, 0.0}),
2518 1e-12);
2519
2520 EXPECT_THROW(FFTD::window_energy(Array<double>()), std::invalid_argument);
2521 EXPECT_THROW(FFTD::window_enbw(Array<double>({1.0, -1.0})), std::domain_error);
2522 EXPECT_THROW(FFTD::frame_offsets(5, 0, 2, true), std::invalid_argument);
2523}
2524
2526{
2527 constexpr double sample_rate = 8000.0;
2528 FFTD::WelchOptions options;
2529 options.hop_size = 32;
2530 options.fft_size = 128;
2531
2532 const auto x = sine_signal(512, sample_rate, 1000.0);
2533 const auto y = sine_signal(512, sample_rate, 1000.0, 2.0, 0.25);
2534 const auto window = FFTD::hann_window(64);
2535
2536 const auto pxx = FFTD::welch(x, window, sample_rate, options);
2537 const auto pxy = FFTD::csd(x, y, window, sample_rate, options);
2538 const auto coh = FFTD::coherence(x, y, window, sample_rate, options);
2539
2540 const size_t peak = max_index(pxx.density);
2541 EXPECT_NEAR(pxx.frequency[peak], 1000.0, 1e-12);
2542 EXPECT_GT(pxx.density[peak], 1e-3);
2543 EXPECT_GT(std::abs(pxy.density[peak]), 1e-3);
2544 EXPECT_GT(coh.magnitude_squared[peak], 0.999);
2545
2546 EXPECT_THROW(FFTD::csd(x, Array<double>({1.0, 2.0}), window, sample_rate, options),
2547 std::invalid_argument);
2548}
2549
2551{
2552 Array<double> signal = {1.0, -1.0, 2.0, 0.5};
2553 Array<double> coeffs = {0.25, 0.5, 0.25};
2554
2555 const auto expected = naive_upfirdn(signal, coeffs, 3, 2);
2556 const auto actual = FFTD::upfirdn(signal, coeffs, 3, 2);
2558
2559 Array<double> identity = {0.0, 1.0, 0.0};
2560 const auto same = FFTD::resample_poly(signal, 1, 1, identity);
2561 expect_real_array_near(same, signal, 1e-12);
2562
2564 constant.reserve(64);
2565 for (size_t i = 0; i < 64; ++i)
2566 constant.append(1.0);
2567
2568 FFTD::ResamplePolyOptions options;
2569 options.taps_per_phase = 12;
2570 const auto doubled = FFTD::resample_poly(constant, 2, 1, options);
2571 ASSERT_EQ(doubled.size(), 128u);
2572 for (size_t i = 16; i < 112; ++i)
2573 EXPECT_NEAR(doubled[i], 1.0, 5e-2);
2574
2575 EXPECT_THROW(FFTD::upfirdn(signal, coeffs, 0, 1), std::invalid_argument);
2576 EXPECT_THROW(FFTD::resample_poly(signal, 2, 1, Array<double>()),
2577 std::invalid_argument);
2578 EXPECT_THROW(FFTD::resample_poly(signal, 2, 1,
2579 FFTD::ResamplePolyOptions{0, 80.0}),
2580 std::invalid_argument);
2581}
2582
2584{
2585 Array<double> signal = {1.0, 2.0, 3.0, 4.0, 5.0};
2586
2587 const auto padded = FFTD::frame_signal(signal, 4, 2, true);
2588 ASSERT_EQ(padded.size(), 3u);
2589 expect_real_array_near(padded[0], Array<double>({1.0, 2.0, 3.0, 4.0}), 1e-12);
2590 expect_real_array_near(padded[1], Array<double>({3.0, 4.0, 5.0, 0.0}), 1e-12);
2591 expect_real_array_near(padded[2], Array<double>({5.0, 0.0, 0.0, 0.0}), 1e-12);
2592
2593 const auto exact = FFTD::frame_signal(signal, 4, 2, false);
2594 ASSERT_EQ(exact.size(), 1u);
2595 expect_real_array_near(exact[0], Array<double>({1.0, 2.0, 3.0, 4.0}), 1e-12);
2596}
2597
2599{
2600 Array<double> signal = {1.0, -0.5, 2.0, 0.25, -1.0, 3.0, 0.5};
2601 Array<double> window = {1.0, 0.5, 0.5, 1.0};
2602
2603 const auto frames = FFTD::frame_signal(signal, window.size(), 2, true);
2604 const auto spectrogram = FFTD::stft(signal, window, 2, true);
2605
2606 ASSERT_EQ(spectrogram.size(), frames.size());
2607 for (size_t i = 0; i < frames.size(); ++i)
2608 {
2609 const auto expected = FFTD::transform_padded(FFTD::apply_window(frames[i], window));
2611 }
2612
2613 const auto hann_stft = FFTD::stft(signal, 4, 2, true);
2614 ASSERT_EQ(hann_stft.size(), frames.size());
2615 ASSERT_EQ(hann_stft[0].size(), 4u);
2616}
2617
2619{
2620 Array<double> signal = {1.0, 2.0, 3.0};
2621 Array<double> window = {1.0, 0.5};
2622
2623 EXPECT_THROW(FFTD::frame_signal(signal, 0, 1, true), std::invalid_argument);
2624 EXPECT_THROW(FFTD::frame_signal(signal, 2, 0, true), std::invalid_argument);
2625 EXPECT_THROW(FFTD::stft(signal, Array<double>(), 1, true), std::invalid_argument);
2626 EXPECT_NO_THROW(FFTD::stft(signal, window, 1, true));
2627}
2628
2630{
2631 ThreadPool pool(4);
2632 Array<double> signal = {1.0, -0.5, 2.0, 0.25, -1.0, 3.0, 0.5, -2.0, 1.25};
2633 Array<double> analysis_window = {1.0, 0.75, 0.5, 1.0};
2634 Array<double> synthesis_window = {0.5, 1.0, 1.0, 0.5};
2635
2636 const auto spectrogram = FFTD::stft(signal, analysis_window, 2, true);
2637 const auto reconstructed =
2638 FFTD::istft(spectrogram, analysis_window, synthesis_window, 2, signal.size());
2639 const auto same_window =
2640 FFTD::istft(spectrogram, analysis_window, 2, signal.size());
2641 const auto parallel =
2642 FFTD::pistft(pool, spectrogram, analysis_window, synthesis_window, 2,
2643 signal.size());
2644
2645 expect_real_array_near(reconstructed, signal, 1e-10);
2646 expect_real_array_near(same_window, signal, 1e-10);
2647 expect_real_array_near(parallel, signal, 1e-10);
2648}
2649
2651{
2652 Array<double> analysis_window = {1.0, 0.5, 0.5, 1.0};
2653 Array<double> synthesis_window = {1.0, 1.0, 1.0, 1.0};
2655 Array<Complex>({Complex(1.0, 0.0), Complex(2.0, 0.0),
2656 Complex(3.0, 0.0), Complex(4.0, 0.0)}),
2657 Array<Complex>({Complex(0.5, 0.0), Complex(1.0, 0.0),
2658 Complex(1.5, 0.0), Complex(2.0, 0.0)})
2659 };
2660
2662 std::invalid_argument);
2664 std::invalid_argument);
2666 std::invalid_argument);
2668 std::invalid_argument);
2669
2671 mismatched.append(Array<Complex>({Complex(1.0, 0.0), Complex(0.0, 0.0)}));
2673 std::invalid_argument);
2674
2676 Array<Complex>({Complex(1.0, 0.0), Complex(0.0, 0.0)})
2677 };
2679 std::invalid_argument);
2680}
2681
2683{
2684 std::mt19937_64 rng(20260305);
2685 std::uniform_real_distribution<double> dist(-1.0, 1.0);
2686
2687 Array<double> signal;
2688 signal.reserve(23);
2689 for (size_t i = 0; i < 23; ++i)
2690 signal.append(dist(rng));
2691
2692 const auto window = FFTD::hann_window(8);
2693 FFTD::STFTOptions stft_options;
2694 stft_options.hop_size = 4;
2695 stft_options.centered = true;
2696 stft_options.pad_end = true;
2697 stft_options.fft_size = 16;
2698 stft_options.validate_nola = true;
2699
2700 FFTD::ISTFTOptions istft_options;
2701 istft_options.hop_size = 4;
2702 istft_options.centered = true;
2703 istft_options.signal_length = signal.size();
2704 istft_options.validate_nola = true;
2705
2706 const auto spectrogram = FFTD::stft(signal, window, stft_options);
2707 ASSERT_FALSE(spectrogram.is_empty());
2708 EXPECT_EQ(spectrogram[0].size(), 16u);
2709
2710 const auto reconstructed = FFTD::istft(spectrogram, window, istft_options);
2712
2713 Array<double> rectangular = {1.0, 1.0, 1.0, 1.0};
2714 const auto profile = FFTD::window_overlap_profile(rectangular, rectangular, 2);
2715 expect_real_array_near(profile, Array<double>({2.0, 2.0}), 1e-12);
2716 EXPECT_TRUE(FFTD::satisfies_nola(rectangular, 2));
2717 EXPECT_TRUE(FFTD::satisfies_cola(rectangular, 2));
2718
2719 Array<double> bad_window = {1.0, 0.0, 1.0, 0.0};
2720 EXPECT_FALSE(FFTD::satisfies_nola(bad_window, 2));
2721
2722 FFTD::STFTOptions bad_options;
2723 bad_options.hop_size = 2;
2724 bad_options.validate_nola = true;
2725 EXPECT_THROW(FFTD::stft(signal, bad_window, bad_options), std::domain_error);
2726}
2727
2729{
2730 Array<double> signal = {
2731 0.0, 1.0, 0.5, -0.25, -1.0, -0.5, 0.75, 1.25, 0.0, -0.75, 0.5
2732 };
2733 Array<double> window = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0};
2734
2735 FFTD::STFTOptions stft_options;
2736 stft_options.hop_size = 3;
2737 stft_options.fft_size = 10;
2738 stft_options.pad_end = true;
2739 stft_options.validate_nola = true;
2740
2741 FFTD::ISTFTOptions istft_options;
2742 istft_options.hop_size = 3;
2743 istft_options.signal_length = signal.size();
2744 istft_options.validate_nola = true;
2745
2746 const auto spectrogram = FFTD::stft(signal, window, stft_options);
2747 ASSERT_FALSE(spectrogram.is_empty());
2748 EXPECT_EQ(spectrogram[0].size(), 10u);
2749
2750 const auto reconstructed = FFTD::istft(spectrogram, window, istft_options);
2752}
2753
2755{
2756 ThreadPool pool(4);
2757 std::mt19937_64 rng(20260306);
2758 std::uniform_real_distribution<double> dist(-1.0, 1.0);
2759
2760 Array<double> signal;
2761 signal.reserve(29);
2762 for (size_t i = 0; i < 29; ++i)
2763 signal.append(dist(rng));
2764
2765 const auto window = FFTD::hann_window(8);
2766 FFTD::STFTOptions options;
2767 options.hop_size = 4;
2768 options.centered = true;
2769 options.pad_end = true;
2770 options.fft_size = 16;
2771 options.validate_nola = true;
2772
2773 const auto offline = FFTD::stft(signal, window, options);
2774 const auto parallel = FFTD::pstft(pool, signal, window, options);
2775 expect_spectrogram_near(parallel, offline, 1e-12);
2776
2777 FFTD::STFTProcessor processor(window, options);
2779 for (size_t offset = 0; offset < signal.size();)
2780 {
2781 const size_t length = std::min(size_t(5 + (offset % 4)),
2782 signal.size() - offset);
2783 Array<double> chunk;
2784 chunk.reserve(length);
2785 for (size_t i = 0; i < length; ++i)
2786 chunk.append(signal[offset + i]);
2787
2788 append_spectrogram(streamed, processor.process_block(chunk));
2789 offset += length;
2790 }
2793 EXPECT_THROW(processor.process_block(Array<double>({1.0})), std::runtime_error);
2794
2795 processor.reset();
2796 streamed = {};
2797 append_spectrogram(streamed, processor.process_block(signal));
2800
2801 FFTD::STFTProcessor parallel_processor(window, options);
2803 for (size_t offset = 0; offset < signal.size();)
2804 {
2805 const size_t length = std::min(size_t(3 + (offset % 5)),
2806 signal.size() - offset);
2807 Array<double> chunk;
2808 chunk.reserve(length);
2809 for (size_t i = 0; i < length; ++i)
2810 chunk.append(signal[offset + i]);
2811
2813 parallel_processor.pprocess_block(pool, chunk));
2814 offset += length;
2815 }
2818
2819 FFTD::STFTProcessor empty_processor(window, options);
2820 EXPECT_TRUE(empty_processor.flush().is_empty());
2821}
2822
2824{
2825 ThreadPool pool(4);
2827 Array<double>({1.0, -0.5, 0.25, 2.0, -1.0, 0.5, 1.5, -0.75, 0.25}),
2828 Array<double>({-1.0, 0.75, 0.5, -0.25, 1.25, -1.5, 0.0, 0.5, 1.0, -0.5,
2829 0.25}),
2830 Array<double>({0.25, -0.125, 0.75, 1.5, -0.5, 0.0, 0.5})
2831 };
2832
2833 const auto window = FFTD::hann_window(8);
2834 FFTD::STFTOptions stft_options;
2835 stft_options.hop_size = 4;
2836 stft_options.centered = true;
2837 stft_options.pad_end = true;
2838 stft_options.fft_size = 16;
2839 stft_options.validate_nola = true;
2840
2841 const auto batch = FFTD::batched_stft(signals, window, stft_options);
2842 const auto parallel_batch = FFTD::pbatched_stft(pool, signals, window, stft_options);
2843 ASSERT_EQ(batch.size(), signals.size());
2844 ASSERT_EQ(parallel_batch.size(), signals.size());
2845 for (size_t i = 0; i < signals.size(); ++i)
2846 {
2847 const auto expected = FFTD::stft(signals[i], window, stft_options);
2850 }
2851
2852 FFTD::ISTFTOptions istft_options;
2853 istft_options.hop_size = 4;
2854 istft_options.centered = true;
2855 istft_options.validate_nola = true;
2856
2858 lengths.reserve(signals.size());
2859 for (size_t i = 0; i < signals.size(); ++i)
2860 lengths.append(signals[i].size());
2861
2862 const auto reconstructed =
2863 FFTD::batched_istft(batch, window, istft_options, lengths);
2864 const auto parallel_reconstructed =
2865 FFTD::pbatched_istft(pool, batch, window, istft_options, lengths);
2866
2867 ASSERT_EQ(reconstructed.size(), signals.size());
2869 for (size_t i = 0; i < signals.size(); ++i)
2870 {
2873 }
2874
2875 EXPECT_THROW(FFTD::batched_istft(batch,
2876 window,
2878 Array<size_t>({signals[0].size()})),
2879 std::invalid_argument);
2880}
2881
2883{
2884 ThreadPool pool(4);
2885 std::mt19937_64 rng(20260307);
2886 std::uniform_real_distribution<double> dist(-1.0, 1.0);
2887
2888 Array<double> signal;
2889 signal.reserve(27);
2890 for (size_t i = 0; i < 27; ++i)
2891 signal.append(dist(rng));
2892
2893 const auto window = FFTD::hann_window(8);
2894 FFTD::STFTOptions stft_options;
2895 stft_options.hop_size = 4;
2896 stft_options.centered = true;
2897 stft_options.pad_end = true;
2898 stft_options.fft_size = 16;
2899 stft_options.validate_nola = true;
2900
2901 FFTD::ISTFTOptions istft_options;
2902 istft_options.hop_size = 4;
2903 istft_options.centered = true;
2904 istft_options.validate_nola = true;
2905
2906 const auto spectrogram = FFTD::stft(signal, window, stft_options);
2907 const auto expected = FFTD::istft(spectrogram, window, istft_options);
2908
2909 FFTD::ISTFTProcessor processor(spectrogram[0].size(), window, istft_options);
2911 for (size_t offset = 0; offset < spectrogram.size();)
2912 {
2913 const size_t length = std::min(size_t(2 + (offset % 3)),
2914 spectrogram.size() - offset);
2915 Array<Array<Complex>> chunk;
2916 for (size_t i = 0; i < length; ++i)
2917 chunk.append(spectrogram[offset + i]);
2918
2919 const auto partial = processor.process_block(chunk);
2920 for (size_t i = 0; i < partial.size(); ++i)
2921 streamed.append(partial[i]);
2922 offset += length;
2923 }
2924
2925 const auto tail = processor.flush();
2926 for (size_t i = 0; i < tail.size(); ++i)
2927 streamed.append(tail[i]);
2928
2930 EXPECT_THROW(processor.process_frame(spectrogram[0]), std::runtime_error);
2931
2932 processor.reset();
2933 streamed = processor.process_block(spectrogram);
2934 const auto full_tail = processor.flush();
2935 for (size_t i = 0; i < full_tail.size(); ++i)
2936 streamed.append(full_tail[i]);
2938
2939 FFTD::ISTFTProcessor parallel_processor(spectrogram[0].size(), window, istft_options);
2941 for (size_t offset = 0; offset < spectrogram.size();)
2942 {
2943 const size_t length = std::min(size_t(1 + (offset % 4)),
2944 spectrogram.size() - offset);
2945 Array<Array<Complex>> chunk;
2946 for (size_t i = 0; i < length; ++i)
2947 chunk.append(spectrogram[offset + i]);
2948
2949 const auto partial = parallel_processor.pprocess_block(pool, chunk);
2950 for (size_t i = 0; i < partial.size(); ++i)
2951 parallel_streamed.append(partial[i]);
2952 offset += length;
2953 }
2954
2955 const auto parallel_tail = parallel_processor.pflush(pool);
2956 for (size_t i = 0; i < parallel_tail.size(); ++i)
2959
2960 FFTD::ISTFTProcessor empty_processor(spectrogram[0].size(), window, istft_options);
2961 EXPECT_TRUE(empty_processor.flush().is_empty());
2962}
2963
2965{
2966 ThreadPool pool(4);
2968 Array<double>({1.0, -0.5, 0.25, 2.0, -1.0, 0.5, 1.5, -0.75, 0.25, 0.0}),
2969 Array<double>({-1.0, 0.75, 0.5, -0.25, 1.25, -1.5, 0.0, 0.5, 1.0}),
2970 Array<double>({0.25, -0.125, 0.75, 1.5, -0.5, 0.0, 0.5, -0.75})
2971 };
2972 const Array<size_t> chunk_pattern = {3, 2, 4, 1};
2973 const Array<size_t> frame_pattern = {1, 2, 1, 3};
2974
2975 const auto window = FFTD::hann_window(8);
2976 FFTD::STFTOptions stft_options;
2977 stft_options.hop_size = 4;
2978 stft_options.centered = true;
2979 stft_options.pad_end = true;
2980 stft_options.fft_size = 16;
2981 stft_options.validate_nola = true;
2982
2983 FFTD::ISTFTOptions istft_options;
2984 istft_options.hop_size = 4;
2985 istft_options.centered = true;
2986 istft_options.validate_nola = true;
2987
2989 lengths.reserve(signals.size());
2990 for (size_t i = 0; i < signals.size(); ++i)
2991 lengths.append(signals[i].size());
2992
2993 const auto offline = FFTD::batched_stft(signals, window, stft_options);
2994 ASSERT_EQ(offline.size(), signals.size());
2995
2996 FFTD::BatchedSTFTProcessor analyzer(signals.size(), window, stft_options);
2997 FFTD::BatchedSTFTProcessor parallel_analyzer(signals.size(), window, stft_options);
3000 Array<size_t> offsets = {0, 0, 0};
3001
3002 for (size_t step = 0;; ++step)
3003 {
3004 bool any_pending = false;
3005 Array<Array<double>> block;
3006 for (size_t channel = 0; channel < signals.size(); ++channel)
3007 {
3008 const size_t remaining = signals[channel].size() - offsets[channel];
3009 const size_t length = std::min(chunk_pattern[step % chunk_pattern.size()],
3010 remaining);
3011 Array<double> chunk;
3012 chunk.reserve(length);
3013 for (size_t i = 0; i < length; ++i)
3014 chunk.append(signals[channel][offsets[channel] + i]);
3015 offsets(channel) += length;
3016 any_pending = any_pending or length != 0;
3017 block.append(chunk);
3018 }
3019
3020 if (not any_pending)
3021 break;
3022
3023 const auto emitted = analyzer.process_block(block);
3024 const auto parallel_emitted = parallel_analyzer.pprocess_block(pool, block);
3025 for (size_t channel = 0; channel < signals.size(); ++channel)
3026 {
3027 append_spectrogram(streamed(channel), emitted[channel]);
3029 }
3030 }
3031
3032 const auto tail = analyzer.flush();
3033 const auto parallel_tail = parallel_analyzer.pflush(pool);
3034 for (size_t channel = 0; channel < signals.size(); ++channel)
3035 {
3036 append_spectrogram(streamed(channel), tail[channel]);
3038 expect_spectrogram_near(streamed[channel], offline[channel], 1e-12);
3039 expect_spectrogram_near(parallel_streamed[channel], offline[channel], 1e-12);
3040 }
3041
3042 FFTD::BatchedISTFTProcessor inverse(signals.size(),
3043 offline[0][0].size(),
3044 window,
3046 lengths);
3047 FFTD::BatchedISTFTProcessor parallel_inverse(signals.size(),
3048 offline[0][0].size(),
3049 window,
3051 lengths);
3054 Array<size_t> frame_offsets = {0, 0, 0};
3055
3056 for (size_t step = 0;; ++step)
3057 {
3058 bool any_pending = false;
3060 for (size_t channel = 0; channel < offline.size(); ++channel)
3061 {
3062 const size_t remaining = offline[channel].size() - frame_offsets[channel];
3063 const size_t length = std::min(frame_pattern[step % frame_pattern.size()],
3064 remaining);
3065 Array<Array<Complex>> chunk;
3066 for (size_t i = 0; i < length; ++i)
3067 chunk.append(offline[channel][frame_offsets[channel] + i]);
3068 frame_offsets(channel) += length;
3069 any_pending = any_pending or length != 0;
3070 block.append(chunk);
3071 }
3072
3073 if (not any_pending)
3074 break;
3075
3076 const auto emitted = inverse.process_block(block);
3077 const auto parallel_emitted = parallel_inverse.pprocess_block(pool, block);
3078 for (size_t channel = 0; channel < signals.size(); ++channel)
3079 {
3080 append_real_samples(reconstructed(channel), emitted[channel]);
3082 }
3083 }
3084
3085 const auto reconstructed_tail = inverse.flush();
3086 const auto parallel_reconstructed_tail = parallel_inverse.pflush(pool);
3087 for (size_t channel = 0; channel < signals.size(); ++channel)
3088 {
3092 expect_real_array_near(reconstructed[channel], signals[channel], 1e-10);
3093 expect_real_array_near(parallel_reconstructed[channel], signals[channel], 1e-10);
3094 }
3095}
3096
3098{
3099 FFTD::STFTProcessor analyzer;
3100 FFTD::ISTFTProcessor inverse;
3101
3102 EXPECT_FALSE(analyzer.configured());
3103 EXPECT_FALSE(inverse.configured());
3104 EXPECT_THROW(analyzer.reset(), std::runtime_error);
3105 EXPECT_THROW(analyzer.process_block(Array<double>({1.0, 2.0})), std::runtime_error);
3106 EXPECT_THROW(analyzer.flush(), std::runtime_error);
3107 EXPECT_THROW(inverse.reset(), std::runtime_error);
3108 EXPECT_THROW(inverse.process_frame(Array<Complex>({Complex(1.0, 0.0)})),
3109 std::runtime_error);
3110 EXPECT_THROW(inverse.flush(), std::runtime_error);
3111}
3112
3114{
3116 Array<Complex>({Complex(1.0, 0.5), Complex(-2.0, 1.0),
3117 Complex(0.25, -0.75), Complex(3.0, 0.0)}),
3118 Array<Complex>({Complex(-1.5, 0.25), Complex(2.5, -1.0),
3119 Complex(0.5, 0.5), Complex(-0.75, 1.25)})
3120 };
3121
3123 for (size_t row = 0; row < matrix.size(); ++row)
3124 for (size_t col = 0; col < matrix[row].size(); ++col)
3125 flat.append(matrix[row][col]);
3126
3127 FFTD::TensorLayout row_major = FFTD::row_major_layout(Array<size_t>({2, 4}));
3128 auto transformed = flat;
3129 FFTD::transform_axis(transformed, row_major, 1, false);
3130
3131 for (size_t row = 0; row < matrix.size(); ++row)
3132 {
3133 const auto expected = FFTD::transformed(matrix[row]);
3134 for (size_t col = 0; col < expected.size(); ++col)
3135 expect_complex_near(transformed[row * 4 + col], expected[col], 1e-12);
3136 }
3137
3138 Array<Complex> padded(10, Complex(-99.0, 0.0));
3139 for (size_t col = 0; col < 4; ++col)
3140 {
3141 padded(col) = matrix[0][col];
3142 padded(5 + col) = matrix[1][col];
3143 }
3144
3145 FFTD::TensorLayout padded_layout;
3146 padded_layout.shape = {2, 4};
3147 padded_layout.strides = {5, 1};
3148 FFTD::transform_axis(padded, padded_layout, 1, false);
3149
3150 EXPECT_EQ(padded[4], Complex(-99.0, 0.0));
3151 EXPECT_EQ(padded[9], Complex(-99.0, 0.0));
3152 for (size_t row = 0; row < matrix.size(); ++row)
3153 {
3154 const auto expected = FFTD::transformed(matrix[row]);
3155 for (size_t col = 0; col < expected.size(); ++col)
3156 expect_complex_near(padded[row * 5 + col], expected[col], 1e-12);
3157 }
3158}
3159
3161{
3162 ThreadPool pool(4);
3164 Array<Complex>({Complex(1.0, 0.0), Complex(2.0, -0.5),
3165 Complex(-1.0, 0.25), Complex(0.5, 1.0)}),
3166 Array<Complex>({Complex(-0.75, 1.5), Complex(1.25, -1.0),
3167 Complex(0.0, 0.5), Complex(-2.0, 0.0)}),
3168 Array<Complex>({Complex(0.5, -0.25), Complex(-1.5, 0.75),
3169 Complex(2.5, 0.0), Complex(1.0, -1.5)})
3170 };
3171
3173 for (size_t row = 0; row < expected.size(); ++row)
3174 expected(row) = FFTD::transformed(expected[row]);
3175
3176 for (size_t col = 0; col < expected[0].size(); ++col)
3177 {
3178 Array<Complex> column;
3179 for (size_t row = 0; row < expected.size(); ++row)
3180 column.append(expected[row][col]);
3181 column = FFTD::transformed(column);
3182 for (size_t row = 0; row < expected.size(); ++row)
3183 expected(row)(col) = column[row];
3184 }
3185
3186 expect_matrix_near(FFTD::transformed2d(matrix), expected, 1e-12);
3187 expect_matrix_near(FFTD::ptransformed2d(pool, matrix), expected, 1e-12);
3188 expect_matrix_near(FFTD::inverse_transform2d(expected), matrix, 1e-10);
3189}
3190
3192{
3193 ThreadPool pool(4);
3195 {
3196 Array<Complex>({Complex(1.0, 0.0), Complex(0.5, -0.25), Complex(-1.0, 0.5)}),
3197 Array<Complex>({Complex(2.0, 1.0), Complex(-0.75, 0.0), Complex(1.5, -0.5)})
3198 },
3199 {
3200 Array<Complex>({Complex(-0.5, 1.25), Complex(1.0, 0.0), Complex(0.25, -0.75)}),
3201 Array<Complex>({Complex(0.0, 0.5), Complex(-1.5, 0.25), Complex(2.0, 0.0)})
3202 }
3203 };
3204
3205 const auto spectrum3d = FFTD::transformed3d(tensor);
3206 const auto parallel3d = FFTD::ptransformed3d(pool, tensor);
3208 expect_tensor3_near(FFTD::inverse_transform3d(spectrum3d), tensor, 1e-10);
3209
3211 for (size_t i = 0; i < tensor.size(); ++i)
3212 expected2d(i) = FFTD::transformed2d(tensor[i]);
3213 expect_tensor3_near(FFTD::transformed2d_batch(tensor), expected2d, 1e-12);
3214 expect_tensor3_near(FFTD::ptransformed2d_batch(pool, tensor), expected2d, 1e-12);
3215
3217 for (size_t i = 0; i < tensor.size(); ++i)
3218 for (size_t j = 0; j < tensor[i].size(); ++j)
3219 for (size_t k = 0; k < tensor[i][j].size(); ++k)
3220 flat.append(tensor[i][j][k]);
3221
3222 const auto axes_out =
3223 FFTD::transformed_axes(flat,
3224 FFTD::row_major_layout(Array<size_t>({2, 2, 3})),
3225 Array<size_t>({1, 2}));
3226
3227 size_t index = 0;
3228 for (size_t i = 0; i < expected2d.size(); ++i)
3229 for (size_t j = 0; j < expected2d[i].size(); ++j)
3230 for (size_t k = 0; k < expected2d[i][j].size(); ++k, ++index)
3231 expect_complex_near(axes_out[index], expected2d[i][j][k], 1e-12);
3232}
3233
3235{
3236 ThreadPool pool(4);
3238 Array<double>({1.0, -0.5, 0.25, 2.0, -1.0, 0.5, 1.5, -0.75, 0.25}),
3239 Array<double>({-1.0, 0.75, 0.5, -0.25, 1.25, -1.5, 0.0, 0.5, 1.0})
3240 };
3241 const auto window = FFTD::hann_window(8);
3242
3243 FFTD::STFTOptions stft_options;
3244 stft_options.hop_size = 4;
3245 stft_options.centered = true;
3246 stft_options.pad_end = true;
3247 stft_options.fft_size = 16;
3248 stft_options.validate_nola = true;
3249
3250 FFTD::ISTFTOptions istft_options;
3251 istft_options.hop_size = 4;
3252 istft_options.centered = true;
3253 istft_options.validate_nola = true;
3254
3255 Array<size_t> lengths = {signals[0].size(), signals[1].size()};
3256
3257 const auto channel_major =
3258 FFTD::multichannel_stft(signals,
3259 window,
3261 FFTD::SpectrogramLayout::channel_frame_bin);
3262 const auto frame_major =
3263 FFTD::multichannel_stft(signals,
3264 window,
3266 FFTD::SpectrogramLayout::frame_channel_bin);
3267 const auto parallel_frame_major =
3268 FFTD::pmultichannel_stft(pool,
3269 signals,
3270 window,
3272 FFTD::SpectrogramLayout::frame_channel_bin);
3273
3274 const auto transposed =
3275 FFTD::transpose_spectrogram_layout(frame_major,
3276 FFTD::SpectrogramLayout::frame_channel_bin,
3277 FFTD::SpectrogramLayout::channel_frame_bin);
3278 const auto parallel_transposed =
3279 FFTD::transpose_spectrogram_layout(parallel_frame_major,
3280 FFTD::SpectrogramLayout::frame_channel_bin,
3281 FFTD::SpectrogramLayout::channel_frame_bin);
3284
3285 const auto reconstructed =
3286 FFTD::multichannel_istft(frame_major,
3287 window,
3288 window,
3290 lengths,
3291 FFTD::SpectrogramLayout::frame_channel_bin);
3292 const auto parallel_reconstructed =
3293 FFTD::pmultichannel_istft(pool,
3295 window,
3296 window,
3298 lengths,
3299 FFTD::SpectrogramLayout::frame_channel_bin);
3302}
3303
3305{
3306 Array<double> signal = {3.5, 3.5, 3.5, 3.5, 3.5, 3.5};
3307 Array<double> identity = {1.0};
3308 Array<double> smoother = {0.25, 0.5, 0.25};
3309
3310 expect_real_array_near(FFTD::filtfilt(signal, identity), signal, 1e-12);
3311 expect_real_array_near(FFTD::filtfilt(signal, smoother), signal, 1e-12);
3312}
3313
3315{
3316 std::mt19937_64 rng(56473829);
3317 std::uniform_real_distribution<double> dist(-2.0, 2.0);
3318
3319 Array<double> signal;
3320 signal.reserve(41);
3321 for (size_t i = 0; i < 41; ++i)
3322 signal.append(dist(rng));
3323
3324 Array<double> coeffs = {0.1, 0.2, 0.4, 0.2, 0.1};
3325
3326 const auto expected = naive_filtfilt(signal, coeffs);
3327 const auto obtained = FFTD::filtfilt(signal, coeffs, 16);
3329}
3330
3332{
3333 ThreadPool pool(4);
3334 Array<double> signal = {1.0, -1.0, 2.0, -2.0, 0.5, 3.0, -0.5, 1.5};
3335 Array<double> coeffs = {0.2, 0.6, 0.2};
3336
3337 const auto sequential = FFTD::filtfilt(signal, coeffs, 8);
3338 const auto parallel = FFTD::pfiltfilt(pool, signal, coeffs, 8);
3339 expect_real_array_near(parallel, sequential, 1e-8);
3340
3341 std::vector<double> signal_vec(signal.begin(), signal.end());
3342 std::vector<double> coeffs_vec(coeffs.begin(), coeffs.end());
3343 expect_real_array_near(FFTD::filtfilt(signal_vec, coeffs_vec, 8), sequential, 1e-8);
3344}
3345
3347{
3348 Array<double> empty;
3349 Array<double> coeffs = {0.25, 0.5, 0.25};
3350 Array<double> signal = {1.0, 2.0, 3.0};
3351
3352 EXPECT_TRUE(FFTD::filtfilt(empty, coeffs).is_empty());
3353 EXPECT_TRUE(FFTD::filtfilt(signal, empty).is_empty());
3354}
3355
3357{
3358 Array<double> signal = {2.5, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5};
3359 Array<double> numerator = {0.075, 0.15, 0.075};
3360 Array<double> denominator = {1.0, -0.9, 0.2};
3361
3362 expect_real_array_near(FFTD::filtfilt(signal,
3363 Array<double>({1.0}),
3364 Array<double>({1.0})),
3365 signal, 1e-12);
3366 expect_real_array_near(FFTD::filtfilt(signal, numerator, denominator),
3367 signal, 1e-10);
3368}
3369
3371{
3372 std::mt19937_64 rng(11235813);
3373 std::uniform_real_distribution<double> dist(-2.0, 2.0);
3374
3375 Array<double> signal;
3376 signal.reserve(57);
3377 for (size_t i = 0; i < 57; ++i)
3378 signal.append(dist(rng));
3379
3380 Array<double> numerator = {0.075, 0.15, 0.075};
3381 Array<double> denominator = {1.0, -0.9, 0.2};
3382 FFTD::BiquadSection section{0.075, 0.15, 0.075, 1.0, -0.9, 0.2};
3383
3384 const auto expected = naive_iir_filtfilt(signal, numerator, denominator);
3385 const auto obtained = FFTD::filtfilt(signal, numerator, denominator);
3386 const auto wrapped = FFTD::filtfilt(signal, section);
3387
3390
3391 std::vector<double> signal_vec(signal.begin(), signal.end());
3392 std::vector<double> numerator_vec(numerator.begin(), numerator.end());
3393 std::vector<double> denominator_vec(denominator.begin(), denominator.end());
3395 expected, 1e-10);
3396}
3397
3399{
3400 std::mt19937_64 rng(42424242);
3401 std::uniform_real_distribution<double> dist(-1.5, 1.5);
3402
3403 Array<double> signal;
3404 signal.reserve(64);
3405 for (size_t i = 0; i < 64; ++i)
3406 signal.append(dist(rng));
3407
3409 FFTD::BiquadSection{0.075, 0.15, 0.075, 1.0, -0.9, 0.2},
3410 FFTD::BiquadSection{0.14, 0.28, 0.14, 1.0, -0.5, 0.06}
3411 };
3412
3413 const auto expected = naive_sos_filtfilt(signal, sections);
3414 const auto obtained = FFTD::filtfilt(signal, sections);
3416
3417 std::vector<FFTD::BiquadSection> sections_vec(sections.begin(), sections.end());
3418 expect_real_array_near(FFTD::filtfilt(signal, sections_vec), expected, 1e-10);
3419}
3420
3422{
3423 std::mt19937_64 rng(17171717);
3424 std::uniform_real_distribution<double> dist(-1.0, 1.0);
3425
3426 Array<double> signal;
3427 signal.reserve(48);
3428 for (size_t i = 0; i < 48; ++i)
3429 signal.append(dist(rng));
3430
3431 Array<double> numerator = {0.075, 0.15, 0.075};
3432 Array<double> denominator = {1.0, -0.9, 0.2};
3433
3434 const auto expected = FFTD::lfilter(signal, numerator, denominator);
3435
3436 FFTD::LFilter filter(numerator, denominator);
3437 EXPECT_EQ(filter.order(), 2u);
3438
3440 for (size_t offset = 0; offset < signal.size(); offset += 11)
3441 {
3442 const size_t length = std::min(size_t(11), signal.size() - offset);
3443 Array<double> chunk;
3444 chunk.reserve(length);
3445 for (size_t i = 0; i < length; ++i)
3446 chunk.append(signal[offset + i]);
3447
3448 const auto partial = filter.filter(chunk);
3449 for (size_t i = 0; i < partial.size(); ++i)
3450 streamed.append(partial[i]);
3451 }
3452
3454
3455 filter.reset();
3456 expect_real_array_near(filter.filter(signal), expected, 1e-12);
3457}
3458
3460{
3461 std::mt19937_64 rng(31313131);
3462 std::uniform_real_distribution<double> dist(-1.0, 1.0);
3463
3464 Array<double> signal;
3465 signal.reserve(52);
3466 for (size_t i = 0; i < 52; ++i)
3467 signal.append(dist(rng));
3468
3470 FFTD::BiquadSection{0.075, 0.15, 0.075, 1.0, -0.9, 0.2},
3471 FFTD::BiquadSection{0.14, 0.28, 0.14, 1.0, -0.5, 0.06}
3472 };
3473
3474 const auto expected = FFTD::sosfilt(signal, sections);
3475 FFTD::SOSFilter filter(sections);
3476 EXPECT_EQ(filter.num_sections(), 2u);
3477
3479 for (size_t offset = 0; offset < signal.size(); offset += 9)
3480 {
3481 const size_t length = std::min(size_t(9), signal.size() - offset);
3482 Array<double> chunk;
3483 chunk.reserve(length);
3484 for (size_t i = 0; i < length; ++i)
3485 chunk.append(signal[offset + i]);
3486
3487 const auto partial = filter.filter(chunk);
3488 for (size_t i = 0; i < partial.size(); ++i)
3489 streamed.append(partial[i]);
3490 }
3491
3493
3494 filter.reset();
3495 expect_real_array_near(filter.filter(signal), expected, 1e-12);
3496}
3497
3499{
3500 ThreadPool pool(4);
3502 Array<double>({1.0, -0.5, 0.25, 2.0, -1.0, 0.5, 1.5, -0.75, 0.25}),
3503 Array<double>({-1.0, 0.75, 0.5, -0.25, 1.25, -1.5, 0.0, 0.5}),
3504 Array<double>({0.25, -0.125, 0.75, 1.5, -0.5, 0.0, 0.5})
3505 };
3506 Array<double> numerator = {0.075, 0.15, 0.075};
3507 Array<double> denominator = {1.0, -0.9, 0.2};
3508 Array<size_t> chunk_pattern = {2, 3, 1, 4};
3509
3511 for (size_t i = 0; i < signals.size(); ++i)
3512 expected(i) = FFTD::lfilter(signals[i], numerator, denominator);
3513
3514 const auto batch = FFTD::batched_lfilter(signals, numerator, denominator);
3515 const auto parallel_batch =
3516 FFTD::pbatched_lfilter(pool, signals, numerator, denominator);
3517 for (size_t i = 0; i < signals.size(); ++i)
3518 {
3519 expect_real_array_near(batch[i], expected[i], 1e-12);
3521 }
3522
3523 FFTD::LFilterBank bank(signals.size(), numerator, denominator);
3524 FFTD::LFilterBank parallel_bank(signals.size(), numerator, denominator);
3527 Array<size_t> offsets = {0, 0, 0};
3528
3529 for (size_t step = 0;; ++step)
3530 {
3531 bool any_pending = false;
3532 Array<Array<double>> block;
3533 for (size_t channel = 0; channel < signals.size(); ++channel)
3534 {
3535 const size_t remaining = signals[channel].size() - offsets[channel];
3536 const size_t length = std::min(chunk_pattern[step % chunk_pattern.size()],
3537 remaining);
3538 Array<double> chunk;
3539 chunk.reserve(length);
3540 for (size_t i = 0; i < length; ++i)
3541 chunk.append(signals[channel][offsets[channel] + i]);
3542 offsets(channel) += length;
3543 any_pending = any_pending or length != 0;
3544 block.append(chunk);
3545 }
3546
3547 if (not any_pending)
3548 break;
3549
3550 const auto emitted = bank.filter(block);
3551 const auto parallel_emitted = parallel_bank.pfilter(pool, block);
3552 for (size_t channel = 0; channel < signals.size(); ++channel)
3553 {
3554 append_real_samples(streamed(channel), emitted[channel]);
3556 }
3557 }
3558
3559 for (size_t i = 0; i < signals.size(); ++i)
3560 {
3563 }
3564
3565 bank.reset();
3566 const auto rerun = bank.filter(signals);
3567 for (size_t i = 0; i < signals.size(); ++i)
3568 expect_real_array_near(rerun[i], expected[i], 1e-12);
3569}
3570
3572{
3573 ThreadPool pool(4);
3575 Array<double>({1.0, -0.5, 0.25, 2.0, -1.0, 0.5, 1.5, -0.75, 0.25}),
3576 Array<double>({-1.0, 0.75, 0.5, -0.25, 1.25, -1.5, 0.0, 0.5}),
3577 Array<double>({0.25, -0.125, 0.75, 1.5, -0.5, 0.0, 0.5})
3578 };
3580 FFTD::BiquadSection{0.075, 0.15, 0.075, 1.0, -0.9, 0.2},
3581 FFTD::BiquadSection{0.14, 0.28, 0.14, 1.0, -0.5, 0.06}
3582 };
3583 Array<size_t> chunk_pattern = {3, 1, 2, 4};
3584
3586 for (size_t i = 0; i < signals.size(); ++i)
3587 expected(i) = FFTD::sosfilt(signals[i], sections);
3588
3589 const auto batch = FFTD::batched_sosfilt(signals, sections);
3590 const auto parallel_batch = FFTD::pbatched_sosfilt(pool, signals, sections);
3591 for (size_t i = 0; i < signals.size(); ++i)
3592 {
3593 expect_real_array_near(batch[i], expected[i], 1e-12);
3595 }
3596
3597 FFTD::SOSFilterBank bank(signals.size(), sections);
3598 FFTD::SOSFilterBank parallel_bank(signals.size(), sections);
3601 Array<size_t> offsets = {0, 0, 0};
3602
3603 for (size_t step = 0;; ++step)
3604 {
3605 bool any_pending = false;
3606 Array<Array<double>> block;
3607 for (size_t channel = 0; channel < signals.size(); ++channel)
3608 {
3609 const size_t remaining = signals[channel].size() - offsets[channel];
3610 const size_t length = std::min(chunk_pattern[step % chunk_pattern.size()],
3611 remaining);
3612 Array<double> chunk;
3613 chunk.reserve(length);
3614 for (size_t i = 0; i < length; ++i)
3615 chunk.append(signals[channel][offsets[channel] + i]);
3616 offsets(channel) += length;
3617 any_pending = any_pending or length != 0;
3618 block.append(chunk);
3619 }
3620
3621 if (not any_pending)
3622 break;
3623
3624 const auto emitted = bank.filter(block);
3625 const auto parallel_emitted = parallel_bank.pfilter(pool, block);
3626 for (size_t channel = 0; channel < signals.size(); ++channel)
3627 {
3628 append_real_samples(streamed(channel), emitted[channel]);
3630 }
3631 }
3632
3633 for (size_t i = 0; i < signals.size(); ++i)
3634 {
3637 }
3638
3639 bank.reset();
3640 const auto rerun = bank.filter(signals);
3641 for (size_t i = 0; i < signals.size(); ++i)
3642 expect_real_array_near(rerun[i], expected[i], 1e-12);
3643}
3644
3646{
3647 Array<double> fir = {0.5, 0.5};
3648 const auto fir_response = FFTD::freqz(fir, 5, false);
3649 ASSERT_EQ(fir_response.omega.size(), 5u);
3650 ASSERT_EQ(fir_response.response.size(), 5u);
3651 expect_complex_near(fir_response.response[0], Complex(1.0, 0.0), 1e-12);
3652 expect_complex_near(fir_response.response[4], Complex(0.0, 0.0), 1e-12);
3653
3654 FFTD::BiquadSection s1{0.075, 0.15, 0.075, 1.0, -0.9, 0.2};
3655 FFTD::BiquadSection s2{0.14, 0.28, 0.14, 1.0, -0.5, 0.06};
3657
3658 const auto r1 = FFTD::freqz(s1, 9, false);
3659 const auto r2 = FFTD::freqz(s2, 9, false);
3660 const auto rs = FFTD::freqz(sections, 9, false);
3661 for (size_t i = 0; i < rs.response.size(); ++i)
3662 expect_complex_near(rs.response[i], r1.response[i] * r2.response[i], 1e-12);
3663}
3664
3666{
3667 Array<double> numerator = {0.5, 0.5};
3668 Array<double> denominator = {1.0, -0.9, 0.2};
3669 FFTD::IIRCoefficients coeffs{numerator, denominator};
3670 FFTD::BiquadSection section{0.5, 0.5, 0.0, 1.0, -0.9, 0.2};
3671
3672 expect_complex_unordered_near(FFTD::zeros(numerator),
3673 Array<Complex>({Complex(-1.0, 0.0)}),
3674 1e-8);
3675 expect_complex_unordered_near(FFTD::poles(denominator),
3676 Array<Complex>({Complex(0.5, 0.0),
3677 Complex(0.4, 0.0)}),
3678 1e-8);
3679 EXPECT_TRUE(FFTD::zeros(Array<double>({0.0, 0.0, 1.0})).is_empty());
3680
3681 EXPECT_TRUE(FFTD::is_stable(denominator));
3682 EXPECT_TRUE(FFTD::is_stable(coeffs));
3683 EXPECT_TRUE(FFTD::is_stable(section));
3684 EXPECT_FALSE(FFTD::is_stable(Array<double>({1.0, -1.1})));
3685 EXPECT_NO_THROW(FFTD::validate_stable(denominator));
3686 EXPECT_THROW(FFTD::validate_stable(Array<double>({1.0, -1.1})),
3687 std::domain_error);
3688
3689 Array<double> delay_line = {0.0, 0.0, 1.0};
3690 const auto response = FFTD::freqz(delay_line, 65, false);
3691 const auto group = FFTD::group_delay(response);
3692 const auto phase = FFTD::phase_delay(response);
3693 const auto wrapped_group = FFTD::group_delay(delay_line, 65, false);
3694 const auto wrapped_phase = FFTD::phase_delay(delay_line, 65, false);
3695
3698
3699 for (size_t i = 1; i + 1 < group.size(); ++i)
3700 {
3701 EXPECT_NEAR(group[i], 2.0, 1e-6);
3702 EXPECT_NEAR(phase[i], 2.0, 1e-6);
3703 }
3704}
3705
3707{
3708 Array<double> numerator = {1.0, 0.59, -0.41};
3709 Array<double> denominator = {1.0, -0.9, 0.2};
3710
3711 const auto pairs = FFTD::pair_poles_and_zeros(numerator, denominator);
3712 ASSERT_EQ(pairs.size(), 2u);
3713 EXPECT_TRUE(pairs[0].has_zero);
3714 EXPECT_TRUE(pairs[0].has_pole);
3715 EXPECT_NEAR(FFTD::minimum_pole_zero_distance(numerator, denominator), 0.01, 1e-8);
3716 EXPECT_NEAR(FFTD::stability_margin(denominator), 0.5, 1e-8);
3717
3718 EXPECT_TRUE(FFTD::has_near_pole_zero_cancellation(numerator, denominator, 0.02));
3719 EXPECT_FALSE(FFTD::has_near_pole_zero_cancellation(numerator, denominator, 0.005));
3720 EXPECT_THROW(FFTD::validate_no_near_pole_zero_cancellation(numerator,
3721 denominator,
3722 0.02),
3723 std::domain_error);
3724 EXPECT_NO_THROW(FFTD::validate_no_near_pole_zero_cancellation(numerator,
3725 denominator,
3726 0.005));
3727
3728 EXPECT_NO_THROW(FFTD::validate_stable(denominator, 0.4));
3729 EXPECT_THROW(FFTD::validate_stable(denominator, 0.6), std::domain_error);
3730}
3731
3733{
3734 Array<Complex> roots = {
3735 Complex(0.985, 0.0),
3736 Complex(0.972, 0.0),
3737 Complex(0.91, 0.12),
3738 Complex(0.91, -0.12),
3739 Complex(0.84, 0.21),
3740 Complex(0.84, -0.21),
3741 Complex(-0.55, 0.0)
3742 };
3743
3744 const auto polynomial = real_polynomial_from_roots(roots);
3745 expect_complex_unordered_near(FFTD::poles(polynomial), roots, 1e-7);
3746
3748 for (size_t i = 0; i < polynomial.size(); ++i)
3749 scaled(i) = polynomial[i] * 1e120;
3750 expect_complex_unordered_near(FFTD::poles(scaled), roots, 1e-7);
3751}
3752
3754{
3755 std::mt19937_64 rng(20260306);
3756 std::uniform_real_distribution<double> real_dist(-0.92, 0.92);
3757 std::uniform_real_distribution<double> radius_dist(0.55, 0.95);
3758 std::uniform_real_distribution<double> angle_dist(0.12, 1.0);
3759
3760 for (size_t trial = 0; trial < 12; ++trial)
3761 {
3762 Array<Complex> roots = {
3763 Complex(real_dist(rng), 0.0),
3764 Complex(real_dist(rng), 0.0)
3765 };
3766
3767 for (size_t pair = 0; pair < 2; ++pair)
3768 {
3769 const double radius = radius_dist(rng);
3770 const double angle = angle_dist(rng);
3771 roots.append(std::polar(radius, angle));
3772 roots.append(std::polar(radius, -angle));
3773 }
3774
3775 const auto denominator = real_polynomial_from_roots(roots);
3776 const auto recovered = FFTD::poles(denominator);
3778 EXPECT_TRUE(FFTD::is_stable(denominator));
3779 }
3780}
3781
3783{
3784 FFTD::BiquadSection s1{0.075, 0.15, 0.075, 1.0, -0.9, 0.2};
3785 FFTD::BiquadSection s2{0.14, 0.28, 0.14, 1.0, -0.5, 0.06};
3787
3788 const auto group_sections = FFTD::group_delay(sections, 257, false);
3789 const auto phase_sections = FFTD::phase_delay(sections, 257, false);
3790 const auto group_1 = FFTD::group_delay(s1, 257, false);
3791 const auto group_2 = FFTD::group_delay(s2, 257, false);
3792 const auto phase_1 = FFTD::phase_delay(s1, 257, false);
3793 const auto phase_2 = FFTD::phase_delay(s2, 257, false);
3794 const auto response_1 = FFTD::freqz(s1, 257, false);
3795 const auto response_2 = FFTD::freqz(s2, 257, false);
3796
3797 ASSERT_EQ(group_sections.size(), group_1.size());
3798 ASSERT_EQ(phase_sections.size(), phase_1.size());
3799 for (size_t i = 0; i < group_sections.size(); ++i)
3800 {
3801 if (std::abs(response_1.response[i]) <= 1e-6
3802 or std::abs(response_2.response[i]) <= 1e-6)
3803 continue;
3804 EXPECT_NEAR(group_sections[i], group_1[i] + group_2[i], 1e-8);
3805 EXPECT_NEAR(phase_sections[i], phase_1[i] + phase_2[i], 1e-8);
3806 }
3807}
3808
3810{
3812 FFTD::BiquadSection{0.3, 0.6, 0.3, 1.0, -0.9, 0.2},
3813 FFTD::BiquadSection{0.14, 0.28, 0.14, 1.0, -0.5, 0.06}
3814 };
3815
3816 const auto coarse_phase = FFTD::phase_margin(sections, 257, true);
3817 const auto ref_phase =
3818 FFTD::phase_margin(FFTD::freqz(sections, 32769, true));
3820 ASSERT_TRUE(ref_phase.found);
3821 EXPECT_NEAR(coarse_phase.crossover_omega, ref_phase.crossover_omega, 1e-4);
3822 EXPECT_NEAR(coarse_phase.degrees, ref_phase.degrees, 1e-3);
3823
3824 const auto coarse_gain = FFTD::gain_margin(sections, 257, true);
3825 const auto ref_gain =
3826 FFTD::gain_margin(FFTD::freqz(sections, 32769, true));
3827 ASSERT_TRUE(coarse_gain.found);
3828 ASSERT_TRUE(ref_gain.found);
3829 EXPECT_NEAR(coarse_gain.crossover_omega, ref_gain.crossover_omega, 1e-4);
3830 EXPECT_NEAR(coarse_gain.decibels, ref_gain.decibels, 1e-3);
3831}
3832
3834{
3835 FFTD::BiquadSection s1{0.075, 0.15, 0.075, 1.0, -0.9, 0.2};
3836 FFTD::BiquadSection s2{0.2, 0.1, 0.05, 1.0, -1.7, 0.72};
3838
3839 const double margin_1 = FFTD::stability_margin(s1);
3840 const double margin_2 = FFTD::stability_margin(s2);
3841 EXPECT_NEAR(FFTD::stability_margin(sections), std::min(margin_1, margin_2), 1e-12);
3842 EXPECT_NO_THROW(FFTD::validate_stable(sections, 0.09));
3843 EXPECT_THROW(FFTD::validate_stable(sections, 0.11), std::domain_error);
3844}
3845
3847{
3848 FFTD::FrequencyResponse gain_crossover_response;
3849 gain_crossover_response.omega = {0.0, 1.0, 2.0};
3850 gain_crossover_response.response = {
3851 std::polar(2.0, 0.0),
3852 std::polar(1.0, -std::numbers::pi / 2.0),
3853 std::polar(0.5, -3.0 * std::numbers::pi / 4.0)
3854 };
3855
3856 const auto phase_margin = FFTD::phase_margin(gain_crossover_response);
3857 EXPECT_TRUE(phase_margin.found);
3858 EXPECT_NEAR(phase_margin.crossover_omega, 1.0, 1e-12);
3859 EXPECT_NEAR(phase_margin.radians, std::numbers::pi / 2.0, 1e-12);
3860 EXPECT_NEAR(phase_margin.degrees, 90.0, 1e-10);
3861
3862 FFTD::FrequencyResponse phase_crossover_response;
3863 phase_crossover_response.omega = {0.0, 1.0, 2.0};
3864 phase_crossover_response.response = {
3865 std::polar(2.0, -std::numbers::pi / 2.0),
3866 std::polar(0.5, -std::numbers::pi),
3867 std::polar(0.25, -3.0 * std::numbers::pi / 2.0)
3868 };
3869
3870 const auto gain_margin = FFTD::gain_margin(phase_crossover_response);
3871 EXPECT_TRUE(gain_margin.found);
3872 EXPECT_NEAR(gain_margin.crossover_omega, 1.0, 1e-12);
3873 EXPECT_NEAR(gain_margin.ratio, 2.0, 1e-12);
3874 EXPECT_NEAR(gain_margin.decibels, 20.0 * std::log10(2.0), 1e-12);
3875}
3876
3878{
3879 FFTD::LFilter iir;
3880 FFTD::SOSFilter sos;
3881
3882 EXPECT_FALSE(iir.configured());
3883 EXPECT_FALSE(sos.configured());
3884 EXPECT_THROW(iir.reset(), std::runtime_error);
3885 EXPECT_THROW(iir.filter(Array<double>({1.0, 2.0})), std::runtime_error);
3886 EXPECT_THROW(sos.reset(), std::runtime_error);
3887 EXPECT_THROW(sos.filter(Array<double>({1.0, 2.0})), std::runtime_error);
3888}
3889
3891{
3892 const auto bilinear =
3893 FFTD::bilinear_transform(Array<double>({1.0}),
3894 Array<double>({1.0, 1.0}),
3895 8.0);
3896 ASSERT_EQ(bilinear.numerator.size(), 2u);
3897 ASSERT_EQ(bilinear.denominator.size(), 2u);
3898 EXPECT_NEAR(bilinear.numerator[0], 1.0 / 17.0, 1e-12);
3899 EXPECT_NEAR(bilinear.numerator[1], 1.0 / 17.0, 1e-12);
3900 EXPECT_NEAR(bilinear.denominator[0], 1.0, 1e-12);
3901 EXPECT_NEAR(bilinear.denominator[1], -15.0 / 17.0, 1e-12);
3902
3903 const auto butter_lp = FFTD::butterworth_lowpass(4, 800.0, 8000.0);
3904 const auto butter_hp = FFTD::butterworth_highpass(4, 800.0, 8000.0);
3905 const auto cheby_lp = FFTD::chebyshev1_lowpass(4, 1.0, 800.0, 8000.0);
3906 const auto cheby_hp = FFTD::chebyshev1_highpass(5, 1.0, 800.0, 8000.0);
3907
3908 EXPECT_TRUE(FFTD::is_stable(butter_lp));
3909 EXPECT_TRUE(FFTD::is_stable(butter_hp));
3910 EXPECT_TRUE(FFTD::is_stable(cheby_lp));
3911 EXPECT_TRUE(FFTD::is_stable(cheby_hp));
3912
3913 const auto butter_lp_response = FFTD::freqz(butter_lp, 513, false);
3914 const auto butter_hp_response = FFTD::freqz(butter_hp, 513, false);
3915 const auto cheby_lp_response = FFTD::freqz(cheby_lp, 513, false);
3916 const auto cheby_hp_response = FFTD::freqz(cheby_hp, 513, false);
3917
3918 EXPECT_NEAR(std::abs(butter_lp_response.response[0]), 1.0, 1e-9);
3919 EXPECT_LT(std::abs(butter_lp_response.response[512]), 1e-3);
3920 EXPECT_LT(std::abs(butter_hp_response.response[0]), 1e-6);
3921 EXPECT_NEAR(std::abs(butter_hp_response.response[512]), 1.0, 1e-6);
3922
3923 EXPECT_NEAR(std::abs(cheby_lp_response.response[0]),
3924 std::pow(10.0, -1.0 / 20.0),
3925 5e-6);
3926 EXPECT_LT(std::abs(cheby_lp_response.response[512]), 1e-3);
3927 EXPECT_NEAR(std::abs(cheby_hp_response.response[512]), 1.0, 1e-4);
3928 EXPECT_LT(std::abs(cheby_hp_response.response[0]), 1e-6);
3929
3930 const auto butter_phase_margin = FFTD::phase_margin(butter_lp, 2049, false);
3932 EXPECT_GT(butter_phase_margin.degrees, 0.0);
3933
3934 EXPECT_THROW(FFTD::butterworth_lowpass(0, 800.0, 8000.0), std::invalid_argument);
3935 EXPECT_THROW(FFTD::butterworth_lowpass(4, 4000.0, 8000.0), std::invalid_argument);
3936 EXPECT_THROW(FFTD::chebyshev1_lowpass(4, 0.0, 800.0, 8000.0),
3937 std::invalid_argument);
3938}
3939
3941{
3942 constexpr double sample_rate = 8000.0;
3943 constexpr size_t num_points = 2049;
3944
3945 const auto butter_bp = FFTD::butterworth_bandpass(4, 700.0, 1500.0, sample_rate);
3946 const auto butter_bs = FFTD::butterworth_bandstop(4, 700.0, 1500.0, sample_rate);
3947 const auto cheby1_bp =
3948 FFTD::chebyshev1_bandpass(4, 1.0, 700.0, 1500.0, sample_rate);
3949 const auto cheby1_bs =
3950 FFTD::chebyshev1_bandstop(4, 1.0, 700.0, 1500.0, sample_rate);
3951 const auto cheby2_lp =
3952 FFTD::chebyshev2_lowpass(4, 40.0, 900.0, sample_rate);
3953 const auto cheby2_hp =
3954 FFTD::chebyshev2_highpass(4, 40.0, 900.0, sample_rate);
3955 const auto cheby2_bp =
3956 FFTD::chebyshev2_bandpass(4, 40.0, 700.0, 1500.0, sample_rate);
3957 const auto cheby2_bs =
3958 FFTD::chebyshev2_bandstop(4, 40.0, 700.0, 1500.0, sample_rate);
3959 const auto bessel_lp = FFTD::bessel_lowpass(4, 900.0, sample_rate);
3960 const auto bessel_hp = FFTD::bessel_highpass(4, 900.0, sample_rate);
3961 const auto bessel_bp = FFTD::bessel_bandpass(4, 700.0, 1500.0, sample_rate);
3962 const auto bessel_bs = FFTD::bessel_bandstop(4, 700.0, 1500.0, sample_rate);
3963
3964 for (const auto & sections : {butter_bp,
3965 butter_bs,
3966 cheby1_bp,
3967 cheby1_bs,
3968 cheby2_lp,
3969 cheby2_hp,
3970 cheby2_bp,
3971 cheby2_bs,
3972 bessel_lp,
3973 bessel_hp,
3974 bessel_bp,
3975 bessel_bs})
3976 EXPECT_TRUE(FFTD::is_stable(sections));
3977
3978 const auto butter_bp_response = FFTD::freqz(butter_bp, num_points, false);
3979 const auto butter_bs_response = FFTD::freqz(butter_bs, num_points, false);
3980 const auto cheby1_bp_response = FFTD::freqz(cheby1_bp, num_points, false);
3981 const auto cheby1_bs_response = FFTD::freqz(cheby1_bs, num_points, false);
3982 const auto cheby2_lp_response = FFTD::freqz(cheby2_lp, num_points, false);
3983 const auto cheby2_hp_response = FFTD::freqz(cheby2_hp, num_points, false);
3984 const auto cheby2_bp_response = FFTD::freqz(cheby2_bp, num_points, false);
3985 const auto cheby2_bs_response = FFTD::freqz(cheby2_bs, num_points, false);
3986 const auto bessel_lp_response = FFTD::freqz(bessel_lp, num_points, false);
3987 const auto bessel_hp_response = FFTD::freqz(bessel_hp, num_points, false);
3988 const auto bessel_bp_response = FFTD::freqz(bessel_bp, num_points, false);
3989 const auto bessel_bs_response = FFTD::freqz(bessel_bs, num_points, false);
3990
3997
4004
4016
4026
4027 EXPECT_THROW(FFTD::butterworth_bandpass(4, 1500.0, 700.0, sample_rate),
4028 std::invalid_argument);
4029 EXPECT_THROW(FFTD::chebyshev2_lowpass(4, 0.0, 900.0, sample_rate),
4030 std::invalid_argument);
4031 EXPECT_THROW(FFTD::bessel_lowpass(0, 900.0, sample_rate),
4032 std::invalid_argument);
4033}
4034
4036{
4037 constexpr double sample_rate = 8000.0;
4038 constexpr size_t num_points = 2049;
4039
4040 const auto elliptic_lp =
4041 FFTD::elliptic_lowpass(4, 1.0, 40.0, 900.0, sample_rate);
4042 const auto elliptic_hp =
4043 FFTD::elliptic_highpass(4, 1.0, 40.0, 900.0, sample_rate);
4044 const auto elliptic_bp =
4045 FFTD::elliptic_bandpass(4, 1.0, 40.0, 700.0, 1500.0, sample_rate);
4046 const auto elliptic_bs =
4047 FFTD::elliptic_bandstop(4, 1.0, 40.0, 700.0, 1500.0, sample_rate);
4048
4049 for (const auto & sections : {elliptic_lp, elliptic_hp, elliptic_bp, elliptic_bs})
4050 {
4051 EXPECT_FALSE(sections.is_empty());
4052 EXPECT_TRUE(FFTD::is_stable(sections));
4053 }
4054
4055 const auto elliptic_lp_response = FFTD::freqz(elliptic_lp, num_points, false);
4056 const auto elliptic_hp_response = FFTD::freqz(elliptic_hp, num_points, false);
4057 const auto elliptic_bp_response = FFTD::freqz(elliptic_bp, num_points, false);
4058 const auto elliptic_bs_response = FFTD::freqz(elliptic_bs, num_points, false);
4059
4062
4065
4069
4073
4074 EXPECT_THROW(FFTD::elliptic_lowpass(0, 1.0, 40.0, 900.0, sample_rate),
4075 std::invalid_argument);
4076 EXPECT_THROW(FFTD::elliptic_lowpass(4, 0.0, 40.0, 900.0, sample_rate),
4077 std::invalid_argument);
4078 EXPECT_THROW(FFTD::elliptic_lowpass(4, 1.0, 0.5, 900.0, sample_rate),
4079 std::invalid_argument);
4080}
4081
4083{
4084 std::mt19937_64 rng(24681357);
4085 std::uniform_real_distribution<double> dist(-2.0, 2.0);
4086
4087 Array<double> signal;
4088 signal.reserve(257);
4089 for (size_t i = 0; i < 257; ++i)
4090 signal.append(dist(rng));
4091
4092 Array<double> kernel;
4093 kernel.reserve(37);
4094 for (size_t i = 0; i < 37; ++i)
4095 kernel.append(dist(rng));
4096
4097 const auto expected = FFTD::multiply(signal, kernel);
4098
4099 for (const size_t block_size : {size_t(0), size_t(8), size_t(31), size_t(64)})
4100 {
4101 const auto obtained = FFTD::overlap_add_convolution(signal, kernel, block_size);
4103 }
4104}
4105
4107{
4108 ThreadPool pool(4);
4109 std::mt19937_64 rng(97531864);
4110 std::uniform_real_distribution<double> dist(-3.0, 3.0);
4111
4112 Array<double> signal;
4113 signal.reserve(513);
4114 for (size_t i = 0; i < 513; ++i)
4115 signal.append(dist(rng));
4116
4117 Array<double> kernel;
4118 kernel.reserve(29);
4119 for (size_t i = 0; i < 29; ++i)
4120 kernel.append(dist(rng));
4121
4122 FFTD::OverlapAdd convolver(kernel, 63);
4123 EXPECT_EQ(convolver.block_size(), 63u);
4124 EXPECT_GE(convolver.fft_size(), 63u + kernel.size() - 1);
4125
4126 const auto sequential = convolver.convolve(signal);
4127 const auto parallel = convolver.pconvolve(pool, signal);
4128 const auto expected = FFTD::multiply(signal, kernel);
4129
4130 expect_real_array_near(sequential, expected, 1e-7);
4131 expect_real_array_near(parallel, expected, 1e-7);
4132 expect_real_array_near(parallel, sequential, 1e-8);
4133
4134 std::vector<double> signal_vec(signal.begin(), signal.end());
4135 std::vector<double> kernel_vec(kernel.begin(), kernel.end());
4136 const auto wrapper_parallel =
4137 FFTD::poverlap_add_convolution(pool, signal_vec, kernel_vec, 63);
4139}
4140
4142{
4143 Array<double> signal = {1.0, 2.0, 3.0};
4144 Array<double> kernel = {0.25, 0.5, 0.25};
4145 Array<double> empty;
4146
4147 EXPECT_TRUE(FFTD::overlap_add_convolution(empty, kernel).is_empty());
4148 EXPECT_TRUE(FFTD::overlap_add_convolution(signal, empty).is_empty());
4149 EXPECT_THROW(([] { FFTD::OverlapAdd invalid{Array<double>()}; }()),
4150 std::invalid_argument);
4151}
4152
4154{
4155 std::mt19937_64 rng(12344321);
4156 std::uniform_real_distribution<double> dist(-2.0, 2.0);
4157
4158 Array<double> signal;
4159 signal.reserve(150);
4160 for (size_t i = 0; i < 150; ++i)
4161 signal.append(dist(rng));
4162
4163 Array<double> kernel;
4164 kernel.reserve(17);
4165 for (size_t i = 0; i < 17; ++i)
4166 kernel.append(dist(rng));
4167
4168 FFTD::OverlapAdd convolver(kernel, 32);
4170
4171 const size_t chunks[] = {13, 7, 64, 5, 61};
4172 size_t offset = 0;
4173 for (const size_t chunk_size : chunks)
4174 {
4175 Array<double> chunk;
4176 chunk.reserve(chunk_size);
4177 for (size_t i = 0; i < chunk_size; ++i)
4178 chunk.append(signal[offset + i]);
4179 offset += chunk_size;
4180
4181 const auto emitted = convolver.process_block(chunk);
4182 for (size_t i = 0; i < emitted.size(); ++i)
4183 streamed.append(emitted[i]);
4184 }
4185
4186 const auto tail = convolver.flush();
4187 for (size_t i = 0; i < tail.size(); ++i)
4188 streamed.append(tail[i]);
4189
4190 const auto expected = FFTD::multiply(signal, kernel);
4192}
4193
4195{
4196 ThreadPool pool(4);
4197 std::mt19937_64 rng(99884411);
4198 std::uniform_real_distribution<double> dist(-1.5, 1.5);
4199
4200 Array<double> signal;
4201 signal.reserve(96);
4202 for (size_t i = 0; i < 96; ++i)
4203 signal.append(dist(rng));
4204
4205 Array<double> kernel;
4206 kernel.reserve(11);
4207 for (size_t i = 0; i < 11; ++i)
4208 kernel.append(dist(rng));
4209
4210 FFTD::OverlapAdd convolver(kernel, 24);
4212
4213 for (size_t offset = 0; offset < signal.size(); offset += 19)
4214 {
4215 const size_t length = std::min(size_t(19), signal.size() - offset);
4216 Array<double> chunk;
4217 chunk.reserve(length);
4218 for (size_t i = 0; i < length; ++i)
4219 chunk.append(signal[offset + i]);
4220
4221 const auto emitted = convolver.pprocess_block(pool, chunk);
4222 for (size_t i = 0; i < emitted.size(); ++i)
4223 first_run.append(emitted[i]);
4224 }
4225 const auto first_tail = convolver.flush();
4226 for (size_t i = 0; i < first_tail.size(); ++i)
4227 first_run.append(first_tail[i]);
4228
4229 const auto expected = FFTD::multiply(signal, kernel);
4231
4232 convolver.reset();
4234 const auto full_block = convolver.process_block(signal);
4235 for (size_t i = 0; i < full_block.size(); ++i)
4237 const auto second_tail = convolver.flush();
4238 for (size_t i = 0; i < second_tail.size(); ++i)
4239 second_run.append(second_tail[i]);
4240
4242 EXPECT_TRUE(convolver.flush().is_empty());
4243}
4244
4246{
4247 std::mt19937_64 rng(11235813);
4248 std::uniform_real_distribution<double> dist(-1.5, 1.5);
4249
4250 Array<double> signal;
4251 signal.reserve(193);
4252 for (size_t i = 0; i < 193; ++i)
4253 signal.append(dist(rng));
4254
4255 Array<double> kernel;
4256 kernel.reserve(29);
4257 for (size_t i = 0; i < 29; ++i)
4258 kernel.append(dist(rng));
4259
4260 const auto expected = FFTD::multiply(signal, kernel);
4261
4262 for (const size_t block_size : {size_t(0), size_t(8), size_t(32)})
4263 {
4264 const auto actual =
4265 FFTD::overlap_save_convolution(signal, kernel, block_size);
4267
4268 FFTD::OverlapSave processor(kernel, block_size);
4271 processor.process_block(slice_real_array(signal, 0, 57)));
4273 processor.process_block(slice_real_array(signal, 57, 61)));
4275 processor.process_block(slice_real_array(signal, 118, 75)));
4278 }
4279}
4280
4282{
4283 std::mt19937_64 rng(424242);
4284 std::uniform_real_distribution<double> dist(-1.0, 1.0);
4285
4286 Array<double> signal;
4287 signal.reserve(257);
4288 for (size_t i = 0; i < 257; ++i)
4289 signal.append(dist(rng));
4290
4291 Array<double> kernel;
4292 kernel.reserve(73);
4293 for (size_t i = 0; i < 73; ++i)
4294 kernel.append(dist(rng));
4295
4296 const auto expected = FFTD::multiply(signal, kernel);
4297
4298 for (const size_t partition_size : {size_t(0), size_t(8), size_t(16)})
4299 {
4300 const auto actual =
4301 FFTD::partitioned_convolution(signal, kernel, partition_size);
4303
4304 FFTD::PartitionedConvolver processor(kernel, partition_size);
4307 processor.process_block(slice_real_array(signal, 0, 11)));
4309 processor.process_block(slice_real_array(signal, 11, 37)));
4311 processor.process_block(slice_real_array(signal, 48, 209)));
4314 }
4315}
4316
4318{
4319 ThreadPool pool(4);
4320 std::mt19937_64 rng(123987456);
4321 std::uniform_real_distribution<double> dist(-2.0, 2.0);
4322
4324 signals.reserve(3);
4325 for (const size_t size : {size_t(193), size_t(257), size_t(141)})
4326 {
4327 Array<double> signal;
4328 signal.reserve(size);
4329 for (size_t i = 0; i < size; ++i)
4330 signal.append(dist(rng));
4331 signals.append(signal);
4332 }
4333
4334 Array<double> kernel;
4335 kernel.reserve(23);
4336 for (size_t i = 0; i < 23; ++i)
4337 kernel.append(dist(rng));
4338
4339 FFTD::OverlapAddBank bank(signals.size(), kernel, 48);
4340 const auto sequential = bank.convolve(signals);
4341 const auto parallel = bank.pconvolve(pool, signals);
4342 const auto wrapper_parallel =
4343 FFTD::poverlap_add_convolution_batch(pool, signals, kernel, 48);
4344
4345 ASSERT_EQ(sequential.size(), signals.size());
4346 ASSERT_EQ(parallel.size(), signals.size());
4347 ASSERT_EQ(wrapper_parallel.size(), signals.size());
4348 for (size_t channel = 0; channel < signals.size(); ++channel)
4349 {
4350 const auto expected = FFTD::multiply(signals[channel], kernel);
4351 expect_real_array_near(sequential[channel], expected, 1e-7);
4352 expect_real_array_near(parallel[channel], expected, 1e-7);
4354 }
4355}
4356
4358{
4359 ThreadPool pool(4);
4360 std::mt19937_64 rng(555666777);
4361 std::uniform_real_distribution<double> dist(-1.5, 1.5);
4362
4364 signals.reserve(3);
4365 for (const size_t size : {size_t(96), size_t(141), size_t(87)})
4366 {
4367 Array<double> signal;
4368 signal.reserve(size);
4369 for (size_t i = 0; i < size; ++i)
4370 signal.append(dist(rng));
4371 signals.append(signal);
4372 }
4373
4374 Array<double> kernel;
4375 kernel.reserve(13);
4376 for (size_t i = 0; i < 13; ++i)
4377 kernel.append(dist(rng));
4378
4379 FFTD::OverlapAddBank bank(signals.size(), kernel, 24);
4381 Array<size_t> offsets = {0, 0, 0};
4382 Array<size_t> chunk_pattern = {11, 7, 29, 5};
4383
4384 for (size_t step = 0;; ++step)
4385 {
4386 bool any_pending = false;
4387 Array<Array<double>> block;
4388 for (size_t channel = 0; channel < signals.size(); ++channel)
4389 {
4390 const size_t remaining = signals[channel].size() - offsets[channel];
4391 const size_t length = std::min(chunk_pattern[step % chunk_pattern.size()],
4392 remaining);
4393 Array<double> chunk;
4394 chunk.reserve(length);
4395 for (size_t i = 0; i < length; ++i)
4396 chunk.append(signals[channel][offsets[channel] + i]);
4397 offsets(channel) += length;
4398 any_pending = any_pending or length != 0;
4399 block.append(chunk);
4400 }
4401
4402 if (not any_pending)
4403 break;
4404
4405 const auto emitted = bank.pprocess_block(pool, block);
4406 for (size_t channel = 0; channel < signals.size(); ++channel)
4407 append_real_samples(streamed(channel), emitted[channel]);
4408 }
4409
4410 const auto tail = bank.pflush(pool);
4411 for (size_t channel = 0; channel < signals.size(); ++channel)
4412 append_real_samples(streamed(channel), tail[channel]);
4413
4414 for (size_t channel = 0; channel < signals.size(); ++channel)
4415 {
4416 const auto expected = FFTD::multiply(signals[channel], kernel);
4417 expect_real_array_near(streamed[channel], expected, 1e-7);
4418 }
4419
4420 bank.reset();
4421 const auto rerun = bank.process_block(signals);
4422 const auto rerun_tail = bank.flush();
4423 for (size_t channel = 0; channel < signals.size(); ++channel)
4424 {
4425 Array<double> combined = rerun[channel];
4427 const auto expected = FFTD::multiply(signals[channel], kernel);
4429 }
4430}
4431
4433{
4434 ThreadPool pool(4);
4436 Array<double>({1.0, -0.5, 0.25, 2.0, -1.0}),
4437 Array<double>(),
4438 Array<double>({0.5, 1.5, -0.25})
4439 };
4440 Array<double> kernel = {0.25, 0.5, 0.25};
4441
4442 FFTD::OverlapAddBank bank(signals.size(), kernel, 4);
4443 const auto sequential = bank.convolve(signals);
4444 const auto parallel = bank.pconvolve(pool, signals);
4445
4446 ASSERT_EQ(sequential.size(), signals.size());
4447 ASSERT_EQ(parallel.size(), signals.size());
4448 EXPECT_TRUE(sequential[1].is_empty());
4449 EXPECT_TRUE(parallel[1].is_empty());
4450 expect_real_array_near(sequential[0], FFTD::multiply(signals[0], kernel), 1e-12);
4451 expect_real_array_near(sequential[2], FFTD::multiply(signals[2], kernel), 1e-12);
4452 expect_real_array_near(parallel[0], FFTD::multiply(signals[0], kernel), 1e-12);
4453 expect_real_array_near(parallel[2], FFTD::multiply(signals[2], kernel), 1e-12);
4454
4455 bank.reset();
4458 Array<double>({1.0, -0.5}),
4459 Array<double>(),
4460 Array<double>({0.5})
4461 };
4463 Array<double>({0.25, 2.0, -1.0}),
4464 Array<double>(),
4465 Array<double>({1.5, -0.25})
4466 };
4467
4468 const auto part_a = bank.pprocess_block(pool, first_block);
4469 const auto part_b = bank.pprocess_block(pool, second_block);
4470 const auto tail = bank.pflush(pool);
4471 for (size_t channel = 0; channel < signals.size(); ++channel)
4472 {
4473 append_real_samples(streamed(channel), part_a[channel]);
4474 append_real_samples(streamed(channel), part_b[channel]);
4475 append_real_samples(streamed(channel), tail[channel]);
4476 }
4477
4478 EXPECT_TRUE(streamed[1].is_empty());
4479 expect_real_array_near(streamed[0], FFTD::multiply(signals[0], kernel), 1e-12);
4480 expect_real_array_near(streamed[2], FFTD::multiply(signals[2], kernel), 1e-12);
4481}
Simple dynamic array with automatic resizing and functional operations.
Definition tpl_array.H:139
static Array create(size_t n)
Create an array with n logical elements.
Definition tpl_array.H:194
constexpr size_t size() const noexcept
Return the number of elements stored in the stack.
Definition tpl_array.H:351
constexpr bool is_empty() const noexcept
Checks if the container is empty.
Definition tpl_array.H:348
T & append(const T &data)
Append a copy of data
Definition tpl_array.H:245
void reserve(size_t cap)
Reserves cap cells into the array.
Definition tpl_array.H:315
Fast Fourier Transform (FFT) and DSP Toolkit.
Definition fft.H:154
A reusable thread pool for efficient parallel task execution.
iterator end() noexcept
Return an STL-compatible end iterator.
iterator begin() noexcept
Return an STL-compatible iterator to the first element.
#define TEST(name)
static mt19937 rng
Fast Fourier Transform (FFT) and Digital Signal Processing (DSP) toolkit.
const long double offset[]
Offset values indexed by symbol string length (bounded by MAX_OFFSET_INDEX)
static mpfr_t y
Definition mpfr_mul_d.c:3
Main namespace for Aleph-w library functions.
Definition ah-arena.H:89
and
Check uniqueness with explicit hash + equality functors.
size_t size(Node *root) noexcept
Container2< typename Container1::Item_Type > filter(Container1 &container, Operation &operation)
Filter elements that satisfy operation.
Divide_Conquer_DP_Result< Cost > divide_and_conquer_partition_dp(const size_t groups, const size_t n, Transition_Cost_Fn transition_cost, const Cost inf=dp_optimization_detail::default_inf< Cost >())
Optimize partition DP using divide-and-conquer optimization.
T product(const Container &container, const T &init=T{1})
Compute product of all elements.
std::pair< First, Second > pair
Alias to std::pair kept for backwards compatibility.
Definition ahPair.H:89
void next()
Advance all underlying iterators (bounds-checked).
Definition ah-zip.H:171
auto max_value(const Container &data) -> std::decay_t< decltype(*std::begin(data))>
Compute maximum value.
Definition stat_utils.H:296
T sum(const Container &container, const T &init=T{})
Compute sum of all elements.
static struct argp_option options[]
Definition ntreepic.C:1886
static void section(const string &title)
FooMap m(5, fst_unit_pair_hash, snd_unit_pair_hash)
static int * k
Dynamic array container with automatic resizing.
ofstream output
Definition writeHeap.C:215