• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 16059887550

03 Jul 2025 08:24PM UTC coverage: 74.575% (+1.6%) from 73.02%
16059887550

push

github

paulmthompson
fix zero phase error and test failures

124 of 125 new or added lines in 4 files covered. (99.2%)

108 existing lines in 3 files now uncovered.

13249 of 17766 relevant lines covered (74.58%)

1104.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.25
/src/WhiskerToolbox/DataManager/utils/filter/filter.cpp
1
#include "filter.hpp"
2
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
3

4
#include <algorithm>
5
#include <cmath>
6
#include <iostream>
7
#include <map>
8
#include <numeric>
9
#include <stdexcept>
10

11
// ========== FilterOptions Validation ==========
12

13
bool FilterOptions::isValid() const {
58✔
14
    return getValidationError().empty();
58✔
15
}
16

17
std::string FilterOptions::getValidationError() const {
70✔
18
    if (order < 1 || order > max_filter_order) {
70✔
19
        return "Filter order must be between 1 and " + std::to_string(max_filter_order);
12✔
20
    }
21

22
    if (sampling_rate_hz <= 0.0) {
64✔
23
        return "Sampling rate must be positive";
12✔
24
    }
25

26
    if (cutoff_frequency_hz <= 0.0) {
60✔
27
        return "Cutoff frequency must be positive";
6✔
28
    }
29

30
    double nyquist = sampling_rate_hz / 2.0;
58✔
31
    if (cutoff_frequency_hz >= nyquist) {
58✔
32
        return "Cutoff frequency must be less than Nyquist frequency (" +
8✔
33
               std::to_string(nyquist) + " Hz)";
8✔
34
    }
35

36
    // Additional validation for band filters
37
    if (response == FilterResponse::BandPass || response == FilterResponse::BandStop) {
56✔
38
        // For RBJ filters, we use cutoff_frequency_hz as center frequency and q_factor
39
        if (type == FilterType::RBJ) {
10✔
40
            if (cutoff_frequency_hz >= nyquist) {
3✔
UNCOV
41
                return "Center frequency must be less than Nyquist frequency";
×
42
            }
43
            if (q_factor <= 0.0) {
3✔
UNCOV
44
                return "Q factor must be positive for RBJ band filters";
×
45
            }
46
        } else {
47
            // For IIR filters, we need both cutoff frequencies
48
            if (high_cutoff_hz <= cutoff_frequency_hz) {
7✔
49
                return "High cutoff frequency must be greater than low cutoff frequency";
6✔
50
            }
51
            if (high_cutoff_hz >= nyquist) {
5✔
UNCOV
52
                return "High cutoff frequency must be less than Nyquist frequency";
×
53
            }
54
        }
55
    }
56

57
    // Validation for filter-specific parameters
58
    if (type == FilterType::ChebyshevI && passband_ripple_db <= 0.0) {
54✔
59
        return "Chebyshev I passband ripple must be positive";
6✔
60
    }
61

62
    if (type == FilterType::ChebyshevII && stopband_ripple_db <= 0.0) {
52✔
63
        return "Chebyshev II stopband ripple must be positive";
6✔
64
    }
65

66
    if (type == FilterType::RBJ && q_factor <= 0.0) {
50✔
67
        return "RBJ Q factor must be positive";
6✔
68
    }
69

70
    return "";// Valid
144✔
71
}
72

73
// ========== Sampling Rate Estimation ==========
74

