• 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

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

4
#include <catch2/catch_test_macros.hpp>
5
#include <cmath>
6
#include <iostream>
7
#include <stdexcept>
8
#include <vector>
9

10
TEST_CASE("FilterOptions validation", "[filter][validation]") {
11✔
11
    SECTION("Valid default options") {
11✔
12
        FilterOptions options;
1✔
13
        REQUIRE(options.isValid());
1✔
14
        CHECK(options.getValidationError().empty());
1✔
15
    }
11✔
16

17
    SECTION("Invalid filter order - too low") {
11✔
18
        FilterOptions options;
1✔
19
        options.order = 0;
1✔
20
        REQUIRE_FALSE(options.isValid());
1✔
21
        CHECK(options.getValidationError().find("Filter order must be between") != std::string::npos);
1✔
22
    }
11✔
23

24
    SECTION("Invalid filter order - too high") {
11✔
25
        FilterOptions options;
1✔
26
        options.order = max_filter_order + 1;
1✔
27
        REQUIRE_FALSE(options.isValid());
1✔
28
        CHECK(options.getValidationError().find("Filter order must be between") != std::string::npos);
1✔
29
    }
11✔
30

31
    SECTION("Invalid sampling rate - negative") {
11✔
32
        FilterOptions options;
1✔
33
        options.sampling_rate_hz = -100.0;
1✔
34
        REQUIRE_FALSE(options.isValid());
1✔
35
        CHECK(options.getValidationError().find("Sampling rate must be positive") != std::string::npos);
1✔
36
    }
11✔
37

38
    SECTION("Invalid sampling rate - zero") {
11✔
39
        FilterOptions options;
1✔
40
        options.sampling_rate_hz = 0.0;
1✔
41
        REQUIRE_FALSE(options.isValid());
1✔
42
        CHECK(options.getValidationError().find("Sampling rate must be positive") != std::string::npos);
1✔
43
    }
11✔
44

45
    SECTION("Invalid cutoff frequency - negative") {
11✔
46
        FilterOptions options;
1✔
47
        options.cutoff_frequency_hz = -50.0;
1✔
48
        REQUIRE_FALSE(options.isValid());
1✔
49
        CHECK(options.getValidationError().find("Cutoff frequency must be positive") != std::string::npos);
1✔
50
    }
11✔
51

52
    SECTION("Invalid cutoff frequency - exceeds Nyquist") {
11✔
53
        FilterOptions options;
1✔
54
        options.sampling_rate_hz = 1000.0;
1✔
55
        options.cutoff_frequency_hz = 600.0;// > Nyquist (500 Hz)
1✔
56
        REQUIRE_FALSE(options.isValid());
1✔
57
        CHECK(options.getValidationError().find("Nyquist frequency") != std::string::npos);
1✔
58
    }
11✔
59

60
    SECTION("Invalid bandpass - high cutoff <= low cutoff") {
11✔
61
        FilterOptions options;
1✔
62
        options.response = FilterResponse::BandPass;
1✔
63
        options.sampling_rate_hz = 1000.0;
1✔
64
        options.cutoff_frequency_hz = 200.0;
1✔
65
        options.high_cutoff_hz = 150.0;// <= low cutoff
1✔
66
        REQUIRE_FALSE(options.isValid());
1✔
67
        CHECK(options.getValidationError().find("High cutoff frequency must be greater") != std::string::npos);
1✔
68
    }
11✔
69

70
    SECTION("Invalid Chebyshev I ripple") {
11✔
71
        FilterOptions options;
1✔
72
        options.type = FilterType::ChebyshevI;
1✔
73
        options.passband_ripple_db = -1.0;
1✔
74
        REQUIRE_FALSE(options.isValid());
1✔
75
        CHECK(options.getValidationError().find("Chebyshev I passband ripple") != std::string::npos);
1✔
76
    }
11✔
77

78
    SECTION("Invalid Chebyshev II ripple") {
11✔
79
        FilterOptions options;
1✔
80
        options.type = FilterType::ChebyshevII;
1✔
81
        options.stopband_ripple_db = -1.0;
1✔
82
        REQUIRE_FALSE(options.isValid());
1✔
83
        CHECK(options.getValidationError().find("Chebyshev II stopband ripple") != std::string::npos);
1✔
84
    }
11✔
85

86
    SECTION("Invalid RBJ Q factor") {
11✔
87
        FilterOptions options;
1✔
88
        options.type = FilterType::RBJ;
1✔
89
        options.q_factor = -1.0;
1✔
90
        REQUIRE_FALSE(options.isValid());
1✔
91
        CHECK(options.getValidationError().find("RBJ Q factor") != std::string::npos);
1✔
92
    }
11✔
93
}
11✔
94

