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

paulmthompson / WhiskerToolbox / 15655387894

14 Jun 2025 07:27PM UTC coverage: 65.21% (+0.6%) from 64.638%
15655387894

push

github

paulmthompson
fixed data aggregation error with DataArrayIndex vs TimeFrameIndex problem

18 of 20 new or added lines in 1 file covered. (90.0%)

183 existing lines in 13 files now uncovered.

7996 of 12262 relevant lines covered (65.21%)

602.13 hits per line

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

99.53
/src/WhiskerToolbox/DataManager/transforms/AnalogTimeSeries/analog_interval_threshold.test.cpp
1
#define CATCH_CONFIG_MAIN
2
#include "catch2/catch_test_macros.hpp"
3
#include "catch2/matchers/catch_matchers_vector.hpp"
4

5
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
6
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
7
#include "transforms/AnalogTimeSeries/analog_interval_threshold.hpp"
8
#include "transforms/data_transforms.hpp"
9

10
#include <cmath>
11
#include <functional>
12
#include <memory>
13
#include <vector>
14

15
// Helper function to validate that all values during intervals are above threshold
16
auto validateIntervalsAboveThreshold = [](
12✔
17
                                               std::vector<float> const & values,
18
                                               std::vector<TimeFrameIndex> const & times,
19
                                               std::vector<Interval> const & intervals,
20
                                               IntervalThresholdParams const & params) -> bool {
21
    for (auto const & interval: intervals) {
34✔
22
        // Find all time indices that fall within this interval
23
        for (size_t i = 0; i < times.size(); ++i) {
171✔
24
            if (times[i].getValue() >= static_cast<int64_t>(interval.start) &&
241✔
25
                times[i].getValue() <= static_cast<int64_t>(interval.end)) {
92✔
26

27
                // Check if this value meets the threshold criteria
28
                bool meetsThreshold = false;
42✔
29
                switch (params.direction) {
42✔
30
                    case IntervalThresholdParams::ThresholdDirection::POSITIVE:
32✔
31
                        meetsThreshold = values[i] > static_cast<float>(params.thresholdValue);
32✔
32
                        break;
32✔
33
                    case IntervalThresholdParams::ThresholdDirection::NEGATIVE:
5✔
34
                        meetsThreshold = values[i] < static_cast<float>(params.thresholdValue);
5✔
35
                        break;
5✔
36
                    case IntervalThresholdParams::ThresholdDirection::ABSOLUTE:
5✔
37
                        meetsThreshold = std::abs(values[i]) > static_cast<float>(params.thresholdValue);
5✔
38
                        break;
5✔
39
                }
40

41
                if (!meetsThreshold) {
42✔
42
                    return false;// Found a value in interval that doesn't meet threshold
×
43
                }
44
            }
45
        }
46
    }
47
    return true;// All values in all intervals meet threshold
12✔
48
};
49