75
double estimateSamplingRate(
4✔
76
        AnalogTimeSeries const * analog_time_series,
77
        std::optional<TimeFrameIndex> start_time,
78
        std::optional<TimeFrameIndex> end_time) {
79
    if (!analog_time_series) {
4✔
80
        return 0.0;
1✔
81
    }
82

83
    size_t num_samples = analog_time_series->getNumSamples();
3✔
84
    if (num_samples < 2) {
3✔
85
        return 0.0;
2✔
86
    }
87

88
    // Get time series for analysis
89
    auto time_indices = analog_time_series->getTimeSeries();
1✔
90

91
    // Determine analysis range
92
    size_t start_idx = 0;
1✔
93
    size_t end_idx = time_indices.size();
1✔
94

95
    if (start_time.has_value()) {
1✔
96
        auto start_data_idx = analog_time_series->findDataArrayIndexGreaterOrEqual(start_time.value());
×
97
        if (start_data_idx.has_value()) {
×
UNCOV
98
            start_idx = start_data_idx.value().getValue();
×
99
        }
100
    }
101

102
    if (end_time.has_value()) {
1✔
103
        auto end_data_idx = analog_time_series->findDataArrayIndexLessOrEqual(end_time.value());
×
104
        if (end_data_idx.has_value()) {
×
UNCOV
105
            end_idx = std::min(end_data_idx.value().getValue() + 1, time_indices.size());
×
106
        }
107
    }
108

109
    if (end_idx <= start_idx + 1) {
1✔
UNCOV
110
        return 0.0;
×
111
    }
112

113
    // Calculate time differences
114
    std::vector<double> time_diffs;
1✔
115
    time_diffs.reserve(end_idx - start_idx - 1);
1✔
116

117
    for (size_t i = start_idx + 1; i < end_idx; ++i) {
100✔
118
        double dt = static_cast<double>(time_indices[i].getValue() - time_indices[i - 1].getValue());
99✔
119
        if (dt > 0) {
99✔
120
            time_diffs.push_back(dt);
99✔
121
        }
122
    }
123

124
    if (time_diffs.empty()) {
1✔
UNCOV
125
        return 0.0;
×
126
    }
127

128
    // Use median time difference for robustness
129
    std::sort(time_diffs.begin(), time_diffs.end());
1✔
130
    double median_dt = time_diffs[time_diffs.size() / 2];
1✔
131

132
    // Assume time indices are in units that give sampling rate of 1/dt
133
    // This is a heuristic - users should specify sampling rate explicitly
134
    return 1.0 / median_dt;
1✔
135
}
1✔
136

137
// ========== Filter Creation Helpers ==========
138