95
TEST_CASE("FilterDefaults factory functions", "[filter][defaults]") {
4✔
96
    SECTION("Lowpass filter defaults") {
4✔
97
        auto options = FilterDefaults::lowpass(100.0, 1000.0, 4);
1✔
98
        REQUIRE(options.isValid());
1✔
99
        CHECK(options.type == FilterType::Butterworth);
1✔
100
        CHECK(options.response == FilterResponse::LowPass);
1✔
101
        CHECK(options.order == 4);
1✔
102
        CHECK(options.cutoff_frequency_hz == 100.0);
1✔
103
        CHECK(options.sampling_rate_hz == 1000.0);
1✔
104
    }
4✔
105

106
    SECTION("Highpass filter defaults") {
4✔
107
        auto options = FilterDefaults::highpass(50.0, 1000.0, 6);
1✔
108
        REQUIRE(options.isValid());
1✔
109
        CHECK(options.type == FilterType::Butterworth);
1✔
110
        CHECK(options.response == FilterResponse::HighPass);
1✔
111
        CHECK(options.order == 6);
1✔
112
        CHECK(options.cutoff_frequency_hz == 50.0);
1✔
113
        CHECK(options.sampling_rate_hz == 1000.0);
1✔
114
    }
4✔
115

116
    SECTION("Bandpass filter defaults") {
4✔
117
        auto options = FilterDefaults::bandpass(50.0, 150.0, 1000.0, 4);
1✔
118
        REQUIRE(options.isValid());
1✔
119
        CHECK(options.type == FilterType::Butterworth);
1✔
120
        CHECK(options.response == FilterResponse::BandPass);
1✔
121
        CHECK(options.order == 4);
1✔
122
        CHECK(options.cutoff_frequency_hz == 50.0);
1✔
123
        CHECK(options.high_cutoff_hz == 150.0);
1✔
124
        CHECK(options.sampling_rate_hz == 1000.0);
1✔
125
    }
4✔
126

127
    SECTION("Notch filter defaults") {
4✔
128
        auto options = FilterDefaults::notch(60.0, 1000.0, 10.0);
1✔
129
        REQUIRE(options.isValid());
1✔
130
        CHECK(options.type == FilterType::RBJ);
1✔
131
        CHECK(options.response == FilterResponse::BandStop);
1✔
132
        CHECK(options.order == 2);// RBJ filters are always 2nd order
1✔
133
        CHECK(options.cutoff_frequency_hz == 60.0);
1✔
134
        CHECK(options.q_factor == 10.0);
1✔
135
        CHECK(options.sampling_rate_hz == 1000.0);
1✔
136
    }
4✔
137
}
4✔
138

139
TEST_CASE("Sampling rate estimation", "[filter][sampling_rate]") {
4✔
140
    SECTION("Regular sampling with known rate") {
4✔
141
        size_t const num_samples = 100;
1✔
142
        double const expected_rate = 1000.0;
1✔
143

144
        std::vector<float> data(num_samples, 1.0f);
3✔
145
        std::vector<TimeFrameIndex> times;
1✔
146
        times.reserve(num_samples);
1✔
147

148
        // Create regularly spaced time indices
149
        for (size_t i = 0; i < num_samples; ++i) {
101✔
150
            times.push_back(TimeFrameIndex(static_cast<int64_t>(i)));
100✔
151
        }
152

153
        AnalogTimeSeries series(std::move(data), std::move(times));
1✔
154
        double estimated_rate = estimateSamplingRate(&series);
1✔
155

156
        // Since time indices are spaced by 1, estimated rate should be 1.0
157
        CHECK(estimated_rate == 1.0);
1✔
158
    }
5✔
159

160
    SECTION("Empty time series") {
4✔
161
        AnalogTimeSeries empty_series;
1✔
162
        double estimated_rate = estimateSamplingRate(&empty_series);
1✔
163
        CHECK(estimated_rate == 0.0);
1✔
164
    }
5✔
165

166
    SECTION("Single sample") {
4✔
167
        std::vector<float> data = {1.0f};
3✔
168
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0)};
3✔
169