50
TEST_CASE("Interval Threshold Happy Path", "[transforms][analog_interval_threshold]") {
9✔
51
    std::vector<float> values;
9✔
52
    std::vector<TimeFrameIndex> times;
9✔
53
    std::shared_ptr<AnalogTimeSeries> ats;
9✔
54
    std::shared_ptr<DigitalIntervalSeries> result_intervals;
9✔
55
    IntervalThresholdParams params;
9✔
56
    // Set default to IGNORE mode to preserve original test behavior
57
    params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
9✔
58
    int volatile progress_val = -1;
9✔
59
    int volatile call_count = 0;
9✔
60
    ProgressCallback cb = [&](int p) {
18✔
61
        progress_val = p;
5✔
62
        call_count++;
5✔
63
    };
9✔
64

65
    SECTION("Positive threshold - simple case") {
9✔
66
        values = {0.5f, 1.5f, 2.0f, 1.8f, 0.8f, 2.5f, 1.2f, 0.3f};
1✔
67
        times = {TimeFrameIndex(100), 
2✔
68
                 TimeFrameIndex(200), 
69
                 TimeFrameIndex(300), 
70
                 TimeFrameIndex(400), 
71
                 TimeFrameIndex(500), 
72
                 TimeFrameIndex(600), 
73
                 TimeFrameIndex(700), 
74
                 TimeFrameIndex(800)};
1✔
75
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
76

77
        params.thresholdValue = 1.0;
1✔
78
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
79
        params.lockoutTime = 0.0;
1✔
80
        params.minDuration = 0.0;
1✔
81
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
1✔
82

83
        result_intervals = interval_threshold(ats.get(), params);
1✔
84
        REQUIRE(result_intervals != nullptr);
1✔
85

86
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
87
        REQUIRE(intervals.size() == 2);// Two intervals: [200-400] and [600-700]
1✔
88

89
        REQUIRE(intervals[0].start == 200);
1✔
90
        REQUIRE(intervals[0].end == 400);
1✔
91
        REQUIRE(intervals[1].start == 600);
1✔
92
        REQUIRE(intervals[1].end == 700);
1✔
93

94
        // Validate that all values during intervals are above threshold
95
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
96

97
        // Test with progress callback
98
        progress_val = -1;
1✔
99
        call_count = 0;
1✔
100
        result_intervals = interval_threshold(ats.get(), params, cb);
1✔
101
        REQUIRE(result_intervals != nullptr);
1✔
102
        REQUIRE(progress_val == 100);
1✔
103
        REQUIRE(call_count > 0);
1✔
104
    }
9✔
105

106
    SECTION("Negative threshold") {
9✔
107
        values = {0.5f, -1.5f, -2.0f, -1.8f, 0.8f, -2.5f, -1.2f, 0.3f};
1✔
108
        times = {TimeFrameIndex(100), 
2✔
109
                 TimeFrameIndex(200), 
110
                 TimeFrameIndex(300), 
111
                 TimeFrameIndex(400), 
112
                 TimeFrameIndex(500), 
113
                 TimeFrameIndex(600), 
114
                 TimeFrameIndex(700), 
115
                 TimeFrameIndex(800)};
1✔
116
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
117

118
        params.thresholdValue = -1.0;
1✔
119
        params.direction = IntervalThresholdParams::ThresholdDirection::NEGATIVE;
1✔
120
        params.lockoutTime = 0.0;
1✔
121
        params.minDuration = 0.0;
1✔
122
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
1✔
123

124
        result_intervals = interval_threshold(ats.get(), params);
1✔
125
        REQUIRE(result_intervals != nullptr);
1✔
126

127
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
128
        REQUIRE(intervals.size() == 2);// Two intervals: [200-400] and [600-700]
1✔
129

130
        REQUIRE(intervals[0].start == 200);
1✔
131
        REQUIRE(intervals[0].end == 400);
1✔
132
        REQUIRE(intervals[1].start == 600);
1✔
133
        REQUIRE(intervals[1].end == 700);
1✔
134

135
        // Validate that all values during intervals meet negative threshold
136
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
137
    }
9✔
138

139
    SECTION("Absolute threshold") {
9✔
140
        values = {0.5f, 1.5f, -2.0f, 1.8f, 0.8f, -2.5f, 1.2f, 0.3f};
1✔
141
        times = {TimeFrameIndex(100), 
2✔
142
                 TimeFrameIndex(200), 
143
                 TimeFrameIndex(300), 
144
                 TimeFrameIndex(400), 
145
                 TimeFrameIndex(500), 
146
                 TimeFrameIndex(600), 
147
                 TimeFrameIndex(700), 
148
                 TimeFrameIndex(800)};
1✔
149
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
150

151
        params.thresholdValue = 1.0;
1✔
152
        params.direction = IntervalThresholdParams::ThresholdDirection::ABSOLUTE;
1✔
153
        params.lockoutTime = 0.0;
1✔
154
        params.minDuration = 0.0;
1✔
155

156
        result_intervals = interval_threshold(ats.get(), params);
1✔
157
        REQUIRE(result_intervals != nullptr);
1✔
158

159
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
160
        REQUIRE(intervals.size() == 2);// Two intervals: [200-400] and [600-700]
1✔
161

162
        REQUIRE(intervals[0].start == 200);
1✔
163
        REQUIRE(intervals[0].end == 400);
1✔
164
        REQUIRE(intervals[1].start == 600);
1✔
165
        REQUIRE(intervals[1].end == 700);
1✔
166

167
        // Validate that all values during intervals meet absolute threshold
168
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
169
    }
9✔
170

171
    SECTION("With lockout time") {
9✔
172
        values = {0.5f, 1.5f, 0.8f, 1.8f, 0.5f, 1.2f, 0.3f};
1✔
173
        times = {TimeFrameIndex(100), 
2✔
174
                 TimeFrameIndex(200), 
175
                 TimeFrameIndex(250), 
176
                 TimeFrameIndex(300), 
177
                 TimeFrameIndex(400), 
178
                 TimeFrameIndex(450), 
179
                 TimeFrameIndex(500)};
1✔
180
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
181

182
        params.thresholdValue = 1.0;
1✔
183
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
184
        params.lockoutTime = 100.0;// 100 time units lockout
1✔
185
        params.minDuration = 0.0;
1✔
186

187
        result_intervals = interval_threshold(ats.get(), params);
1✔
188
        REQUIRE(result_intervals != nullptr);
1✔
189

190
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
191
        REQUIRE(intervals.size() == 3);// Three separate intervals - lockout prevents starting too soon
1✔
192

193
        REQUIRE(intervals[0].start == 200);
1✔
194
        REQUIRE(intervals[0].end == 200);
1✔
195
        REQUIRE(intervals[1].start == 300);
1✔
196
        REQUIRE(intervals[1].end == 300);
1✔
197
        REQUIRE(intervals[2].start == 450);
1✔
198
        REQUIRE(intervals[2].end == 450);
1✔
199

200
        // Validate that all values during intervals are above threshold
201
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
202
    }
9✔
203

204
    SECTION("With minimum duration") {
9✔
205
        values = {0.5f, 1.5f, 0.8f, 1.8f, 1.2f, 1.1f, 0.5f};
1✔
206
        times = {TimeFrameIndex(100), 
2✔
207
                 TimeFrameIndex(200), 
208
                 TimeFrameIndex(250), 
209
                 TimeFrameIndex(300), 
210
                 TimeFrameIndex(400), 
211
                 TimeFrameIndex(500), 
212
                 TimeFrameIndex(600)};
1✔
213
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
214

215
        params.thresholdValue = 1.0;
1✔
216
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
217
        params.lockoutTime = 0.0;
1✔
218
        params.minDuration = 150.0;// Minimum 150 time units
1✔
219

220
        result_intervals = interval_threshold(ats.get(), params);
1✔
221
        REQUIRE(result_intervals != nullptr);
1✔
222

223
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
224
        REQUIRE(intervals.size() == 1);// Only one interval meets minimum duration
1✔
225

226
        REQUIRE(intervals[0].start == 300);
1✔
227
        REQUIRE(intervals[0].end == 500);
1✔
228

229
        // Validate that all values during intervals are above threshold
230
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
231
    }
9✔
232

233
    SECTION("Signal ends while above threshold") {
9✔
234
        values = {0.5f, 1.5f, 2.0f, 1.8f, 1.2f};
1✔
235
        times = {TimeFrameIndex(100), 
2✔
236
                 TimeFrameIndex(200), 
237
                 TimeFrameIndex(300), 
238
                 TimeFrameIndex(400), 
239
                 TimeFrameIndex(500)};
1✔
240
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
241

242
        params.thresholdValue = 1.0;
1✔
243
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
244
        params.lockoutTime = 0.0;
1✔
245
        params.minDuration = 0.0;
1✔
246

247
        result_intervals = interval_threshold(ats.get(), params);
1✔
248
        REQUIRE(result_intervals != nullptr);
1✔
249

250
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
251
        REQUIRE(intervals.size() == 1);
1✔
252

253
        REQUIRE(intervals[0].start == 200);
1✔
254
        REQUIRE(intervals[0].end == 500);// Should extend to end of signal
1✔
255

256
        // Validate that all values during intervals are above threshold
257
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
258
    }
9✔
259

260
    SECTION("No intervals detected") {
9✔
261
        values = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f};
1✔
262
        times = {TimeFrameIndex(100), 
2✔
263
                 TimeFrameIndex(200), 
264
                 TimeFrameIndex(300), 
265
                 TimeFrameIndex(400), 
266
                 TimeFrameIndex(500)};
1✔
267
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
268

269
        params.thresholdValue = 1.0;
1✔
270
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
271
        params.lockoutTime = 0.0;
1✔
272
        params.minDuration = 0.0;
1✔
273

274
        result_intervals = interval_threshold(ats.get(), params);
1✔
275
        REQUIRE(result_intervals != nullptr);
1✔
276

277
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
278
        REQUIRE(intervals.empty());
1✔
279
    }