139
namespace {
140

141
template<int Order>
142
class FilterVariant {
143
public:
144
    // Butterworth filters
145
    std::optional<Iir::Butterworth::LowPass<Order>> butterworth_lowpass;
146
    std::optional<Iir::Butterworth::HighPass<Order>> butterworth_highpass;
147
    std::optional<Iir::Butterworth::BandPass<Order>> butterworth_bandpass;
148
    std::optional<Iir::Butterworth::BandStop<Order>> butterworth_bandstop;
149

150
    // Chebyshev I filters
151
    std::optional<Iir::ChebyshevI::LowPass<Order>> chebyshev1_lowpass;
152
    std::optional<Iir::ChebyshevI::HighPass<Order>> chebyshev1_highpass;
153
    std::optional<Iir::ChebyshevI::BandPass<Order>> chebyshev1_bandpass;
154
    std::optional<Iir::ChebyshevI::BandStop<Order>> chebyshev1_bandstop;
155

156
    // Chebyshev II filters
157
    std::optional<Iir::ChebyshevII::LowPass<Order>> chebyshev2_lowpass;
158
    std::optional<Iir::ChebyshevII::HighPass<Order>> chebyshev2_highpass;
159
    std::optional<Iir::ChebyshevII::BandPass<Order>> chebyshev2_bandpass;
160
    std::optional<Iir::ChebyshevII::BandStop<Order>> chebyshev2_bandstop;
161

162
    void setupFilter(FilterOptions const & options) {
24✔
163
        try {
164
            switch (options.type) {
24✔
165
                case FilterType::Butterworth:
22✔
166
                    setupButterworthFilter(options);
22✔
167
                    break;
22✔
168
                case FilterType::ChebyshevI:
1✔
169
                    setupChebyshevIFilter(options);
1✔
170
                    break;
1✔
171
                case FilterType::ChebyshevII:
1✔
172
                    setupChebyshevIIFilter(options);
1✔
173
                    break;
1✔
UNCOV
174
                case FilterType::RBJ:
×
175
                    // RBJ filters are only 2nd order, handled separately
UNCOV
176
                    break;
×
177
            }
178
        } catch (std::exception const & e) {
×
UNCOV
179
            throw std::runtime_error("Filter setup failed: " + std::string(e.what()));
×
180
        }
181
    }
24✔
182

183
    float filter(float input) {
20,450✔
184
        // Apply the active filter
185
        if (butterworth_lowpass) return static_cast<float>(butterworth_lowpass->filter(input));
20,450✔
186
        if (butterworth_highpass) return static_cast<float>(butterworth_highpass->filter(input));
10,734✔
187
        if (butterworth_bandpass) return static_cast<float>(butterworth_bandpass->filter(input));
6,534✔
188
        if (butterworth_bandstop) return static_cast<float>(butterworth_bandstop->filter(input));
200✔
189

190
        if (chebyshev1_lowpass) return static_cast<float>(chebyshev1_lowpass->filter(input));
200✔
191
        if (chebyshev1_highpass) return static_cast<float>(chebyshev1_highpass->filter(input));
100✔
192
        if (chebyshev1_bandpass) return static_cast<float>(chebyshev1_bandpass->filter(input));
100✔
193
        if (chebyshev1_bandstop) return static_cast<float>(chebyshev1_bandstop->filter(input));
100✔
194

195
        if (chebyshev2_lowpass) return static_cast<float>(chebyshev2_lowpass->filter(input));
100✔
196
        if (chebyshev2_highpass) return static_cast<float>(chebyshev2_highpass->filter(input));
×
197
        if (chebyshev2_bandpass) return static_cast<float>(chebyshev2_bandpass->filter(input));
×
UNCOV
198
        if (chebyshev2_bandstop) return static_cast<float>(chebyshev2_bandstop->filter(input));
×
199

UNCOV
200
        return input;// No filter active
×
201
    }
202

203
    void reset() {
48✔
204
        if (butterworth_lowpass) butterworth_lowpass->reset();
48✔
205
        if (butterworth_highpass) butterworth_highpass->reset();
48✔
206
        if (butterworth_bandpass) butterworth_bandpass->reset();
48✔
207
        if (butterworth_bandstop) butterworth_bandstop->reset();
48✔
208

209
        if (chebyshev1_lowpass) chebyshev1_lowpass->reset();
48✔
210
        if (chebyshev1_highpass) chebyshev1_highpass->reset();
48✔
211
        if (chebyshev1_bandpass) chebyshev1_bandpass->reset();
48✔
212
        if (chebyshev1_bandstop) chebyshev1_bandstop->reset();
48✔
213

214
        if (chebyshev2_lowpass) chebyshev2_lowpass->reset();
48✔
215
        if (chebyshev2_highpass) chebyshev2_highpass->reset();
48✔
216
        if (chebyshev2_bandpass) chebyshev2_bandpass->reset();
48✔
217
        if (chebyshev2_bandstop) chebyshev2_bandstop->reset();
48✔
218
    }
48✔
219

220
private:
221
    void setupButterworthFilter(FilterOptions const & options) {
22✔
222
        switch (options.response) {
22✔
223
            case FilterResponse::LowPass:
17✔
224
                butterworth_lowpass.emplace();
17✔
225
                butterworth_lowpass->setup(options.order, options.sampling_rate_hz, options.cutoff_frequency_hz);
17✔
226
                break;
17✔
227
            case FilterResponse::HighPass:
2✔
228
                butterworth_highpass.emplace();
2✔
229
                butterworth_highpass->setup(options.order, options.sampling_rate_hz, options.cutoff_frequency_hz);
2✔
230
                break;
2✔
231
            case FilterResponse::BandPass: {
3✔
232
                butterworth_bandpass.emplace();
3✔
233
                // Calculate center frequency and bandwidth from low and high cutoffs
234
                double center_freq = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
3✔
235
                double bandwidth = options.high_cutoff_hz - options.cutoff_frequency_hz;
3✔
236
                butterworth_bandpass->setup(options.order, options.sampling_rate_hz, center_freq, bandwidth);
3✔
237
                break;
3✔
238
            }
239
            case FilterResponse::BandStop: {
×
UNCOV
240
                butterworth_bandstop.emplace();
×
241
                // Calculate center frequency and bandwidth from low and high cutoffs  
242
                double center_freq_stop = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
UNCOV
243
                double bandwidth_stop = options.high_cutoff_hz - options.cutoff_frequency_hz;
×
244
                // Note: BandStop might not have the 4-parameter setup method, using 3-parameter version
245
                butterworth_bandstop->setup(options.sampling_rate_hz, center_freq_stop, bandwidth_stop);
×
UNCOV
246
                break;
×
247
            }
248
            default:
×
UNCOV
249
                throw std::runtime_error("Unsupported Butterworth filter response type");
×
250
        }
251
    }
22✔
252

253
    void setupChebyshevIFilter(FilterOptions const & options) {
1✔
254
        switch (options.response) {
1✔
255
            case FilterResponse::LowPass:
1✔
256
                chebyshev1_lowpass.emplace();
1✔
257
                chebyshev1_lowpass->setup(options.order, options.sampling_rate_hz, options.cutoff_frequency_hz, options.passband_ripple_db);
1✔
258
                break;
1✔
259
            case FilterResponse::HighPass:
×
260
                chebyshev1_highpass.emplace();
×
261
                chebyshev1_highpass->setup(options.order, options.sampling_rate_hz, options.cutoff_frequency_hz, options.passband_ripple_db);
×
262
                break;
×
263
            case FilterResponse::BandPass: {
×
UNCOV
264
                chebyshev1_bandpass.emplace();
×
265
                // Calculate center frequency and bandwidth from low and high cutoffs
266
                double center_freq = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
267
                double bandwidth = options.high_cutoff_hz - options.cutoff_frequency_hz;
×
268
                chebyshev1_bandpass->setup(options.order, options.sampling_rate_hz, center_freq, bandwidth, options.passband_ripple_db);
×
UNCOV
269
                break;
×
270
            }
271
            case FilterResponse::BandStop: {
×
UNCOV
272
                chebyshev1_bandstop.emplace();
×
273
                // Calculate center frequency and bandwidth from low and high cutoffs
274
                double center_freq_stop = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
UNCOV
275
                double bandwidth_stop = options.high_cutoff_hz - options.cutoff_frequency_hz;
×
276
                // Assume similar interface to BandPass
277
                chebyshev1_bandstop->setup(options.order, options.sampling_rate_hz, center_freq_stop, bandwidth_stop, options.passband_ripple_db);
×
UNCOV
278
                break;
×
279
            }
280
            default:
×
UNCOV
281
                throw std::runtime_error("Unsupported Chebyshev I filter response type");
×
282
        }
283
    }
1✔
284

285
    void setupChebyshevIIFilter(FilterOptions const & options) {
1✔
286
        switch (options.response) {
1✔
287
            case FilterResponse::LowPass:
1✔
288
                chebyshev2_lowpass.emplace();
1✔
289
                chebyshev2_lowpass->setup(options.order, options.sampling_rate_hz, options.cutoff_frequency_hz, options.stopband_ripple_db);
1✔
290
                break;
1✔
291
            case FilterResponse::HighPass:
×
292
                chebyshev2_highpass.emplace();
×
293
                chebyshev2_highpass->setup(options.order, options.sampling_rate_hz, options.cutoff_frequency_hz, options.stopband_ripple_db);
×
294
                break;
×
295
            case FilterResponse::BandPass: {
×
UNCOV
296
                chebyshev2_bandpass.emplace();
×
297
                // Calculate center frequency and bandwidth from low and high cutoffs
298
                double center_freq = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
299
                double bandwidth = options.high_cutoff_hz - options.cutoff_frequency_hz;
×
300
                chebyshev2_bandpass->setup(options.order, options.sampling_rate_hz, center_freq, bandwidth, options.stopband_ripple_db);
×
UNCOV
301
                break;
×
302
            }
303
            case FilterResponse::BandStop: {
×
UNCOV
304
                chebyshev2_bandstop.emplace();
×
305
                // Calculate center frequency and bandwidth from low and high cutoffs
306
                double center_freq_stop = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
307
                double bandwidth_stop = options.high_cutoff_hz - options.cutoff_frequency_hz;
×
308
                chebyshev2_bandstop->setup(options.order, options.sampling_rate_hz, center_freq_stop, bandwidth_stop, options.stopband_ripple_db);
×
UNCOV
309
                break;
×
310
            }
311
            default:
×
UNCOV
312
                throw std::runtime_error("Unsupported Chebyshev II filter response type");
×
313
        }
314
    }
1✔
315
};
316

317
// RBJ Filter (always 2nd order)
318
class RBJFilter {
319
private:
320
    std::optional<Iir::RBJ::LowPass> lowpass;
321
    std::optional<Iir::RBJ::HighPass> highpass;
322
    std::optional<Iir::RBJ::BandPass2> bandpass;
323
    std::optional<Iir::RBJ::BandStop> bandstop;
324

325
public:
326
    void setupFilter(FilterOptions const & options) {
3✔
327
        try {
328
            switch (options.response) {
3✔
329
                case FilterResponse::LowPass:
1✔
330
                    lowpass.emplace();
1✔
331
                    lowpass->setup(options.sampling_rate_hz, options.cutoff_frequency_hz, options.q_factor);
1✔
332
                    break;
1✔
333
                case FilterResponse::HighPass:
×
334
                    highpass.emplace();
×
335
                    highpass->setup(options.sampling_rate_hz, options.cutoff_frequency_hz, options.q_factor);
×
336
                    break;
×
337
                case FilterResponse::BandPass: {
×
338
                    bandpass.emplace();
×
UNCOV
339
                    if (options.high_cutoff_hz > options.cutoff_frequency_hz) {
×
340
                        // Calculate center frequency and bandwidth in octaves from low and high cutoffs
341
                        double center_freq = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
342
                        double bandwidth_octaves = std::log2(options.high_cutoff_hz / options.cutoff_frequency_hz);
×
UNCOV
343
                        bandpass->setup(options.sampling_rate_hz, center_freq, bandwidth_octaves);
×
344
                    } else {
345
                        // Use center frequency and Q factor
346
                        // Convert Q factor to bandwidth in octaves: BW ≈ 1.44 / Q
347
                        double center_freq = options.cutoff_frequency_hz;
×
348
                        double bandwidth_octaves = 1.44 / options.q_factor;
×
UNCOV
349
                        bandpass->setup(options.sampling_rate_hz, center_freq, bandwidth_octaves);
×
350
                    }
UNCOV
351
                    break;
×
352
                }
353
                case FilterResponse::BandStop: {
2✔
354
                    bandstop.emplace();
2✔
355
                    if (options.high_cutoff_hz > options.cutoff_frequency_hz) {
2✔
356
                        // Calculate center frequency and bandwidth in octaves from low and high cutoffs
357
                        double center_freq = (options.cutoff_frequency_hz + options.high_cutoff_hz) / 2.0;
×
358
                        double bandwidth_octaves = std::log2(options.high_cutoff_hz / options.cutoff_frequency_hz);
×
UNCOV
359
                        bandstop->setup(options.sampling_rate_hz, center_freq, bandwidth_octaves);
×
360
                    } else {
361
                        // Notch filter: use center frequency and Q factor
362
                        // Convert Q factor to bandwidth in octaves: BW ≈ 1.44 / Q
363
                        double center_freq = options.cutoff_frequency_hz;
2✔
364
                        double bandwidth_octaves = 1.44 / options.q_factor;
2✔
365
                        bandstop->setup(options.sampling_rate_hz, center_freq, bandwidth_octaves);
2✔
366
                    }
367
                    break;
2✔
368
                }
369
                default:
×
UNCOV
370
                    throw std::runtime_error("Unsupported RBJ filter response type");
×
371
            }
372
        } catch (std::exception const & e) {
×
373
            throw std::runtime_error("RBJ filter setup failed: " + std::string(e.what()));
×
UNCOV
374
        }
×
375
    }
3✔
376

377
    float filter(float input) {
4,300✔
378
        if (lowpass) return static_cast<float>(lowpass->filter(input));
4,300✔
379
        if (highpass) return static_cast<float>(highpass->filter(input));
4,200✔
380
        if (bandpass) return static_cast<float>(bandpass->filter(input));
4,200✔
381
        if (bandstop) return static_cast<float>(bandstop->filter(input));
4,200✔
UNCOV
382
        return input;
×
383
    }
384

385
    void reset() {
6✔
386
        if (lowpass) lowpass->reset();
6✔
387
        if (highpass) highpass->reset();
6✔
388
        if (bandpass) bandpass->reset();
6✔
389
        if (bandstop) bandstop->reset();
6✔
390
    }
6✔
391
};
392

393
// Generic filter interface
394
class IIRFilter {
395
private:
396
    FilterVariant<1> filter1;
397
    FilterVariant<2> filter2;
398
    FilterVariant<3> filter3;
399
    FilterVariant<4> filter4;
400
    FilterVariant<5> filter5;
401
    FilterVariant<6> filter6;
402
    FilterVariant<7> filter7;
403
    FilterVariant<8> filter8;
404
    RBJFilter rbj_filter;
405
    int active_order = 0;
406
    FilterType active_type = FilterType::Butterworth;
407

408
public:
409
    void setupFilter(FilterOptions const & options) {
27✔
410
        active_order = options.order;
27✔
411
        active_type = options.type;
27✔
412

413
        if (options.type == FilterType::RBJ) {
27✔
414
            rbj_filter.setupFilter(options);
3✔
415
            return;
3✔
416
        }
417

418
        // Setup the appropriate order filter
419
        switch (options.order) {
24✔
420
            case 1:
1✔
421
                filter1.setupFilter(options);
1✔
422
                break;
1✔
423
            case 2:
4✔
424
                filter2.setupFilter(options);
4✔
425
                break;
4✔
426
            case 3:
1✔
427
                filter3.setupFilter(options);
1✔
428
                break;
1✔
429
            case 4:
14✔
430
                filter4.setupFilter(options);
14✔
431
                break;
14✔
432
            case 5:
1✔
433
                filter5.setupFilter(options);
1✔
434
                break;
1✔
435
            case 6:
1✔
436
                filter6.setupFilter(options);
1✔
437
                break;
1✔
438
            case 7:
1✔
439
                filter7.setupFilter(options);
1✔
440
                break;
1✔
441
            case 8:
1✔
442
                filter8.setupFilter(options);
1✔
443
                break;
1✔
444
            default:
×
UNCOV
445
                throw std::runtime_error("Unsupported filter order: " + std::to_string(options.order));
×
446
        }
447
    }
448

449
    float filter(float input) {
24,750✔
450
        if (active_type == FilterType::RBJ) {
24,750✔
451
            return rbj_filter.filter(input);
4,300✔
452
        }
453

454
        switch (active_order) {
20,450✔
455
            case 1:
100✔
456
                return filter1.filter(input);
100✔
457
            case 2:
4,388✔
458
                return filter2.filter(input);
4,388✔
459
            case 3:
100✔
460
                return filter3.filter(input);
100✔
461
            case 4:
15,462✔
462
                return filter4.filter(input);
15,462✔
463
            case 5:
100✔
464
                return filter5.filter(input);
100✔
465
            case 6:
100✔
466
                return filter6.filter(input);
100✔
467
            case 7:
100✔
468
                return filter7.filter(input);
100✔
469
            case 8:
100✔
470
                return filter8.filter(input);
100✔
471
            default:
×
UNCOV
472
                return input;
×
473
        }
474
    }
475

476
    void reset() {
54✔
477
        if (active_type == FilterType::RBJ) {
54✔
478
            rbj_filter.reset();
6✔
479
            return;
6✔
480
        }
481

482
        switch (active_order) {
48✔
483
            case 1:
2✔
484
                filter1.reset();
2✔
485
                break;
2✔
486
            case 2:
8✔
487
                filter2.reset();
8✔
488
                break;
8✔
489
            case 3:
2✔
490
                filter3.reset();
2✔
491
                break;
2✔
492
            case 4:
28✔
493
                filter4.reset();
28✔
494
                break;
28✔
495
            case 5:
2✔
496
                filter5.reset();
2✔
497
                break;
2✔
498
            case 6:
2✔
499
                filter6.reset();
2✔
500
                break;
2✔
501
            case 7:
2✔
502
                filter7.reset();
2✔
503
                break;
2✔
504
            case 8:
2✔
505
                filter8.reset();
2✔
506
                break;
2✔
507
        }
508
    }
509
};
510

511
// Data interpolation for irregular sampling
UNCOV
512
std::vector<float> interpolateData(
×
513
        std::vector<float> const & data,
514
        std::vector<TimeFrameIndex> const & time_indices,
515
        InterpolationMethod method) {
516
    if (method == InterpolationMethod::None || data.size() != time_indices.size()) {
×
UNCOV
517
        return data;
×
518
    }
519

520
    // For now, return original data.
521
    // TODO: Implement interpolation for irregular sampling
UNCOV
522
    return data;
×
523
}
524

525
// Process data with potential gaps
526
struct DataSegment {
527
    std::vector<float> data;
528
    std::vector<TimeFrameIndex> time_indices;
529
    size_t start_idx;
530
    size_t end_idx;
531
};
532

533
std::vector<DataSegment> segmentData(
27✔
534
        std::vector<float> const & data,
535
        std::vector<TimeFrameIndex> const & time_indices,
536
        size_t max_gap_samples) {
537
    std::vector<DataSegment> segments;
27✔
538

539
    if (data.empty() || data.size() != time_indices.size()) {
27✔
UNCOV
540
        return segments;
×
541
    }
542

543
    size_t segment_start = 0;
27✔
544

545
    for (size_t i = 1; i < time_indices.size(); ++i) {
12,375✔
546
        int64_t gap = time_indices[i].getValue() - time_indices[i - 1].getValue();
12,348✔
547

548
        if (static_cast<size_t>(gap) > max_gap_samples) {
12,348✔
549
            // End current segment
550
            DataSegment segment;
×
551
            segment.start_idx = segment_start;
×
552
            segment.end_idx = i;
×
553
            segment.data.assign(data.begin() + segment_start, data.begin() + i);
×
554
            segment.time_indices.assign(time_indices.begin() + segment_start, time_indices.begin() + i);
×
UNCOV
555
            segments.push_back(segment);
×
556

557
            segment_start = i;
×
UNCOV
558
        }
×
559
    }
560

561
    // Add final segment
562
    if (segment_start < data.size()) {
27✔
563
        DataSegment segment;
27✔
564
        segment.start_idx = segment_start;
27✔
565
        segment.end_idx = data.size();
27✔
566
        segment.data.assign(data.begin() + segment_start, data.end());
27✔
567
        segment.time_indices.assign(time_indices.begin() + segment_start, time_indices.end());
27✔
568
        segments.push_back(segment);
27✔
569
    }
27✔
570

571
    return segments;
27✔
UNCOV
572
}
×
573

574
}// anonymous namespace
575