170
        AnalogTimeSeries series(std::move(data), std::move(times));
1✔
171
        double estimated_rate = estimateSamplingRate(&series);
1✔
172
        CHECK(estimated_rate == 0.0);
1✔
173
    }
5✔
174

175
    SECTION("Null pointer handling") {
4✔
176
        double estimated_rate = estimateSamplingRate(nullptr);
1✔
177
        CHECK(estimated_rate == 0.0);
1✔
178
    }
4✔
179
}
4✔
180

181
TEST_CASE("Basic filtering functionality", "[filter][basic]") {
5✔
182
    // Create test data: sine wave with noise
183
    size_t const num_samples = 100;
5✔
184
    double const sampling_rate = 1000.0;
5✔
185
    double const signal_freq = 50.0;
5✔
186
    double const noise_freq = 200.0;
5✔
187
    
188
    std::vector<float> test_data;
5✔
189
    std::vector<TimeFrameIndex> test_times;
5✔
190
    
191
    test_data.reserve(num_samples);
5✔
192
    test_times.reserve(num_samples);
5✔
193
    
194
    // Generate test signal: sine wave + high-frequency noise
195
    for (size_t i = 0; i < num_samples; ++i) {
505✔
196
        double t = static_cast<double>(i) / sampling_rate;
500✔
197
        double signal = std::sin(2.0 * M_PI * signal_freq * t);
500✔
198
        double noise = 0.5 * std::sin(2.0 * M_PI * noise_freq * t);
500✔
199
        
200
        test_data.push_back(static_cast<float>(signal + noise));
500✔
201
        test_times.push_back(TimeFrameIndex(static_cast<int64_t>(i)));
500✔
202
    }
203
    
204
    auto analog_series = std::make_shared<AnalogTimeSeries>(test_data, test_times);
5✔
205
    
206
    SECTION("Low-pass filter") {
5✔
207
        auto options = FilterDefaults::lowpass(100.0, sampling_rate, 4);
1✔
208
        auto result = filterAnalogTimeSeries(analog_series.get(), options);
1✔
209

210
        REQUIRE(result.success);
1✔
211
        CHECK(result.error_message.empty());
1✔
212
        CHECK(result.samples_processed == num_samples);
1✔
213
        CHECK(result.segments_processed == 1);
1✔
214
        CHECK(result.filtered_data != nullptr);
1✔
215
        CHECK(result.filtered_data->getNumSamples() == num_samples);
1✔
216
    }
6✔
217

218
    SECTION("High-pass filter") {
5✔
219
        auto options = FilterDefaults::highpass(25.0, sampling_rate, 4);
1✔
220
        auto result = filterAnalogTimeSeries(analog_series.get(), options);
1✔
221

222
        REQUIRE(result.success);
1✔
223
        CHECK(result.error_message.empty());
1✔
224
        CHECK(result.samples_processed == num_samples);
1✔
225
        CHECK(result.segments_processed == 1);
1✔
226
        CHECK(result.filtered_data != nullptr);
1✔
227
        CHECK(result.filtered_data->getNumSamples() == num_samples);
1✔
228
    }
6✔
229

230
    SECTION("Band-pass filter") {
5✔
231
        auto options = FilterDefaults::bandpass(40.0, 60.0, sampling_rate, 4);
1✔
232
        auto result = filterAnalogTimeSeries(analog_series.get(), options);
1✔
233

234
        REQUIRE(result.success);
1✔
235
        CHECK(result.error_message.empty());
1✔
236
        CHECK(result.samples_processed == num_samples);
1✔
237
        CHECK(result.segments_processed == 1);
1✔
238
        CHECK(result.filtered_data != nullptr);
1✔
239
        CHECK(result.filtered_data->getNumSamples() == num_samples);
1✔
240
    }
6✔
241

242
    SECTION("Notch filter (RBJ)") {
5✔
243
        auto options = FilterDefaults::notch(200.0, sampling_rate, 10.0);
1✔
244
        auto result = filterAnalogTimeSeries(analog_series.get(), options);
1✔
245

246
        REQUIRE(result.success);
1✔
247
        CHECK(result.error_message.empty());
1✔
248
        CHECK(result.samples_processed == num_samples);
1✔
249
        CHECK(result.segments_processed == 1);
1✔
250
        CHECK(result.filtered_data != nullptr);
1✔
251
        CHECK(result.filtered_data->getNumSamples() == num_samples);
1✔
252
    }
6✔
253

254
    SECTION("Zero-phase filtering (filtfilt)") {
5✔
255
        auto options = FilterDefaults::lowpass(100.0, sampling_rate, 4);
1✔
256
        options.zero_phase = true;
1✔
257
        auto result = filterAnalogTimeSeries(analog_series.get(), options);
1✔
258

259
        REQUIRE(result.success);
1✔
260
        CHECK(result.error_message.empty());
1✔
261
        CHECK(result.samples_processed == num_samples);
1✔
262
        CHECK(result.segments_processed == 1);
1✔
263
        CHECK(result.filtered_data != nullptr);
1✔
264
        CHECK(result.filtered_data->getNumSamples() == num_samples);
1✔
265
    }
6✔
266
}
10✔
267