9✔
280

281
    SECTION("Progress callback detailed check") {
9✔
282
        values = {0.5f, 1.5f, 0.8f, 2.0f, 0.3f};
1✔
283
        times = {TimeFrameIndex(100), 
2✔
284
                 TimeFrameIndex(200), 
285
                 TimeFrameIndex(300), 
286
                 TimeFrameIndex(400), 
287
                 TimeFrameIndex(500)};
1✔
288
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
289

290
        params.thresholdValue = 1.0;
1✔
291
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
292
        params.lockoutTime = 0.0;
1✔
293
        params.minDuration = 0.0;
1✔
294

295
        progress_val = 0;
1✔
296
        call_count = 0;
1✔
297
        std::vector<int> progress_values_seen;
1✔
298
        ProgressCallback detailed_cb = [&](int p) {
2✔
299
            progress_val = p;
5✔
300
            call_count++;
5✔
301
            progress_values_seen.push_back(p);
5✔
302
        };
1✔
303

304
        result_intervals = interval_threshold(ats.get(), params, detailed_cb);
1✔
305
        REQUIRE(progress_val == 100);
1✔
306
        REQUIRE(call_count > 0);
1✔
307

308
        // Check that we see increasing progress values
309
        REQUIRE(!progress_values_seen.empty());
1✔
310
        REQUIRE(progress_values_seen.front() >= 0);
1✔
311
        REQUIRE(progress_values_seen.back() == 100);
1✔
312

313
        // Verify progress is monotonically increasing
314
        for (size_t i = 1; i < progress_values_seen.size(); ++i) {
5✔
315
            REQUIRE(progress_values_seen[i] >= progress_values_seen[i - 1]);
4✔
316
        }
317
    }
10✔
318

319
    SECTION("Complex signal with multiple parameters") {
9✔
320
        values = {0.0f, 2.0f, 1.8f, 1.5f, 0.5f, 2.5f, 2.2f, 1.9f, 0.8f, 1.1f, 0.3f};
1✔
321
        times = {TimeFrameIndex(0), 
2✔
322
                 TimeFrameIndex(100), 
323
                 TimeFrameIndex(150), 
324
                 TimeFrameIndex(200), 
325
                 TimeFrameIndex(300), 
326
                 TimeFrameIndex(400), 
327
                 TimeFrameIndex(450), 
328
                 TimeFrameIndex(500), 
329
                 TimeFrameIndex(600), 
330
                 TimeFrameIndex(700), 
331
                 TimeFrameIndex(800)};
1✔
332
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
333

334
        params.thresholdValue = 1.0;
1✔
335
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
336
        params.lockoutTime = 50.0;
1✔
337
        params.minDuration = 100.0;
1✔
338

339
        result_intervals = interval_threshold(ats.get(), params);
1✔
340
        REQUIRE(result_intervals != nullptr);
1✔
341

342
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
343
        REQUIRE(intervals.size() == 2);// Two intervals that meet minimum duration
1✔
344

345
        REQUIRE(intervals[0].start == 100);
1✔
346
        REQUIRE(intervals[0].end == 200);
1✔
347
        REQUIRE(intervals[1].start == 400);
1✔
348
        REQUIRE(intervals[1].end == 500);
1✔
349

350
        // Validate that all values during intervals are above threshold
351
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
352
    }
9✔
353
}
18✔
354

