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

paulmthompson / WhiskerToolbox / 16055270388

03 Jul 2025 04:02PM UTC coverage: 73.02% (+0.6%) from 72.408%
16055270388

push

github

paulmthompson
filter interface with iir added

713 of 840 new or added lines in 2 files covered. (84.88%)

12501 of 17120 relevant lines covered (73.02%)

1055.13 hits per line

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

73.73
/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 {
47✔
14
    return getValidationError().empty();
47✔
15
}
16

17
std::string FilterOptions::getValidationError() const {
59✔
18
    if (order < 1 || order > max_filter_order) {
59✔
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) {
53✔
23
        return "Sampling rate must be positive";
12✔
24
    }
25

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

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

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

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

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

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

69
    return "";// Valid
111✔
70
}
71

72
// ========== Sampling Rate Estimation ==========
73

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

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

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

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

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

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

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

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

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

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

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

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

136
// ========== Filter Creation Helpers ==========
137

138
namespace {
139

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

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

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

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

182
    float filter(float input) {
1,191✔
183
        // Apply the active filter
184
        if (butterworth_lowpass) return static_cast<float>(butterworth_lowpass->filter(input));
1,191✔
185
        if (butterworth_highpass) return static_cast<float>(butterworth_highpass->filter(input));
300✔
186
        if (butterworth_bandpass) return static_cast<float>(butterworth_bandpass->filter(input));
200✔
187
        if (butterworth_bandstop) return static_cast<float>(butterworth_bandstop->filter(input));
100✔
188

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

194
        if (chebyshev2_lowpass) return static_cast<float>(chebyshev2_lowpass->filter(input));
50✔
NEW
195
        if (chebyshev2_highpass) return static_cast<float>(chebyshev2_highpass->filter(input));
×
NEW
196
        if (chebyshev2_bandpass) return static_cast<float>(chebyshev2_bandpass->filter(input));
×
NEW
197
        if (chebyshev2_bandstop) return static_cast<float>(chebyshev2_bandstop->filter(input));
×
198

NEW
199
        return input;// No filter active
×
200
    }
201

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

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

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

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

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

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

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

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

376
    float filter(float input) {
150✔
377
        if (lowpass) return static_cast<float>(lowpass->filter(input));
150✔
378
        if (highpass) return static_cast<float>(highpass->filter(input));
100✔
379
        if (bandpass) return static_cast<float>(bandpass->filter(input));
100✔
380
        if (bandstop) return static_cast<float>(bandstop->filter(input));
100✔
NEW
381
        return input;
×
382
    }
383

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

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

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

412
        if (options.type == FilterType::RBJ) {
20✔
413
            rbj_filter.setupFilter(options);
2✔
414
            return;
2✔
415
        }
416

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

448
    float filter(float input) {
1,341✔
449
        if (active_type == FilterType::RBJ) {
1,341✔
450
            return rbj_filter.filter(input);
150✔
451
        }
452

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

475
    void reset() {
21✔
476
        if (active_type == FilterType::RBJ) {
21✔
477
            rbj_filter.reset();
2✔
478
            return;
2✔
479
        }
480

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

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

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

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

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

538
    if (data.empty() || data.size() != time_indices.size()) {
20✔
NEW
539
        return segments;
×
540
    }
541

542
    size_t segment_start = 0;
20✔
543

544
    for (size_t i = 1; i < time_indices.size(); ++i) {
1,241✔
545
        int64_t gap = time_indices[i].getValue() - time_indices[i - 1].getValue();
1,221✔
546

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

NEW
556
            segment_start = i;
×
NEW
557
        }
×
558
    }
559

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

570
    return segments;
20✔
NEW
571
}
×
572

573
}// anonymous namespace
574

575
// ========== Main Filtering Functions ==========
576

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

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

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

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

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

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

610
        for (auto const & point: time_value_range) {
1,261✔
611
            input_times.push_back(point.time_frame_index);
1,241✔
612
        }
613

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

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

622
        std::vector<float> filtered_data;
20✔
623
        std::vector<TimeFrameIndex> filtered_times;
20✔
624

625
        for (auto & segment: segments) {
40✔
626
            if (segment.data.size() < 2) {
20✔
627
                // Skip segments that are too small
NEW
628
                continue;
×
629
            }
630

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

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

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

643
                filter.reset();
1✔
644
                for (float sample: segment.data) {
101✔
645
                    forward_output.push_back(filter.filter(sample));
100✔
646
                }
647

648
                // Backward pass
649
                filter.reset();
1✔
650
                segment_output.resize(segment.data.size());
1✔
651

652
                for (int i = static_cast<int>(forward_output.size()) - 1; i >= 0; --i) {
101✔
653
                    segment_output[i] = filter.filter(forward_output[i]);
100✔
654
                }
655

656
                // Reverse the result to correct time order
657
                std::reverse(segment_output.begin(), segment_output.end());
1✔
658
            } else {
1✔
659
                // Single forward pass
660
                filter.reset();
19✔
661
                for (float sample: segment.data) {
1,160✔
662
                    segment_output.push_back(filter.filter(sample));
1,141✔
663
                }
664
            }