268
TEST_CASE("Filter type variations", "[filter][types]") {
4✔
269
    // Create simple test data
270
    size_t const num_samples = 50;
4✔
271
    std::vector<float> data(num_samples, 1.0f);
12✔
272
    std::vector<TimeFrameIndex> times;
4✔
273
    times.reserve(num_samples);
4✔
274

275
    for (size_t i = 0; i < num_samples; ++i) {
204✔
276
        times.push_back(TimeFrameIndex(static_cast<int64_t>(i)));
200✔
277
    }
278

279
    AnalogTimeSeries series(data, times);
4✔
280

281
    SECTION("Butterworth filter") {
4✔
282
        FilterOptions options;
1✔
283
        options.type = FilterType::Butterworth;
1✔
284
        options.response = FilterResponse::LowPass;
1✔
285
        options.order = 4;
1✔
286
        options.sampling_rate_hz = 1000.0;
1✔
287
        options.cutoff_frequency_hz = 100.0;
1✔
288

289
        auto result = filterAnalogTimeSeries(&series, options);
1✔
290
        REQUIRE(result.success);
1✔
291
        CHECK(result.filtered_data != nullptr);
1✔
292
    }
5✔
293

294
    SECTION("Chebyshev I filter") {
4✔
295
        FilterOptions options;
1✔
296
        options.type = FilterType::ChebyshevI;
1✔
297
        options.response = FilterResponse::LowPass;
1✔
298
        options.order = 4;
1✔
299
        options.sampling_rate_hz = 1000.0;
1✔
300
        options.cutoff_frequency_hz = 100.0;
1✔
301
        options.passband_ripple_db = 1.0;
1✔
302

303
        auto result = filterAnalogTimeSeries(&series, options);
1✔
304
        REQUIRE(result.success);
1✔
305
        CHECK(result.filtered_data != nullptr);
1✔
306
    }
5✔
307

308
    SECTION("Chebyshev II filter") {
4✔
309
        FilterOptions options;
1✔
310
        options.type = FilterType::ChebyshevII;
1✔
311
        options.response = FilterResponse::LowPass;
1✔
312
        options.order = 4;
1✔
313
        options.sampling_rate_hz = 1000.0;
1✔
314
        options.cutoff_frequency_hz = 100.0;
1✔
315
        options.stopband_ripple_db = 20.0;
1✔
316

317
        auto result = filterAnalogTimeSeries(&series, options);
1✔
318
        REQUIRE(result.success);
1✔
319
        CHECK(result.filtered_data != nullptr);
1✔
320
    }
5✔
321

322
    SECTION("RBJ filter") {
4✔
323
        FilterOptions options;
1✔
324
        options.type = FilterType::RBJ;
1✔
325
        options.response = FilterResponse::LowPass;
1✔
326
        options.order = 2;// RBJ is always 2nd order
1✔
327
        options.sampling_rate_hz = 1000.0;
1✔
328
        options.cutoff_frequency_hz = 100.0;
1✔
329
        options.q_factor = 1.0;
1✔
330

331
        auto result = filterAnalogTimeSeries(&series, options);
1✔
332
        REQUIRE(result.success);
1✔
333
        CHECK(result.filtered_data != nullptr);
1✔
334
    }
5✔
335
}
8✔
336