355
TEST_CASE("Interval Threshold Error and Edge Cases", "[transforms][analog_interval_threshold]") {
10✔
356
    std::shared_ptr<AnalogTimeSeries> ats;
10✔
357
    std::shared_ptr<DigitalIntervalSeries> result_intervals;
10✔
358
    IntervalThresholdParams params;
10✔
359
    // Set default to IGNORE mode to preserve original test behavior
360
    params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
10✔
361
    int volatile progress_val = -1;
10✔
362
    int volatile call_count = 0;
10✔
363
    ProgressCallback cb = [&](int p) {
20✔
UNCOV
364
        progress_val = p;
×
UNCOV
365
        call_count++;
×
366
    };
10✔
367

368
    SECTION("Null input AnalogTimeSeries") {
10✔
369
        ats = nullptr;
1✔
370
        params.thresholdValue = 1.0;
1✔
371
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
372

373
        result_intervals = interval_threshold(ats.get(), params);
1✔
374
        REQUIRE(result_intervals != nullptr);
1✔
375
        REQUIRE(result_intervals->getDigitalIntervalSeries().empty());
1✔
376

377
        // Test with progress callback
378
        progress_val = -1;
1✔
379
        call_count = 0;
1✔
380
        result_intervals = interval_threshold(ats.get(), params, cb);
1✔
381
        REQUIRE(result_intervals != nullptr);
1✔
382
        REQUIRE(result_intervals->getDigitalIntervalSeries().empty());
1✔
383
        // Progress callback should not be called for null input
384
        REQUIRE(call_count == 0);
1✔
385
    }
10✔
386

387
    SECTION("Empty time series") {
10✔
388
        std::vector<float> empty_values;
1✔
389
        std::vector<TimeFrameIndex> empty_times;
1✔
390
        ats = std::make_shared<AnalogTimeSeries>(empty_values, empty_times);
1✔
391
        params.thresholdValue = 1.0;
1✔
392
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
393

394
        result_intervals = interval_threshold(ats.get(), params);
1✔
395
        REQUIRE(result_intervals != nullptr);
1✔
396
        REQUIRE(result_intervals->getDigitalIntervalSeries().empty());
1✔
397
    }
11✔
398

399
    SECTION("Single sample above threshold") {
10✔
400
        std::vector<float> values = {2.0f};
3✔
401
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100)};
3✔
402
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
403

404
        params.thresholdValue = 1.0;
1✔
405
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
406
        params.lockoutTime = 0.0;
1✔
407
        params.minDuration = 0.0;
1✔
408

409
        result_intervals = interval_threshold(ats.get(), params);
1✔
410
        REQUIRE(result_intervals != nullptr);
1✔
411

412
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
413
        REQUIRE(intervals.size() == 1);
1✔
414
        REQUIRE(intervals[0].start == 100);
1✔
415
        REQUIRE(intervals[0].end == 100);
1✔
416

417
        // Validate that all values during intervals are above threshold
418
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
419
    }
11✔
420

421
    SECTION("Single sample below threshold") {
10✔
422
        std::vector<float> values = {0.5f};
3✔
423
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100)};
3✔
424
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
425

426
        params.thresholdValue = 1.0;
1✔
427
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
428
        params.lockoutTime = 0.0;
1✔
429
        params.minDuration = 0.0;
1✔
430

431
        result_intervals = interval_threshold(ats.get(), params);
1✔
432
        REQUIRE(result_intervals != nullptr);
1✔
433

434
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
435
        REQUIRE(intervals.empty());
1✔
436
    }
11✔
437

438
    SECTION("All values above threshold") {
10✔
439
        std::vector<float> values = {1.5f, 2.0f, 1.8f, 2.5f, 1.2f};
3✔
440
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
441
                 TimeFrameIndex(200), 
442
                 TimeFrameIndex(300), 
443
                 TimeFrameIndex(400), 
444
                 TimeFrameIndex(500)};
3✔
445
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
446

447
        params.thresholdValue = 1.0;
1✔
448
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
449
        params.lockoutTime = 0.0;
1✔
450
        params.minDuration = 0.0;
1✔
451

452
        result_intervals = interval_threshold(ats.get(), params);
1✔
453
        REQUIRE(result_intervals != nullptr);
1✔
454

455
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
456
        REQUIRE(intervals.size() == 1);
1✔
457
        REQUIRE(intervals[0].start == 100);
1✔
458
        REQUIRE(intervals[0].end == 500);
1✔
459
    }
11✔
460

461
    SECTION("Zero threshold") {
10✔
462
        std::vector<float> values = {-1.0f, 0.0f, 1.0f, -0.5f, 0.5f};
3✔
463
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
464
                 TimeFrameIndex(200), 
465
                 TimeFrameIndex(300), 
466
                 TimeFrameIndex(400), 
467
                 TimeFrameIndex(500)};
3✔
468
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
469

470
        params.thresholdValue = 0.0;
1✔
471
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
472
        params.lockoutTime = 0.0;
1✔
473
        params.minDuration = 0.0;
1✔
474

475
        result_intervals = interval_threshold(ats.get(), params);
1✔
476
        REQUIRE(result_intervals != nullptr);
1✔
477

478
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
479
        REQUIRE(intervals.size() == 2);
1✔
480
        REQUIRE(intervals[0].start == 300);
1✔
481
        REQUIRE(intervals[0].end == 300);
1✔
482
        REQUIRE(intervals[1].start == 500);
1✔
483
        REQUIRE(intervals[1].end == 500);
1✔
484
    }
11✔
485

486
    SECTION("Negative threshold value") {
10✔
487
        std::vector<float> values = {-2.0f, -1.0f, -0.5f, -1.5f, -0.8f};
3✔
488
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
489
                 TimeFrameIndex(200), 
490
                 TimeFrameIndex(300), 
491
                 TimeFrameIndex(400), 
492
                 TimeFrameIndex(500)};
3✔
493
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
494

495
        params.thresholdValue = -1.0;
1✔
496
        params.direction = IntervalThresholdParams::ThresholdDirection::NEGATIVE;
1✔
497
        params.lockoutTime = 0.0;
1✔
498
        params.minDuration = 0.0;
1✔
499

500
        result_intervals = interval_threshold(ats.get(), params);
1✔
501
        REQUIRE(result_intervals != nullptr);
1✔
502

503
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
504
        REQUIRE(intervals.size() == 2);
1✔
505
        REQUIRE(intervals[0].start == 100);
1✔
506
        REQUIRE(intervals[0].end == 100);
1✔
507
        REQUIRE(intervals[1].start == 400);
1✔
508
        REQUIRE(intervals[1].end == 400);
1✔
509
    }
11✔
510

511
    SECTION("Very large lockout time") {
10✔
512
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f, 0.5f, 1.2f};
3✔
513
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
514
                 TimeFrameIndex(200), 
515
                 TimeFrameIndex(300), 
516
                 TimeFrameIndex(400), 
517
                 TimeFrameIndex(500), 
518
                 TimeFrameIndex(600)};
3✔
519
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
520

521
        params.thresholdValue = 1.0;
1✔
522
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
523
        params.lockoutTime = 1000.0;// Very large lockout
1✔
524
        params.minDuration = 0.0;
1✔
525

526
        result_intervals = interval_threshold(ats.get(), params);
1✔
527
        REQUIRE(result_intervals != nullptr);
1✔
528

529
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
530
        REQUIRE(intervals.size() == 1);// Only first interval should be detected
1✔
531
        REQUIRE(intervals[0].start == 200);
1✔
532
        REQUIRE(intervals[0].end == 200);
1✔
533
    }
11✔
534

535
    SECTION("Very large minimum duration") {
10✔
536
        std::vector<float> values = {0.5f, 1.5f, 1.8f, 1.2f, 0.5f};
3✔
537
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
538
                 TimeFrameIndex(200), 
539
                 TimeFrameIndex(300), 
540
                 TimeFrameIndex(400), 
541
                 TimeFrameIndex(500)};
3✔
542
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
543

544
        params.thresholdValue = 1.0;
1✔
545
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
546
        params.lockoutTime = 0.0;
1✔
547
        params.minDuration = 1000.0;// Very large minimum duration
1✔
548

549
        result_intervals = interval_threshold(ats.get(), params);
1✔
550
        REQUIRE(result_intervals != nullptr);
1✔
551

552
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
553
        REQUIRE(intervals.empty());// No intervals meet minimum duration
1✔
554
    }
11✔
555

556
    SECTION("Irregular timestamp spacing") {
10✔
557
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f, 0.5f};
3✔
558
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0), 
1✔
559
                 TimeFrameIndex(1), 
560
                 TimeFrameIndex(100), 
561
                 TimeFrameIndex(101), 
562
                 TimeFrameIndex(1000)};// Irregular spacing
3✔
563
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
564

565
        params.thresholdValue = 1.0;
1✔
566
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
567
        params.lockoutTime = 0.0;
1✔
568
        params.minDuration = 0.0;
1✔
569

570
        result_intervals = interval_threshold(ats.get(), params);
1✔
571
        REQUIRE(result_intervals != nullptr);
1✔
572

573
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
574
        REQUIRE(intervals.size() == 2);
1✔
575
        REQUIRE(intervals[0].start == 1);
1✔
576
        REQUIRE(intervals[0].end == 1);
1✔
577
        REQUIRE(intervals[1].start == 101);
1✔
578
        REQUIRE(intervals[1].end == 101);
1✔
579
    }
11✔
580
}
20✔
581

582
TEST_CASE("Single Sample Above Threshold Zero Lockout", "[transforms][analog_interval_threshold]") {
2✔
583
    SECTION("Single sample above threshold followed by below threshold") {
2✔
584
        // Test case for the specific scenario: single sample above threshold
585
        // with lockout period of zero. The interval should start and end at
586
        // the same time point (the time of the threshold crossing).
587
        // During the interval, ALL signals should be above threshold.
588

589
        std::vector<float> values = {0.5f, 2.0f, 0.8f, 0.3f};// Only sample at index 1 is above threshold
3✔
590
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
591
                 TimeFrameIndex(200), 
592
                 TimeFrameIndex(300), 
593
                 TimeFrameIndex(400)};
3✔
594
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
595

596
        IntervalThresholdParams params;
1✔
597
        params.thresholdValue = 1.0;
1✔
598
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
599
        params.lockoutTime = 0.0;// Zero lockout period
1✔
600
        params.minDuration = 0.0;
1✔
601
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
1✔
602

603
        auto result_intervals = interval_threshold(ats.get(), params);
1✔
604
        REQUIRE(result_intervals != nullptr);
1✔
605

606
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
607
        REQUIRE(intervals.size() == 1);
1✔
608

609
        // The interval should start and end at time 200 (the single sample above threshold)
610
        // This ensures all signals during the detected interval are above threshold
611
        REQUIRE(intervals[0].start == 200);
1✔
612
        REQUIRE(intervals[0].end == 200);
1✔
613

614
        // Validate that all values during intervals are above threshold
615
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
616
    }
3✔
617

618
    SECTION("Multiple single samples above threshold") {
2✔
619
        // Test with multiple isolated single samples above threshold
620
        std::vector<float> values = {0.5f, 2.0f, 0.8f, 1.5f, 0.3f, 1.8f, 0.6f};
3✔
621
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
622
                 TimeFrameIndex(200), 
623
                 TimeFrameIndex(300), 
624
                 TimeFrameIndex(400), 
625
                 TimeFrameIndex(500), 
626
                 TimeFrameIndex(600), 
627
                 TimeFrameIndex(700)};
3✔
628
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
629

630
        IntervalThresholdParams params;
1✔
631
        params.thresholdValue = 1.0;
1✔
632
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
633
        params.lockoutTime = 0.0;// Zero lockout period
1✔
634
        params.minDuration = 0.0;
1✔
635
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
1✔
636

637
        auto result_intervals = interval_threshold(ats.get(), params);
1✔
638
        REQUIRE(result_intervals != nullptr);
1✔
639

640
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
641
        REQUIRE(intervals.size() == 3);// Three isolated single samples above threshold
1✔
642

643
        // Each interval should be a single point in time
644
        REQUIRE(intervals[0].start == 200);
1✔
645
        REQUIRE(intervals[0].end == 200);
1✔
646
        REQUIRE(intervals[1].start == 400);
1✔
647
        REQUIRE(intervals[1].end == 400);
1✔
648
        REQUIRE(intervals[2].start == 600);
1✔
649
        REQUIRE(intervals[2].end == 600);
1✔
650

651
        // Validate that all values during intervals are above threshold
652
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
653
    }
3✔
654
}
2✔
655

656
TEST_CASE("IntervalThresholdOperation Class Tests", "[transforms][analog_interval_threshold][operation]") {
8✔
657
    IntervalThresholdOperation operation;
8✔
658
    DataTypeVariant variant;
8✔
659
    IntervalThresholdParams params;
8✔
660
    params.thresholdValue = 1.0;
8✔
661
    params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
8✔
662
    params.lockoutTime = 0.0;
8✔
663
    params.minDuration = 0.0;
8✔
664
    params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
8✔
665

666
    SECTION("Operation metadata") {
8✔
667
        REQUIRE(operation.getName() == "Threshold Interval Detection");
1✔
668
        REQUIRE(operation.getTargetInputTypeIndex() == typeid(std::shared_ptr<AnalogTimeSeries>));
1✔
669
    }
8✔
670

671
    SECTION("canApply with valid data") {
8✔
672
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f};
3✔
673
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
674
                 TimeFrameIndex(200), 
675
                 TimeFrameIndex(300), 
676
                 TimeFrameIndex(400)};
3✔
677
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
678
        variant = ats;
1✔
679

680
        REQUIRE(operation.canApply(variant));
1✔
681
    }
9✔
682

683
    SECTION("canApply with null shared_ptr") {
8✔
684
        std::shared_ptr<AnalogTimeSeries> null_ats = nullptr;
1✔
685
        variant = null_ats;
1✔
686

687
        REQUIRE_FALSE(operation.canApply(variant));
1✔
688
    }
9✔
689

690
    SECTION("execute with valid parameters") {
8✔
691
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f};
3✔
692
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
693
                 TimeFrameIndex(200), 
694
                 TimeFrameIndex(300), 
695
                 TimeFrameIndex(400)};
3✔
696
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
697
        variant = ats;
1✔
698

699
        auto result = operation.execute(variant, &params);
1✔
700
        REQUIRE(std::holds_alternative<std::shared_ptr<DigitalIntervalSeries>>(result));
1✔
701

702
        auto result_intervals = std::get<std::shared_ptr<DigitalIntervalSeries>>(result);
1✔
703
        REQUIRE(result_intervals != nullptr);
1✔
704

705
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
706
        REQUIRE(intervals.size() == 2);
1✔
707
    }
9✔
708

709
    SECTION("execute with null parameters") {
8✔
710
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f};
3✔
711
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
712
                 TimeFrameIndex(200), 
713
                 TimeFrameIndex(300), 
714
                 TimeFrameIndex(400)};
3✔
715
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
716
        variant = ats;
1✔
717

718
        auto result = operation.execute(variant, nullptr);
1✔
719
        REQUIRE(std::holds_alternative<std::shared_ptr<DigitalIntervalSeries>>(result));
1✔
720

721
        auto result_intervals = std::get<std::shared_ptr<DigitalIntervalSeries>>(result);
1✔
722
        REQUIRE(result_intervals != nullptr);
1✔
723
    }
9✔
724

725
    SECTION("execute with progress callback") {
8✔
726
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f};
3✔
727
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
728
                 TimeFrameIndex(200), 
729
                 TimeFrameIndex(300), 
730
                 TimeFrameIndex(400)};
3✔
731
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
732
        variant = ats;
1✔
733

734
        int volatile progress_val = -1;
1✔
735
        int volatile call_count = 0;
1✔
736
        ProgressCallback cb = [&](int p) {
2✔
737
            progress_val = p;
5✔
738
            call_count++;
5✔
739
        };
1✔
740

741
        auto result = operation.execute(variant, &params, cb);
1✔
742
        REQUIRE(std::holds_alternative<std::shared_ptr<DigitalIntervalSeries>>(result));
1✔
743

744
        auto result_intervals = std::get<std::shared_ptr<DigitalIntervalSeries>>(result);
1✔
745
        REQUIRE(result_intervals != nullptr);
1✔
746
        REQUIRE(progress_val == 100);
1✔
747
        REQUIRE(call_count > 0);
1✔
748
    }
9✔
749

750
    SECTION("execute with wrong parameter type") {
8✔
751
        std::vector<float> values = {0.5f, 1.5f, 0.8f, 1.8f};
3✔
752
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
753
                 TimeFrameIndex(200), 
754
                 TimeFrameIndex(300), 
755
                 TimeFrameIndex(400)};
3✔
756
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
757
        variant = ats;
1✔
758

759
        // Create a different parameter type
760
        struct WrongParams : public TransformParametersBase {
761
            int dummy = 42;
762
        } wrong_params;
1✔
763

764
        auto result = operation.execute(variant, &wrong_params);
1✔
765
        REQUIRE(std::holds_alternative<std::shared_ptr<DigitalIntervalSeries>>(result));
1✔
766

767
        // Should still work with default parameters
768
        auto result_intervals = std::get<std::shared_ptr<DigitalIntervalSeries>>(result);
1✔
769
        REQUIRE(result_intervals != nullptr);
1✔
770
    }
9✔
771

772
    SECTION("execute with different threshold directions") {
8✔
773
        std::vector<float> values = {0.5f, -1.5f, 0.8f, 1.8f};
3✔
774
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(100), 
1✔
775
                 TimeFrameIndex(200), 
776
                 TimeFrameIndex(300), 
777
                 TimeFrameIndex(400)};
3✔
778
        auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
779
        variant = ats;
1✔
780

781
        // Test negative threshold
782
        params.direction = IntervalThresholdParams::ThresholdDirection::NEGATIVE;
1✔
783
        params.thresholdValue = -1.0;
1✔
784

785
        auto result = operation.execute(variant, &params);
1✔
786
        REQUIRE(std::holds_alternative<std::shared_ptr<DigitalIntervalSeries>>(result));
1✔
787

788
        auto result_intervals = std::get<std::shared_ptr<DigitalIntervalSeries>>(result);
1✔
789
        REQUIRE(result_intervals != nullptr);
1✔
790

791
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
792
        REQUIRE(intervals.size() == 1);
1✔
793
        REQUIRE(intervals[0].start == 200);
1✔
794
        REQUIRE(intervals[0].end == 200);
1✔
795

796
        // Test absolute threshold
797
        params.direction = IntervalThresholdParams::ThresholdDirection::ABSOLUTE;
1✔
798
        params.thresholdValue = 1.0;
1✔
799

800
        result = operation.execute(variant, &params);
1✔
801
        REQUIRE(std::holds_alternative<std::shared_ptr<DigitalIntervalSeries>>(result));
1✔
802

803
        result_intervals = std::get<std::shared_ptr<DigitalIntervalSeries>>(result);
1✔
804
        REQUIRE(result_intervals != nullptr);
1✔
805

806
        auto const & abs_intervals = result_intervals->getDigitalIntervalSeries();
1✔
807
        REQUIRE(abs_intervals.size() == 2);
1✔
808
    }
9✔
809
}
16✔
810

811
TEST_CASE("Missing Data Handling", "[transforms][analog_interval_threshold]") {
5✔
812
    std::vector<float> values;
5✔
813
    std::vector<TimeFrameIndex> times;
5✔
814
    std::shared_ptr<AnalogTimeSeries> ats;
5✔
815
    std::shared_ptr<DigitalIntervalSeries> result_intervals;
5✔
816
    IntervalThresholdParams params;
5✔
817

818
    SECTION("Missing data treated as zero - positive threshold") {
5✔
819
        // Signal with gaps: times are not consecutive (gaps of 50 vs normal step of 1)
820
        values = {0.5f, 1.5f, 1.8f, 0.5f, 1.2f};// above, above, above, below, above
1✔
821
        times = {TimeFrameIndex(100), 
2✔
822
                 TimeFrameIndex(101), 
823
                 TimeFrameIndex(102), 
824
                 TimeFrameIndex(152), 
825
                 TimeFrameIndex(153)};      // Gap between 102 and 152 (50 missing samples)
1✔
826
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
827

828
        params.thresholdValue = 1.0;
1✔
829
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
830
        params.lockoutTime = 0.0;
1✔
831
        params.minDuration = 0.0;
1✔
832
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::TREAT_AS_ZERO;
1✔
833

834
        result_intervals = interval_threshold(ats.get(), params);
1✔
835
        REQUIRE(result_intervals != nullptr);
1✔
836

837
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
838
        // Should get: [101,102] (ends when gap with zeros starts), [153,153] (single sample)
839
        REQUIRE(intervals.size() == 2);
1✔
840

841
        REQUIRE(intervals[0].start == 101);
1✔
842
        REQUIRE(intervals[0].end == 102);// Ends before gap because zeros don't meet threshold
1✔
843
        REQUIRE(intervals[1].start == 153);
1✔
844
        REQUIRE(intervals[1].end == 153);
1✔
845

846
        // Validate that all values during intervals are above threshold
847
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
848
    }
5✔
849

850
    SECTION("Missing data treated as zero - negative threshold") {
5✔
851
        // Test case where zeros do NOT meet the threshold (negative threshold)
852
        values = {0.5f, -1.5f, 0.5f, -1.2f};// above, below, above, below
1✔
853
        times = {TimeFrameIndex(100), 
2✔
854
                 TimeFrameIndex(101), 
855
                 TimeFrameIndex(151), 
856
                 TimeFrameIndex(152)};       // Gap between 101 and 151 (50 missing samples)
1✔
857
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
858

859
        params.thresholdValue = -0.5;
1✔
860
        params.direction = IntervalThresholdParams::ThresholdDirection::NEGATIVE;
1✔
861
        params.lockoutTime = 0.0;
1✔
862
        params.minDuration = 0.0;
1✔
863
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::TREAT_AS_ZERO;
1✔
864

865
        result_intervals = interval_threshold(ats.get(), params);
1✔
866
        REQUIRE(result_intervals != nullptr);
1✔
867

868
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
869
        // Should get: [101,101], [152,152] (gap with zeros that don't meet negative threshold)
870
        // Note: zeros (0.0) > -0.5, so they don't meet the negative threshold
871
        REQUIRE(intervals.size() == 2);
1✔
872

873
        REQUIRE(intervals[0].start == 101);
1✔
874
        REQUIRE(intervals[0].end == 101);
1✔
875
        REQUIRE(intervals[1].start == 152);
1✔
876
        REQUIRE(intervals[1].end == 152);
1✔
877
    }
5✔
878

879
    SECTION("Missing data treated as zero - negative threshold where zeros DO meet threshold") {
5✔
880
        // Test case where zeros DO meet the threshold (very negative threshold)
881
        values = {0.5f, -1.5f, 0.5f, -1.2f};// above, below, above, below
1✔
882
        times = {TimeFrameIndex(100), 
2✔
883
                 TimeFrameIndex(101), 
884
                 TimeFrameIndex(151), 
885
                 TimeFrameIndex(152)};       // Gap between 101 and 151 (50 missing samples)
1✔
886
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
887

888
        params.thresholdValue = 0.5;// threshold > 0, so zeros (0.0) < 0.5 meet negative threshold
1✔
889
        params.direction = IntervalThresholdParams::ThresholdDirection::NEGATIVE;
1✔
890
        params.lockoutTime = 0.0;
1✔
891
        params.minDuration = 0.0;
1✔
892
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::TREAT_AS_ZERO;
1✔
893

894
        result_intervals = interval_threshold(ats.get(), params);
1✔
895
        REQUIRE(result_intervals != nullptr);
1✔
896

897
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
898
        // Should get: [101,150] (interval continues through gap with zeros), [152,152]
899
        // Note: zeros (0.0) < 0.5, so they meet the negative threshold and extend the interval
900
        REQUIRE(intervals.size() == 2);
1✔
901

902
        REQUIRE(intervals[0].start == 101);
1✔
903
        REQUIRE(intervals[0].end == 150);// Interval extends through gap to just before next sample
1✔
904
        REQUIRE(intervals[1].start == 152);
1✔
905
        REQUIRE(intervals[1].end == 152);
1✔
906
    }
5✔
907

908
    SECTION("Missing data ignored mode") {
5✔
909
        // Same data as first test but with IGNORE mode
910
        values = {0.5f, 1.5f, 1.8f, 0.5f, 1.2f};
1✔
911
        times = {TimeFrameIndex(100), 
2✔
912
                 TimeFrameIndex(101), 
913
                 TimeFrameIndex(102), 
914
                 TimeFrameIndex(152), 
915
                 TimeFrameIndex(153)};
1✔
916
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
917

918
        params.thresholdValue = 1.0;
1✔
919
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
920
        params.lockoutTime = 0.0;
1✔
921
        params.minDuration = 0.0;
1✔
922
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
1✔
923

924
        result_intervals = interval_threshold(ats.get(), params);
1✔
925
        REQUIRE(result_intervals != nullptr);
1✔
926

927
        auto const & intervals = result_intervals->getDigitalIntervalSeries();
1✔
928
        // With IGNORE mode, gaps are not considered, so we get: [101,102] and [153,153]
929
        REQUIRE(intervals.size() == 2);
1✔
930

931
        REQUIRE(intervals[0].start == 101);
1✔
932
        REQUIRE(intervals[0].end == 102);// Continuous despite time gap
1✔
933
        REQUIRE(intervals[1].start == 153);
1✔
934
        REQUIRE(intervals[1].end == 153);
1✔
935

936
        // Validate that all values during intervals are above threshold
937
        REQUIRE(validateIntervalsAboveThreshold(values, times, intervals, params));
1✔
938
    }
5✔
939

940
    SECTION("No gaps in data") {
5✔
941
        // Test that normal continuous data works the same regardless of mode
942
        values = {0.5f, 1.5f, 1.8f, 0.5f, 1.2f};
1✔
943
        times = {TimeFrameIndex(100), 
2✔
944
                 TimeFrameIndex(101), 
945
                 TimeFrameIndex(102), 
946
                 TimeFrameIndex(103), 
947
                 TimeFrameIndex(104)}; // Consecutive times
1✔
948
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
949

950
        params.thresholdValue = 1.0;
1✔
951
        params.direction = IntervalThresholdParams::ThresholdDirection::POSITIVE;
1✔
952
        params.lockoutTime = 0.0;
1✔
953
        params.minDuration = 0.0;
1✔
954

955
        // Test both modes give same result for continuous data
956
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::TREAT_AS_ZERO;
1✔
957
        auto result_zero_mode = interval_threshold(ats.get(), params);
1✔
958

959
        params.missingDataMode = IntervalThresholdParams::MissingDataMode::IGNORE;
1✔
960
        auto result_ignore_mode = interval_threshold(ats.get(), params);
1✔
961

962
        REQUIRE(result_zero_mode != nullptr);
1✔
963
        REQUIRE(result_ignore_mode != nullptr);
1✔
964

965
        auto const & intervals_zero = result_zero_mode->getDigitalIntervalSeries();
1✔
966
        auto const & intervals_ignore = result_ignore_mode->getDigitalIntervalSeries();
1✔
967

968
        REQUIRE(intervals_zero.size() == intervals_ignore.size());
1✔
969
        REQUIRE(intervals_zero.size() == 2);// [101,102] and [104,104]
1✔
970

971
        for (size_t i = 0; i < intervals_zero.size(); ++i) {
3✔
972
            REQUIRE(intervals_zero[i].start == intervals_ignore[i].start);
2✔
973
            REQUIRE(intervals_zero[i].end == intervals_ignore[i].end);
2✔
974
        }
975
    }
6✔
976
}
10✔
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