576
// ========== Main Filtering Functions ==========
577

578
FilterResult filterAnalogTimeSeries(
28✔
579
        AnalogTimeSeries const * analog_time_series,
580
        TimeFrameIndex start_time,
581
        TimeFrameIndex end_time,
582
        FilterOptions const & options) {
583
    FilterResult result;
28✔
584

585
    // Validate inputs
586
    if (!analog_time_series) {
28✔
587
        result.error_message = "Input AnalogTimeSeries is null";
×
UNCOV
588
        return result;
×
589
    }
590

591
    if (!options.isValid()) {
28✔
592
        result.error_message = "Invalid filter options: " + options.getValidationError();
1✔
593
        return result;
1✔
594
    }
595

596
    try {
597
        // Extract data from the specified time range
598
        auto data_span = analog_time_series->getDataInTimeFrameIndexRange(start_time, end_time);
27✔
599
        auto time_value_range = analog_time_series->getTimeValueRangeInTimeFrameIndexRange(start_time, end_time);
27✔
600

601
        if (data_span.empty()) {
27✔
602
            result.error_message = "No data found in specified time range";
×
UNCOV
603
            return result;
×
604
        }
605

606
        // Convert span to vector for processing
607
        std::vector<float> input_data(data_span.begin(), data_span.end());
81✔
608
        std::vector<TimeFrameIndex> input_times;
27✔
609
        input_times.reserve(time_value_range.size());
27✔
610

611
        for (auto const & point: time_value_range) {
12,402✔
612
            input_times.push_back(point.time_frame_index);
12,375✔
613
        }
614

615
        // Handle irregular sampling if requested
616
        if (options.interpolation != InterpolationMethod::None) {
27✔
UNCOV
617
            input_data = interpolateData(input_data, input_times, options.interpolation);
×
618
        }
619

620
        // Segment data for gaps
621
        auto segments = segmentData(input_data, input_times, options.max_gap_samples);
27✔
622

623
        std::vector<float> filtered_data;
27✔
624
        std::vector<TimeFrameIndex> filtered_times;
27✔
625

626
        for (auto & segment: segments) {
54✔
627
            if (segment.data.size() < 2) {
27✔
628
                // Skip segments that are too small
UNCOV
629
                continue;
×
630
            }
631

632
            // Create and setup filter
633
            IIRFilter filter;
27✔
634
            filter.setupFilter(options);
27✔
635

636
            std::vector<float> segment_output;
27✔
637
            segment_output.reserve(segment.data.size());
27✔
638

639
            if (options.zero_phase) {
27✔
640
                // Forward pass
641
                std::vector<float> forward_output;
27✔
642
                forward_output.reserve(segment.data.size());
27✔
643

644
                filter.reset();
27✔
645
                for (float sample: segment.data) {
12,402✔
646
                    forward_output.push_back(filter.filter(sample));
12,375✔
647
                }
648

649
                // Backward pass
650
                // First reverse the forward-filtered data
651
                std::reverse(forward_output.begin(), forward_output.end());
27✔
652
                
653
                filter.reset();
27✔
654
                segment_output.resize(segment.data.size());
27✔
655
                
656
                // Apply filter to reversed data
657
                for (size_t i = 0; i < forward_output.size(); ++i) {
12,402✔
658
                    segment_output[i] = filter.filter(forward_output[i]);
12,375✔
659
                }
660
                
661
                // Reverse the result to get correct time ordering
662
                std::reverse(segment_output.begin(), segment_output.end());
27✔
663
            } else {
27✔
664
                // Single forward pass
UNCOV
665
                filter.reset();
×
UNCOV
666
                for (float sample: segment.data) {
×
UNCOV
667
                    segment_output.push_back(filter.filter(sample));
×
668
                }
669
            }
670

671
            // Append segment results
672
            filtered_data.insert(filtered_data.end(), segment_output.begin(), segment_output.end());
27✔
673
            filtered_times.insert(filtered_times.end(), segment.time_indices.begin(), segment.time_indices.end());
27✔
674

675
            result.samples_processed += segment_output.size();
27✔
676
        }
27✔
677

678
        result.segments_processed = segments.size();
27✔
679

680
        if (filtered_data.empty()) {
27✔
681
            result.error_message = "No data could be processed";
×
UNCOV
682
            return result;
×
683
        }
684

685
        // Create new AnalogTimeSeries with filtered data
686
        result.filtered_data = std::make_shared<AnalogTimeSeries>(
81✔
687
                std::move(filtered_data),
27✔
688
                std::move(filtered_times));
54✔
689

690
        result.success = true;
27✔
691

692
    } catch (std::exception const & e) {
27✔
693
        result.error_message = "Filtering failed: " + std::string(e.what());
×
UNCOV
694
    }
×
695

696
    return result;
27✔
UNCOV
697
}
×
698