337
TEST_CASE("Simple filter test", "[filter][simple]") {
1✔
338
    // Create simple test data
339
    std::vector<float> data = {1.0f, 2.0f, 3.0f, 2.0f, 1.0f, 0.0f, -1.0f, -2.0f, -1.0f, 0.0f};
3✔
340
    std::vector<TimeFrameIndex> times;
1✔
341
    times.reserve(data.size());
1✔
342
    
343
    for (size_t i = 0; i < data.size(); ++i) {
11✔
344
        times.push_back(TimeFrameIndex(static_cast<int64_t>(i)));
10✔
345
    }
346
    
347
    AnalogTimeSeries series(data, times);
1✔
348
    
349
    SECTION("Simple Butterworth lowpass filter") {
1✔
350
        FilterOptions options;
1✔
351
        options.type = FilterType::Butterworth;
1✔
352
        options.response = FilterResponse::LowPass;
1✔
353
        options.order = 2;
1✔
354
        options.sampling_rate_hz = 1000.0; // Higher sampling rate
1✔
355
        options.cutoff_frequency_hz = 50.0; // Lower relative cutoff (0.1 normalized)
1✔
356
        
357
        INFO("Sampling rate: " << options.sampling_rate_hz);
1✔
358
        INFO("Cutoff frequency: " << options.cutoff_frequency_hz);
1✔
359
        INFO("Nyquist frequency: " << options.sampling_rate_hz / 2.0);
1✔
360
        INFO("Normalized cutoff for setup(): " << options.cutoff_frequency_hz / options.sampling_rate_hz);
1✔
361
        INFO("Filter order (from template): " << options.order);
1✔
362
        
363
        INFO("Options valid: " << (options.isValid() ? "true" : "false"));
1✔
364
        if (!options.isValid()) {
1✔
NEW
365
            INFO("Validation error: " << options.getValidationError());
×
NEW
366
        }
×
367
        REQUIRE(options.isValid());
1✔
368
        
369
        FilterResult result;
1✔
370
        try {
371
            result = filterAnalogTimeSeries(&series, options);
1✔
NEW
372
        } catch (const std::exception& e) {
×
NEW
373
            FAIL("Exception during filtering: " << e.what());
×
NEW
374
        }
×
375
        
376
        INFO("Result success: " << (result.success ? "true" : "false"));
1✔
377
        INFO("Error message: '" << result.error_message << "'");
1✔
378
        INFO("Samples processed: " << result.samples_processed);
1✔
379
        INFO("Input samples: " << data.size());
1✔
380
        
381
        REQUIRE(result.success);
1✔
382
        REQUIRE(result.filtered_data != nullptr);
1✔
383
        CHECK(result.filtered_data->getNumSamples() == data.size());
1✔
384
    }
2✔
385
}
2✔
386

387
TEST_CASE("Filter order variations", "[filter][order]") {
8✔
388
    // Create more realistic test data (sine wave)
389
    size_t const num_samples = 50;
8✔
390
    std::vector<float> data;
8✔
391
    std::vector<TimeFrameIndex> times;
8✔
392
    data.reserve(num_samples);
8✔
393
    times.reserve(num_samples);
8✔
394
    
395
    // Generate a simple sine wave instead of constant values
396
    for (size_t i = 0; i < num_samples; ++i) {
408✔
397
        double t = static_cast<double>(i) / 1000.0; // Assuming 1000 Hz sampling
400✔
398
        data.push_back(static_cast<float>(std::sin(2.0 * M_PI * 50.0 * t))); // 50 Hz sine wave
400✔
399
        times.push_back(TimeFrameIndex(static_cast<int64_t>(i)));
400✔
400
    }
401
    
402
    AnalogTimeSeries series(data, times);
8✔
403

404
        // Test different filter orders
405
    for (int order = 1; order <= max_filter_order; ++order) {
72✔
406
        SECTION("Filter order " + std::to_string(order)) {
64✔
407
            FilterOptions options;
8✔
408
            options.type = FilterType::Butterworth;
8✔
409
            options.response = FilterResponse::LowPass;
8✔
410
            options.order = order;
8✔
411
            options.sampling_rate_hz = 1000.0;
8✔
412
            options.cutoff_frequency_hz = 100.0;
8✔
413
            
414
            // First check if options are valid
415
            REQUIRE(options.isValid());
8✔
416
            
417
            // Try-catch to see if there's an exception
418
            FilterResult result;
8✔
419
            try {
420
                result = filterAnalogTimeSeries(&series, options);
8✔
NEW
421
            } catch (const std::exception& e) {
×
NEW
422
                FAIL("Exception thrown during filtering for order " << order << ": " << e.what());
×
NEW
423
            }
×
424
            
425
            // Print diagnostic info regardless of success
426
            INFO("Filter order: " << order);
8✔
427
            INFO("Result success: " << (result.success ? "true" : "false"));
8✔
428
            INFO("Error message: '" << result.error_message << "'");
8✔
429
            INFO("Samples processed: " << result.samples_processed);
8✔
430
            INFO("Segments processed: " << result.segments_processed);
8✔
431
            INFO("Filtered data ptr: " << (result.filtered_data ? "valid" : "null"));
8✔
432
            
433
            REQUIRE(result.success);
8✔
434
            CHECK(result.filtered_data != nullptr);
8✔
435
            CHECK(result.filtered_data->getNumSamples() == num_samples);
8✔
436
        }
72✔
437
    }
438
}
16✔
439

440
TEST_CASE("Error handling", "[filter][errors]") {
2✔
441
    SECTION("Null AnalogTimeSeries pointer") {
2✔
442
        auto options = FilterDefaults::lowpass(100.0, 1000.0, 4);
1✔
443
        auto result = filterAnalogTimeSeries(nullptr, options);
1✔
444

445
        REQUIRE_FALSE(result.success);
1✔
446
        CHECK(result.error_message.find("null") != std::string::npos);
1✔
447
        CHECK(result.filtered_data == nullptr);
1✔
448
    }
3✔
449

450
    SECTION("Invalid filter options") {
2✔
451
        std::vector<float> data = {1.0f, 2.0f, 3.0f};
3✔
452
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0), TimeFrameIndex(1), TimeFrameIndex(2)};
3✔
453
        AnalogTimeSeries series(std::move(data), std::move(times));
1✔
454

455
        FilterOptions invalid_options;
1✔
456
        invalid_options.order = 0;// Invalid order
1✔
457

458
        auto result = filterAnalogTimeSeries(&series, invalid_options);
1✔
459
        REQUIRE_FALSE(result.success);
1✔
460
        CHECK(result.error_message.find("Invalid filter options") != std::string::npos);
1✔
461
    }
3✔
462
}
2✔
463

464
TEST_CASE("Time range filtering", "[filter][time_range]") {
2✔
465
    // Create test data with specific time indices
466
    size_t const num_samples = 100;
2✔
467
    std::vector<float> data;
2✔
468
    std::vector<TimeFrameIndex> times;
2✔
469

470
    data.reserve(num_samples);
2✔
471
    times.reserve(num_samples);
2✔
472

473
    for (size_t i = 0; i < num_samples; ++i) {
202✔
474
        data.push_back(static_cast<float>(i));
200✔
475
        times.push_back(TimeFrameIndex(static_cast<int64_t>(i * 10)));// Spaced by 10
200✔
476
    }
477

478
    AnalogTimeSeries series(std::move(data), std::move(times));
2✔
479
    auto options = FilterDefaults::lowpass(100.0, 1000.0, 4);
2✔
480

481
    SECTION("Filter specific time range") {
2✔
482
        TimeFrameIndex start_time(200);// Should include indices >= 200
1✔
483
        TimeFrameIndex end_time(500);  // Should include indices <= 500
1✔
484

485
        auto result = filterAnalogTimeSeries(&series, start_time, end_time, options);
1✔
486

487
        REQUIRE(result.success);
1✔
488
        CHECK(result.filtered_data != nullptr);
1✔
489
        // Should have samples for times 200, 210, 220, ..., 500
490
        // That's (500-200)/10 + 1 = 31 samples
491
        CHECK(result.filtered_data->getNumSamples() == 31);
1✔
492
    }
3✔
493

494
    SECTION("Filter entire series") {
2✔
495
        auto result = filterAnalogTimeSeries(&series, options);
1✔
496

497
        REQUIRE(result.success);
1✔
498
        CHECK(result.filtered_data != nullptr);
1✔
499
        CHECK(result.filtered_data->getNumSamples() == num_samples);
1✔
500
    }
3✔
501
} 
4✔
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