665

666
            // Append segment results
667
            filtered_data.insert(filtered_data.end(), segment_output.begin(), segment_output.end());
20✔
668
            filtered_times.insert(filtered_times.end(), segment.time_indices.begin(), segment.time_indices.end());
20✔
669

670
            result.samples_processed += segment_output.size();
20✔
671
        }
20✔
672

673
        result.segments_processed = segments.size();
20✔
674

675
        if (filtered_data.empty()) {
20✔
NEW
676
            result.error_message = "No data could be processed";
×
NEW
677
            return result;
×
678
        }
679

680
        // Create new AnalogTimeSeries with filtered data
681
        result.filtered_data = std::make_shared<AnalogTimeSeries>(
60✔
682
                std::move(filtered_data),
20✔
683
                std::move(filtered_times));
40✔
684

685
        result.success = true;
20✔
686

687
    } catch (std::exception const & e) {
20✔
NEW
688
        result.error_message = "Filtering failed: " + std::string(e.what());
×
NEW
689
    }
×
690

691
    return result;
20✔
NEW
692
}
×
693

694
FilterResult filterAnalogTimeSeries(
21✔
695
        AnalogTimeSeries const * analog_time_series,
696
        FilterOptions const & options) {
697
    if (!analog_time_series) {
21✔
698
        FilterResult result;
1✔
699
        result.error_message = "Input AnalogTimeSeries is null";
1✔
700
        return result;
1✔
701
    }
1✔
702

703
    // Get the full time range
704
    auto time_series = analog_time_series->getTimeSeries();
20✔
705
    if (time_series.empty()) {
20✔
NEW
706
        FilterResult result;
×
NEW
707
        result.error_message = "AnalogTimeSeries contains no data";
×
NEW
708
        return result;
×
NEW
709
    }
×
710

711
    TimeFrameIndex start_time = time_series.front();
20✔
712
    TimeFrameIndex end_time = time_series.back();
20✔
713

714
    return filterAnalogTimeSeries(analog_time_series, start_time, end_time, options);
20✔
715
}
20✔
716

717
// ========== Filter Defaults ==========
718

719
namespace FilterDefaults {
720

721
FilterOptions lowpass(double cutoff_hz, double sampling_rate_hz, int order) {
6✔
722
    FilterOptions options;
6✔
723
    options.type = FilterType::Butterworth;
6✔
724
    options.response = FilterResponse::LowPass;
6✔
725
    options.order = order;
6✔
726
    options.sampling_rate_hz = sampling_rate_hz;
6✔
727
    options.cutoff_frequency_hz = cutoff_hz;
6✔
728
    return options;
6✔
729
}
730

731
FilterOptions highpass(double cutoff_hz, double sampling_rate_hz, int order) {
2✔
732
    FilterOptions options;
2✔
733
    options.type = FilterType::Butterworth;
2✔
734
    options.response = FilterResponse::HighPass;
2✔
735
    options.order = order;
2✔
736
    options.sampling_rate_hz = sampling_rate_hz;
2✔
737
    options.cutoff_frequency_hz = cutoff_hz;
2✔
738
    return options;
2✔
739
}
740

741
FilterOptions bandpass(double low_hz, double high_hz, double sampling_rate_hz, int order) {
2✔
742
    FilterOptions options;
2✔
743
    options.type = FilterType::Butterworth;
2✔
744
    options.response = FilterResponse::BandPass;
2✔
745
    options.order = order;
2✔
746
    options.sampling_rate_hz = sampling_rate_hz;
2✔
747
    options.cutoff_frequency_hz = low_hz;
2✔
748
    options.high_cutoff_hz = high_hz;
2✔
749
    return options;
2✔
750
}
751

752
FilterOptions notch(double center_hz, double sampling_rate_hz, double q_factor) {
2✔
753
    FilterOptions options;
2✔
754
    options.type = FilterType::RBJ;
2✔
755
    options.response = FilterResponse::BandStop;
2✔
756
    options.order = 2;// RBJ filters are always 2nd order
2✔
757
    options.sampling_rate_hz = sampling_rate_hz;
2✔
758
    options.cutoff_frequency_hz = center_hz;
2✔
759
    options.q_factor = q_factor;
2✔
760
    return options;
2✔
761
}
762

763
}// 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