699
FilterResult filterAnalogTimeSeries(
28✔
700
        AnalogTimeSeries const * analog_time_series,
701
        FilterOptions const & options) {
702
    if (!analog_time_series) {
28✔
703
        FilterResult result;
1✔
704
        result.error_message = "Input AnalogTimeSeries is null";
1✔
705
        return result;
1✔
706
    }
1✔
707

708
    // Get the full time range
709
    auto time_series = analog_time_series->getTimeSeries();
27✔
710
    if (time_series.empty()) {
27✔
711
        FilterResult result;
×
712
        result.error_message = "AnalogTimeSeries contains no data";
×
713
        return result;
×
UNCOV
714
    }
×
715

716
    TimeFrameIndex start_time = time_series.front();
27✔
717
    TimeFrameIndex end_time = time_series.back();
27✔
718

719
    return filterAnalogTimeSeries(analog_time_series, start_time, end_time, options);
27✔
720
}
27✔
721

722
// ========== Filter Defaults ==========
723

724
namespace FilterDefaults {
725

726
FilterOptions lowpass(double cutoff_hz, double sampling_rate_hz, int order) {
9✔
727
    FilterOptions options;
9✔
728
    options.type = FilterType::Butterworth;
9✔
729
    options.response = FilterResponse::LowPass;
9✔
730
    options.cutoff_frequency_hz = cutoff_hz;
9✔
731
    options.sampling_rate_hz = sampling_rate_hz;
9✔
732
    options.order = order;
9✔
733
    return options;
9✔
734
}
735

736
FilterOptions highpass(double cutoff_hz, double sampling_rate_hz, int order) {
3✔
737
    FilterOptions options;
3✔
738
    options.type = FilterType::Butterworth;
3✔
739
    options.response = FilterResponse::HighPass;
3✔
740
    options.cutoff_frequency_hz = cutoff_hz;
3✔
741
    options.sampling_rate_hz = sampling_rate_hz;
3✔
742
    options.order = order;
3✔
743
    return options;
3✔
744
}
745

746
FilterOptions bandpass(double low_cutoff_hz, double high_cutoff_hz, double sampling_rate_hz, int order) {
3✔
747
    FilterOptions options;
3✔
748
    options.type = FilterType::Butterworth;
3✔
749
    options.response = FilterResponse::BandPass;
3✔
750
    options.cutoff_frequency_hz = low_cutoff_hz;
3✔
751
    options.high_cutoff_hz = high_cutoff_hz;
3✔
752
    options.sampling_rate_hz = sampling_rate_hz;
3✔
753
    options.order = order;
3✔
754
    return options;
3✔
755
}
756

757
FilterOptions notch(double center_freq_hz, double sampling_rate_hz, double q_factor) {
3✔
758
    FilterOptions options;
3✔
759
    options.type = FilterType::RBJ;
3✔
760
    options.response = FilterResponse::BandStop;
3✔
761
    options.cutoff_frequency_hz = center_freq_hz;
3✔
762
    options.high_cutoff_hz = center_freq_hz;  // Set equal to center frequency to force Q-factor path
3✔
763
    options.sampling_rate_hz = sampling_rate_hz;
3✔
764
    options.q_factor = q_factor;
3✔
765
    options.order = 2;  // RBJ filters are always 2nd order
3✔
766
    return options;
3✔
767
}
768

769
}// namespace FilterDefaults
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc