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

paulmthompson / WhiskerToolbox / 17335530769

29 Aug 2025 09:57PM UTC coverage: 66.478% (+0.3%) from 66.194%
17335530769

push

github

paulmthompson
update testing for analog interval threshold

113 of 116 new or added lines in 3 files covered. (97.41%)

103 existing lines in 6 files now uncovered.

27064 of 40711 relevant lines covered (66.48%)

1114.57 hits per line

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

98.97
/src/DataManager/transforms/AnalogTimeSeries/AnalogHilbertPhase/analog_hilbert_phase.test.cpp
1
#define CATCH_CONFIG_MAIN
2
#include "catch2/catch_approx.hpp"
3
#include "catch2/catch_test_macros.hpp"
4
#include "catch2/matchers/catch_matchers_floating_point.hpp"
5
#include "catch2/matchers/catch_matchers_vector.hpp"
6

7
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
8
#include "TimeFrame/TimeFrame.hpp"
9
#include "transforms/AnalogTimeSeries/AnalogHilbertPhase/analog_hilbert_phase.hpp"
10
#include "transforms/data_transforms.hpp"
11

12
#include <cmath>
13
#include <functional>
14
#include <memory>
15
#include <numbers>
16
#include <vector>
17

18
TEST_CASE("Data Transform: Hilbert Phase - Happy Path", "[transforms][analog_hilbert_phase]") {
7✔
19
    std::vector<float> values;
7✔
20
    std::vector<TimeFrameIndex> times;
7✔
21
    std::shared_ptr<AnalogTimeSeries> ats;
7✔
22
    std::shared_ptr<AnalogTimeSeries> result_phase;
7✔
23
    HilbertPhaseParams params;
7✔
24
    int volatile progress_val = -1;
7✔
25
    int volatile call_count = 0;
7✔
26
    ProgressCallback cb = [&](int p) {
14✔
27
        progress_val = p;
7✔
28
        call_count++;
7✔
29
    };
7✔
30

31
    SECTION("Simple sine wave - known phase relationship") {
7✔
32
        // Create a simple sine wave: sin(2*pi*f*t)
33
        // The phase of sin(x) should be x (modulo 2*pi)
34
        constexpr float frequency = 1.0f;       // 1 Hz
1✔
35
        constexpr size_t sample_rate = 100;     // 100 Hz
1✔
36
        constexpr size_t duration_samples = 200;// 2 seconds
1✔
37

38
        values.reserve(duration_samples);
1✔
39
        times.reserve(duration_samples);
1✔
40

41
        for (size_t i = 0; i < duration_samples; ++i) {
201✔
42
            float t = static_cast<float>(i) / sample_rate;
200✔
43
            values.push_back(std::sin(2.0f * std::numbers::pi_v<float> * frequency * t));
200✔
44
            times.push_back(TimeFrameIndex(i));
200✔
45
        }
46

47
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
48
        params.lowFrequency = 0.5;
1✔
49
        params.highFrequency = 2.0;
1✔
50

51
        result_phase = hilbert_phase(ats.get(), params);
1✔
52
        REQUIRE(result_phase != nullptr);
1✔
53
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
54

55
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
56

57
        // Check that phase values are in the expected range [-π, π]
58
        for (auto const & phase: phase_values) {
201✔
59
            REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
60
            REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
61
        }
62

63
        // Test with progress callback
64
        progress_val = -1;
1✔
65
        call_count = 0;
1✔
66
        result_phase = hilbert_phase(ats.get(), params, cb);
1✔
67
        REQUIRE(result_phase != nullptr);
1✔
68
        REQUIRE(progress_val == 100);
1✔
69
        REQUIRE(call_count > 0);
1✔
70
    }
7✔
71

72
    SECTION("Cosine wave - phase should be shifted by π/2 from sine") {
7✔
73
        constexpr float frequency = 2.0f;       // 2 Hz
1✔
74
        constexpr size_t sample_rate = 50;      // 50 Hz
1✔
75
        constexpr size_t duration_samples = 100;// 2 seconds
1✔
76

77
        values.reserve(duration_samples);
1✔
78
        times.reserve(duration_samples);
1✔
79

80
        for (size_t i = 0; i < duration_samples; ++i) {
101✔
81
            float t = static_cast<float>(i) / sample_rate;
100✔
82
            values.push_back(std::cos(2.0f * std::numbers::pi_v<float> * frequency * t));
100✔
83
            times.push_back(TimeFrameIndex(i));
100✔
84
        }
85

86
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
87
        params.lowFrequency = 1.0;
1✔
88
        params.highFrequency = 4.0;
1✔
89

90
        result_phase = hilbert_phase(ats.get(), params);
1✔
91
        REQUIRE(result_phase != nullptr);
1✔
92
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
93

94
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
95

96
        // Check that phase values are in the expected range
97
        for (auto const & phase: phase_values) {
101✔
98
            REQUIRE(phase >= -std::numbers::pi_v<float>);
100✔
99
            REQUIRE(phase <= std::numbers::pi_v<float>);
100✔
100
        }
101
    }
7✔
102

103
    SECTION("Complex signal with multiple frequencies") {
7✔
104
        constexpr size_t sample_rate = 100;
1✔
105
        constexpr size_t duration_samples = 300;
1✔
106

107
        values.reserve(duration_samples);
1✔
108
        times.reserve(duration_samples);
1✔
109

110
        for (size_t i = 0; i < duration_samples; ++i) {
301✔
111
            float t = static_cast<float>(i) / sample_rate;
300✔
112
            // Mix of 2Hz and 5Hz components
113
            float signal = std::sin(2.0f * std::numbers::pi_v<float> * 2.0f * t) +
300✔
114
                           0.5f * std::sin(2.0f * std::numbers::pi_v<float> * 5.0f * t);
300✔
115
            values.push_back(signal);
300✔
116
            times.push_back(TimeFrameIndex(i));
300✔
117
        }
118

119
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
120
        params.lowFrequency = 1.0;
1✔
121
        params.highFrequency = 10.0;
1✔
122

123
        result_phase = hilbert_phase(ats.get(), params);
1✔
124
        REQUIRE(result_phase != nullptr);
1✔
125
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
126

127
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
128
        REQUIRE(phase_values.size() == times.back().getValue() + 1);// Should match output timestamp size
1✔
129

130
        // Verify phase continuity (no large jumps except at wrap-around)
131
        for (size_t i = 1; i < phase_values.size(); ++i) {
300✔
132
            float phase_diff = std::abs(phase_values[i] - phase_values[i - 1]);
299✔
133
            // Allow for phase wrapping around ±π
134
            if (phase_diff > std::numbers::pi_v<float>) {
299✔
135
                phase_diff = 2.0f * std::numbers::pi_v<float> - phase_diff;
6✔
136
            }
137
            REQUIRE(phase_diff < std::numbers::pi_v<float> / 2.0f);// Reasonable continuity
299✔
138
        }
139
    }
7✔
140

141
    SECTION("Discontinuous time series - chunked processing") {
7✔
142
        // Create a discontinuous time series with large gaps
143
        values = {1.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, -1.0f, 0.0f};
1✔
144
        times = {TimeFrameIndex(0),
2✔
145
                 TimeFrameIndex(1),
146
                 TimeFrameIndex(2),
147
                 TimeFrameIndex(3),
148
                 TimeFrameIndex(2000),
149
                 TimeFrameIndex(2001),
150
                 TimeFrameIndex(2002),
151
                 TimeFrameIndex(2003)};// Large gap at 2000
1✔
152

153
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
154
        params.lowFrequency = 5.0;
1✔
155
        params.highFrequency = 15.0;
1✔
156
        params.discontinuityThreshold = 100;// Should split at gap of 2000-3=1997
1✔
157

158
        result_phase = hilbert_phase(ats.get(), params);
1✔
159
        REQUIRE(result_phase != nullptr);
1✔
160
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
161

162
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
163
        REQUIRE(phase_values.size() == times.back().getValue() + 1);// Should match output timestamp size
1✔
164

165
        // Check that phase values are in the expected range
166
        for (auto const & phase: phase_values) {
2,005✔
167
            REQUIRE(phase >= -std::numbers::pi_v<float>);
2,004✔
168
            REQUIRE(phase <= std::numbers::pi_v<float>);
2,004✔
169
        }
170

171
        // Test with progress callback
172
        progress_val = -1;
1✔
173
        call_count = 0;
1✔
174
        result_phase = hilbert_phase(ats.get(), params, cb);
1✔
175
        REQUIRE(result_phase != nullptr);
1✔
176
        REQUIRE(progress_val == 100);
1✔
177
        REQUIRE(call_count > 0);
1✔
178
    }
7✔
179

180
    SECTION("Multiple discontinuities") {
7✔
181
        // Create multiple chunks
182
        values = {1.0f, 0.0f, -1.0f, 1.0f, 0.0f, -1.0f};
1✔
183
        times = {TimeFrameIndex(0),
2✔
184
                 TimeFrameIndex(1),
185
                 TimeFrameIndex(2),
186
                 TimeFrameIndex(1000),
187
                 TimeFrameIndex(1001),
188
                 TimeFrameIndex(2000)};// Two large gaps
1✔
189

190
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
191
        params.lowFrequency = 5.0;
1✔
192
        params.highFrequency = 15.0;
1✔
193
        params.discontinuityThreshold = 100;// Should create 3 chunks
1✔
194

195
        result_phase = hilbert_phase(ats.get(), params);
1✔
196
        REQUIRE(result_phase != nullptr);
1✔
197
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
198

199
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
200
        REQUIRE(phase_values.size() == times.back().getValue() + 1);
1✔
201

202
        // Check that phase values are in the expected range
203
        for (auto const & phase: phase_values) {
2,002✔
204
            REQUIRE(phase >= -std::numbers::pi_v<float>);
2,001✔
205
            REQUIRE(phase <= std::numbers::pi_v<float>);
2,001✔
206
        }
207
    }
7✔
208

209
    SECTION("Progress callback detailed check") {
7✔
210
        values = {1.0f, 0.0f, -1.0f, 0.0f, 1.0f};
1✔
211
        times = {TimeFrameIndex(0),
2✔
212
                 TimeFrameIndex(25),
213
                 TimeFrameIndex(50),
214
                 TimeFrameIndex(75),
215
                 TimeFrameIndex(100)};
1✔
216
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
217
        params.lowFrequency = 5.0;
1✔
218
        params.highFrequency = 15.0;
1✔
219

220
        progress_val = 0;
1✔
221
        call_count = 0;
1✔
222
        std::vector<int> progress_values_seen;
1✔
223
        ProgressCallback detailed_cb = [&](int p) {
2✔
224
            progress_val = p;
3✔
225
            call_count++;
3✔
226
            progress_values_seen.push_back(p);
3✔
227
        };
1✔
228

229
        result_phase = hilbert_phase(ats.get(), params, detailed_cb);
1✔
230
        REQUIRE(progress_val == 100);
1✔
231
        REQUIRE(call_count > 0);
1✔
232

233
        // Check that we see increasing progress values
234
        REQUIRE(!progress_values_seen.empty());
1✔
235
        REQUIRE(progress_values_seen.front() >= 0);
1✔
236
        REQUIRE(progress_values_seen.back() == 100);
1✔
237

238
        // Verify progress is monotonically increasing
239
        for (size_t i = 1; i < progress_values_seen.size(); ++i) {
3✔
240
            REQUIRE(progress_values_seen[i] >= progress_values_seen[i - 1]);
2✔
241
        }
242
    }
8✔
243

244
    SECTION("Default parameters") {
7✔
245
        values = {1.0f, 2.0f, 1.0f, 0.0f, -1.0f};
1✔
246
        times = {TimeFrameIndex(0),
2✔
247
                 TimeFrameIndex(10),
248
                 TimeFrameIndex(20),
249
                 TimeFrameIndex(30),
250
                 TimeFrameIndex(40)};
1✔
251
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
252

253
        // Use default parameters
254
        HilbertPhaseParams default_params;
1✔
255

256
        result_phase = hilbert_phase(ats.get(), default_params);
1✔
257
        REQUIRE(result_phase != nullptr);
1✔
258
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
259

260
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
261
        for (auto const & phase: phase_values) {
42✔
262
            REQUIRE(phase >= -std::numbers::pi_v<float>);
41✔
263
            REQUIRE(phase <= std::numbers::pi_v<float>);
41✔
264
        }
265
    }
8✔
266
}
14✔
267

268
TEST_CASE("Data Transform: Hilbert Phase - Error and Edge Cases", "[transforms][analog_hilbert_phase]") {
10✔
269
    std::shared_ptr<AnalogTimeSeries> ats;
10✔
270
    std::shared_ptr<AnalogTimeSeries> result_phase;
10✔
271
    HilbertPhaseParams params;
10✔
272
    int volatile progress_val = -1;
10✔
273
    int volatile call_count = 0;
10✔
274
    ProgressCallback cb = [&](int p) {
20✔
UNCOV
275
        progress_val = p;
×
UNCOV
276
        call_count++;
×
277
    };
10✔
278

279
    SECTION("Null input AnalogTimeSeries") {
10✔
280
        ats = nullptr;
1✔
281
        params.lowFrequency = 5.0;
1✔
282
        params.highFrequency = 15.0;
1✔
283

284
        result_phase = hilbert_phase(ats.get(), params);
1✔
285
        REQUIRE(result_phase != nullptr);
1✔
286
        REQUIRE(result_phase->getAnalogTimeSeries().empty());
1✔
287

288
        // Test with progress callback
289
        progress_val = -1;
1✔
290
        call_count = 0;
1✔
291
        result_phase = hilbert_phase(ats.get(), params, cb);
1✔
292
        REQUIRE(result_phase != nullptr);
1✔
293
        REQUIRE(result_phase->getAnalogTimeSeries().empty());
1✔
294
        // Progress callback should not be called for null input
295
        REQUIRE(call_count == 0);
1✔
296
    }
10✔
297

298
    SECTION("Empty time series") {
10✔
299
        std::vector<float> empty_values;
1✔
300
        std::vector<TimeFrameIndex> empty_times;
1✔
301
        ats = std::make_shared<AnalogTimeSeries>(empty_values, empty_times);
1✔
302
        params.lowFrequency = 5.0;
1✔
303
        params.highFrequency = 15.0;
1✔
304

305
        result_phase = hilbert_phase(ats.get(), params);
1✔
306
        REQUIRE(result_phase != nullptr);
1✔
307
        REQUIRE(result_phase->getAnalogTimeSeries().empty());
1✔
308
    }
11✔
309

310
    SECTION("Single sample") {
10✔
311
        std::vector<float> values = {1.0f};
3✔
312
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0)};
3✔
313
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
314
        params.lowFrequency = 5.0;
1✔
315
        params.highFrequency = 15.0;
1✔
316

317
        result_phase = hilbert_phase(ats.get(), params);
1✔
318
        REQUIRE(result_phase != nullptr);
1✔
319
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
320

321
        // Single sample should produce a single phase value
322
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
323
        REQUIRE(phase_values.size() == 1);
1✔
324
        REQUIRE(phase_values[0] >= -std::numbers::pi_v<float>);
1✔
325
        REQUIRE(phase_values[0] <= std::numbers::pi_v<float>);
1✔
326
    }
11✔
327

328
    SECTION("Invalid frequency parameters - negative frequencies") {
10✔
329
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
330
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
331
                                             TimeFrameIndex(25),
332
                                             TimeFrameIndex(50),
333
                                             TimeFrameIndex(75)};
3✔
334
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
335

336
        params.lowFrequency = -1.0;// Invalid
1✔
337
        params.highFrequency = 15.0;
1✔
338

339
        result_phase = hilbert_phase(ats.get(), params);
1✔
340
        REQUIRE(result_phase != nullptr);
1✔
341
        // Should still produce output despite invalid parameters
342
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
343
    }
11✔
344

345
    SECTION("Invalid frequency parameters - frequencies too high") {
10✔
346
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
347
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
348
                                             TimeFrameIndex(25),
349
                                             TimeFrameIndex(50),
350
                                             TimeFrameIndex(75)};
3✔
351
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
352

353
        params.lowFrequency = 5.0;
1✔
354
        params.highFrequency = 1000.0;// Way above Nyquist
1✔
355

356
        result_phase = hilbert_phase(ats.get(), params);
1✔
357
        REQUIRE(result_phase != nullptr);
1✔
358
        // Should still produce output despite invalid parameters
359
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
360
    }
11✔
361

362
    SECTION("Invalid frequency parameters - low >= high") {
10✔
363
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
364
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
365
                                             TimeFrameIndex(25),
366
                                             TimeFrameIndex(50),
367
                                             TimeFrameIndex(75)};
3✔
368
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
369

370
        params.lowFrequency = 15.0;
1✔
371
        params.highFrequency = 5.0;// Low > High
1✔
372

373
        result_phase = hilbert_phase(ats.get(), params);
1✔
374
        REQUIRE(result_phase != nullptr);
1✔
375
        // Should still produce output despite invalid parameters
376
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
377
    }
11✔
378

379
    SECTION("Time series with NaN values") {
10✔
380
        std::vector<float> values = {1.0f, std::numeric_limits<float>::quiet_NaN(), -1.0f, 0.0f};
3✔
381
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
382
                                             TimeFrameIndex(25),
383
                                             TimeFrameIndex(50),
384
                                             TimeFrameIndex(75)};
3✔
385
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
386
        params.lowFrequency = 5.0;
1✔
387
        params.highFrequency = 15.0;
1✔
388

389
        result_phase = hilbert_phase(ats.get(), params);
1✔
390
        REQUIRE(result_phase != nullptr);
1✔
391
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
392

393
        // NaN values should be replaced with 0, so computation should succeed
394
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
395
        for (auto const & phase: phase_values) {
77✔
396
            REQUIRE(!std::isnan(phase));
76✔
397
            REQUIRE(phase >= -std::numbers::pi_v<float>);
76✔
398
            REQUIRE(phase <= std::numbers::pi_v<float>);
76✔
399
        }
400
    }
11✔
401

402
    SECTION("Irregular timestamp spacing") {
10✔
403
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f, 1.0f};
3✔
404
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
405
                                             TimeFrameIndex(1),
406
                                             TimeFrameIndex(10),
407
                                             TimeFrameIndex(11),
408
                                             TimeFrameIndex(100)};// Irregular spacing
3✔
409
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
410
        params.lowFrequency = 5.0;
1✔
411
        params.highFrequency = 15.0;
1✔
412

413
        result_phase = hilbert_phase(ats.get(), params);
1✔
414
        REQUIRE(result_phase != nullptr);
1✔
415
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
416

417
        // Should handle irregular spacing gracefully
418
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
419
        REQUIRE(phase_values.size() == times.back().getValue() + 1);// Continuous output
1✔
420
    }
11✔
421

422
    SECTION("Very small discontinuity threshold") {
10✔
423
        // Test with threshold smaller than natural gaps
424
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
425
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
426
                                             TimeFrameIndex(5),
427
                                             TimeFrameIndex(10),
428
                                             TimeFrameIndex(15)};// Gaps of 5
3✔
429
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
430

431
        params.lowFrequency = 5.0;
1✔
432
        params.highFrequency = 15.0;
1✔
433
        params.discontinuityThreshold = 2;// Smaller than gaps
1✔
434

435
        result_phase = hilbert_phase(ats.get(), params);
1✔
436
        REQUIRE(result_phase != nullptr);
1✔
437
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
438

439
        // Should create multiple small chunks
440
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
441
        REQUIRE(phase_values.size() == times.back().getValue() + 1);
1✔
442
    }
11✔
443

444
    SECTION("Very large discontinuity threshold") {
10✔
445
        // Test with threshold larger than any gaps
446
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
447
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
448
                                             TimeFrameIndex(100),
449
                                             TimeFrameIndex(200),
450
                                             TimeFrameIndex(300)};// Large gaps
3✔
451
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
452

453
        params.lowFrequency = 5.0;
1✔
454
        params.highFrequency = 15.0;
1✔
455
        params.discontinuityThreshold = 1000;// Larger than gaps
1✔
456

457
        result_phase = hilbert_phase(ats.get(), params);
1✔
458
        REQUIRE(result_phase != nullptr);
1✔
459
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
460

461
        // Should process as single chunk
462
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
463
        REQUIRE(phase_values.size() == times.back().getValue() + 1);
1✔
464
    }
11✔
465
}
20✔
466

467

468
TEST_CASE("Data Transform: Hilbert Phase - Irregularly Sampled Data", "[transforms][analog_hilbert_phase]") {
3✔
469
    // Create irregularly sampled sine wave with both small and large gaps
470
    std::vector<float> data;
3✔
471
    std::vector<TimeFrameIndex> times;
3✔
472

473
    double sampling_rate = 1000.0;// 1kHz
3✔
474
    double freq = 10.0;           // 10Hz sine wave
3✔
475

476
    // Create data with three segments:
477
    // 1. Dense segment with small gaps (should be interpolated)
478
    // 2. Large gap (should not be interpolated)
479
    // 3. Another dense segment with small gaps
480

481
    // First segment: points at 0,1,3,4,6,7,9,10 (skipping 2,5,8)
482
    for (int i = 0; i <= 10; i++) {
36✔
483
        if (i % 3 == 2) continue;// Skip every third point
33✔
484

485
        double t = i / sampling_rate;
24✔
486
        data.push_back(static_cast<float>(std::sin(2.0 * M_PI * freq * t)));
24✔
487
        times.push_back(TimeFrameIndex(i));
24✔
488
    }
489

490
    // Large gap (100 samples)
491

492
    // Second segment: points at 110,111,113,114,116,117,119,120
493
    for (int i = 110; i <= 120; i++) {
36✔
494
        if (i % 3 == 2) continue;// Skip every third point
33✔
495

496
        double t = i / sampling_rate;
21✔
497
        data.push_back(static_cast<float>(std::sin(2.0 * M_PI * freq * t)));
21✔
498
        times.push_back(TimeFrameIndex(i));
21✔
499
    }
500

501
    AnalogTimeSeries series(data, times);
3✔
502

503
    // Configure Hilbert transform parameters
504
    HilbertPhaseParams params;
3✔
505
    params.lowFrequency = 5.0;
3✔
506
    params.highFrequency = 15.0;
3✔
507
    params.discontinuityThreshold = 10;// Allow interpolation for gaps <= 10 samples
3✔
508

509
    // Apply transform
510
    auto result = hilbert_phase(&series, params);
3✔
511

512
    REQUIRE(result != nullptr);
3✔
513

514
    // Get time indices from result
515
    auto result_times = result->getTimeSeries();
3✔
516
    auto original_times = series.getTimeSeries();
3✔
517

518
    SECTION("Original time points are preserved") {
3✔
519
        // Every original time point should exist in the result
520
        for (auto const & original_time: original_times) {
16✔
521
            INFO("Checking preservation of time index " << original_time.getValue());
15✔
522
            bool found = false;
15✔
523
            for (auto const & result_time: result_times) {
862✔
524
                if (result_time.getValue() == original_time.getValue()) {
862✔
525
                    found = true;
15✔
526
                    break;
15✔
527
                }
528
            }
529
            REQUIRE(found);
15✔
530
        }
15✔
531
    }
3✔
532

533
    SECTION("Small gaps are interpolated") {
3✔
534
        // Check first segment (0-10)
535
        // Should have points for 0,1,2,3,4,5,6,7,8,9,10
536
        for (int i = 0; i <= 10; i++) {
12✔
537
            INFO("Checking interpolation at time " << i);
11✔
538
            bool found = false;
11✔
539
            for (auto const & time: result_times) {
66✔
540
                if (time.getValue() == i) {
66✔
541
                    found = true;
11✔
542
                    break;
11✔
543
                }
544
            }
545
            REQUIRE(found);
11✔
546
        }
11✔
547
    }
3✔
548

549
    /*
550
    TODO: Fix this test
551
    SECTION("Large gaps are not interpolated") {
552
        // Check that points in the large gap (11-109) are not present
553
        for (int i = 11; i < 110; i++) {
554
            INFO("Checking gap at time " << i);
555
            bool found = false;
556
            for (auto const& time : result_times) {
557
                if (time.getValue() == i) {
558
                    found = true;
559
                    break;
560
                }
561
            }
562
            REQUIRE_FALSE(found);
563
        }
564
    }
565
    */
566

567
    SECTION("Data values at original times are preserved") {
3✔
568
        // Get the original and transformed data
569
        auto original_data = series.getAnalogTimeSeries();
1✔
570
        auto result_data = result->getAnalogTimeSeries();
1✔
571

572
        // For each original point, find its corresponding point in the result
573
        for (size_t i = 0; i < original_times.size(); i++) {
16✔
574
            auto time = original_times[i].getValue();
15✔
575
            auto original_value = original_data[i];
15✔
576

577
            // Find this time in result_times
578
            auto it = std::find_if(result_times.begin(), result_times.end(),
15✔
579
                                   [time](TimeFrameIndex const & t) { return t.getValue() == time; });
862✔
580

581
            REQUIRE(it != result_times.end());
15✔
582

583
            size_t result_idx = std::distance(result_times.begin(), it);
15✔
584
            // Note: We don't check exact equality because the Hilbert transform
585
            // modifies values, but we can check the value exists
586
            REQUIRE(std::isfinite(result_data[result_idx]));
15✔
587
        }
588
    }
4✔
589
}
6✔
590

591
#include "DataManager.hpp"
592
#include "IO/LoaderRegistry.hpp"
593
#include "transforms/TransformPipeline.hpp"
594
#include "transforms/TransformRegistry.hpp"
595
#include "transforms/ParameterFactory.hpp"
596

597
#include <filesystem>
598
#include <fstream>
599
#include <iostream>
600

601
TEST_CASE("Data Transform: Analog Hilbert Phase - JSON pipeline", "[transforms][analog_hilbert_phase][json]") {
1✔
602
    const nlohmann::json json_config = {
1✔
603
        {"steps", {{
604
            {"step_id", "hilbert_phase_step_1"},
605
            {"transform_name", "Hilbert Phase"},
606
            {"input_key", "TestSignal.channel1"},
607
            {"output_key", "PhaseSignal"},
608
            {"parameters", {
609
                {"low_frequency", 5.0},
1✔
610
                {"high_frequency", 15.0},
1✔
611
                {"discontinuity_threshold", 1000}
1✔
612
            }}
613
        }}}
614
    };
42✔
615

616
    DataManager dm;
1✔
617
    TransformRegistry registry;
1✔
618

619
    auto time_frame = std::make_shared<TimeFrame>();
1✔
620
    dm.setTime(TimeKey("default"), time_frame);
1✔
621

622
    // Create test analog data - a sine wave
623
    constexpr size_t sample_rate = 100;
1✔
624
    constexpr size_t duration_samples = 200;
1✔
625
    std::vector<float> values;
1✔
626
    std::vector<TimeFrameIndex> times;
1✔
627
    
628
    values.reserve(duration_samples);
1✔
629
    times.reserve(duration_samples);
1✔
630
    
631
    for (size_t i = 0; i < duration_samples; ++i) {
201✔
632
        float t = static_cast<float>(i) / sample_rate;
200✔
633
        values.push_back(std::sin(2.0f * std::numbers::pi_v<float> * 10.0f * t)); // 10 Hz sine wave
200✔
634
        times.push_back(TimeFrameIndex(i));
200✔
635
    }
636
    
637
    auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
638
    ats->setTimeFrame(time_frame);
1✔
639
    dm.setData("TestSignal.channel1", ats, TimeKey("default"));
3✔
640

641
    TransformPipeline pipeline(&dm, &registry);
1✔
642
    pipeline.loadFromJson(json_config);
1✔
643
    pipeline.execute();
1✔
644

645
    // Verify the results
646
    auto phase_series = dm.getData<AnalogTimeSeries>("PhaseSignal");
3✔
647
    REQUIRE(phase_series != nullptr);
1✔
648
    REQUIRE(!phase_series->getAnalogTimeSeries().empty());
1✔
649
    
650
    // Check that phase values are in the expected range [-π, π]
651
    auto const& phase_values = phase_series->getAnalogTimeSeries();
1✔
652
    for (auto const& phase : phase_values) {
201✔
653
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
654
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
655
    }
656
}
45✔
657

658
TEST_CASE("Data Transform: Analog Hilbert Phase - Parameter Factory", "[transforms][analog_hilbert_phase][factory]") {
1✔
659
    auto& factory = ParameterFactory::getInstance();
1✔
660
    factory.initializeDefaultSetters();
1✔
661

662
    auto params_base = std::make_unique<HilbertPhaseParams>();
1✔
663
    REQUIRE(params_base != nullptr);
1✔
664

665
    const nlohmann::json params_json = {
1✔
666
        {"low_frequency", 2.5},
1✔
667
        {"high_frequency", 25.0},
1✔
668
        {"discontinuity_threshold", 500}
1✔
669
    };
14✔
670

671
    for (auto const& [key, val] : params_json.items()) {
4✔
672
        factory.setParameter("Hilbert Phase", params_base.get(), key, val, nullptr);
9✔
673
    }
1✔
674

675
    auto* params = dynamic_cast<HilbertPhaseParams*>(params_base.get());
1✔
676
    REQUIRE(params != nullptr);
1✔
677

678
    REQUIRE(params->lowFrequency == 2.5);
1✔
679
    REQUIRE(params->highFrequency == 25.0);
1✔
680
    REQUIRE(params->discontinuityThreshold == 500);
1✔
681
}
15✔
682

683
TEST_CASE("Data Transform: Analog Hilbert Phase - load_data_from_json_config", "[transforms][analog_hilbert_phase][json_config]") {
1✔
684
    // Create DataManager and populate it with AnalogTimeSeries in code
685
    DataManager dm;
1✔
686

687
    // Create a TimeFrame for our data
688
    auto time_frame = std::make_shared<TimeFrame>();
1✔
689
    dm.setTime(TimeKey("default"), time_frame);
1✔
690
    
691
    // Create test analog data - a sine wave
692
    constexpr size_t sample_rate = 100;
1✔
693
    constexpr size_t duration_samples = 200;
1✔
694
    std::vector<float> values;
1✔
695
    std::vector<TimeFrameIndex> times;
1✔
696
    
697
    values.reserve(duration_samples);
1✔
698
    times.reserve(duration_samples);
1✔
699
    
700
    for (size_t i = 0; i < duration_samples; ++i) {
201✔
701
        float t = static_cast<float>(i) / sample_rate;
200✔
702
        values.push_back(std::sin(2.0f * std::numbers::pi_v<float> * 10.0f * t)); // 10 Hz sine wave
200✔
703
        times.push_back(TimeFrameIndex(i));
200✔
704
    }
705
    
706
    auto test_analog = std::make_shared<AnalogTimeSeries>(values, times);
1✔
707
    test_analog->setTimeFrame(time_frame);
1✔
708
    
709
    // Store the analog data in DataManager with a known key
710
    dm.setData("test_signal", test_analog, TimeKey("default"));
3✔
711
    
712
    // Create JSON configuration for transformation pipeline using unified format
713
    const char* json_config = 
1✔
714
        "[\n"
715
        "{\n"
716
        "    \"transformations\": {\n"
717
        "        \"metadata\": {\n"
718
        "            \"name\": \"Hilbert Phase Pipeline\",\n"
719
        "            \"description\": \"Test Hilbert phase calculation on analog signal\",\n"
720
        "            \"version\": \"1.0\"\n"
721
        "        },\n"
722
        "        \"steps\": [\n"
723
        "            {\n"
724
        "                \"step_id\": \"1\",\n"
725
        "                \"transform_name\": \"Hilbert Phase\",\n"
726
        "                \"phase\": \"analysis\",\n"
727
        "                \"input_key\": \"test_signal\",\n"
728
        "                \"output_key\": \"phase_signal\",\n"
729
        "                \"parameters\": {\n"
730
        "                    \"low_frequency\": 5.0,\n"
731
        "                    \"high_frequency\": 15.0,\n"
732
        "                    \"discontinuity_threshold\": 1000\n"
733
        "                }\n"
734
        "            }\n"
735
        "        ]\n"
736
        "    }\n"
737
        "}\n"
738
        "]";
739
    
740
    // Create temporary directory and write JSON config to file
741
    std::filesystem::path test_dir = std::filesystem::temp_directory_path() / "analog_hilbert_phase_pipeline_test";
1✔
742
    std::filesystem::create_directories(test_dir);
1✔
743
    
744
    std::filesystem::path json_filepath = test_dir / "pipeline_config.json";
1✔
745
    {
746
        std::ofstream json_file(json_filepath);
1✔
747
        REQUIRE(json_file.is_open());
1✔
748
        json_file << json_config;
1✔
749
        json_file.close();
1✔
750
    }
1✔
751
    
752
    // Execute the transformation pipeline using load_data_from_json_config
753
    auto data_info_list = load_data_from_json_config(&dm, json_filepath.string());
1✔
754
    
755
    // Verify the transformation was executed and results are available
756
    auto result_phase = dm.getData<AnalogTimeSeries>("phase_signal");
3✔
757
    REQUIRE(result_phase != nullptr);
1✔
758
    REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
759
    
760
    // Verify the phase calculation results
761
    auto const& phase_values = result_phase->getAnalogTimeSeries();
1✔
762
    for (auto const& phase : phase_values) {
201✔
763
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
764
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
765
    }
766
    
767
    // Test another pipeline with different parameters (different frequency band)
768
    const char* json_config_wideband = 
1✔
769
        "[\n"
770
        "{\n"
771
        "    \"transformations\": {\n"
772
        "        \"metadata\": {\n"
773
        "            \"name\": \"Hilbert Phase Wideband\",\n"
774
        "            \"description\": \"Test Hilbert phase with wider frequency band\",\n"
775
        "            \"version\": \"1.0\"\n"
776
        "        },\n"
777
        "        \"steps\": [\n"
778
        "            {\n"
779
        "                \"step_id\": \"1\",\n"
780
        "                \"transform_name\": \"Hilbert Phase\",\n"
781
        "                \"phase\": \"analysis\",\n"
782
        "                \"input_key\": \"test_signal\",\n"
783
        "                \"output_key\": \"phase_signal_wideband\",\n"
784
        "                \"parameters\": {\n"
785
        "                    \"low_frequency\": 1.0,\n"
786
        "                    \"high_frequency\": 50.0,\n"
787
        "                    \"discontinuity_threshold\": 1000\n"
788
        "                }\n"
789
        "            }\n"
790
        "        ]\n"
791
        "    }\n"
792
        "}\n"
793
        "]";
794
    
795
    std::filesystem::path json_filepath_wideband = test_dir / "pipeline_config_wideband.json";
1✔
796
    {
797
        std::ofstream json_file(json_filepath_wideband);
1✔
798
        REQUIRE(json_file.is_open());
1✔
799
        json_file << json_config_wideband;
1✔
800
        json_file.close();
1✔
801
    }
1✔
802
    
803
    // Execute the wideband pipeline
804
    auto data_info_list_wideband = load_data_from_json_config(&dm, json_filepath_wideband.string());
1✔
805
    
806
    // Verify the wideband results
807
    auto result_phase_wideband = dm.getData<AnalogTimeSeries>("phase_signal_wideband");
3✔
808
    REQUIRE(result_phase_wideband != nullptr);
1✔
809
    REQUIRE(!result_phase_wideband->getAnalogTimeSeries().empty());
1✔
810
    
811
    auto const& phase_values_wideband = result_phase_wideband->getAnalogTimeSeries();
1✔
812
    for (auto const& phase : phase_values_wideband) {
201✔
813
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
814
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
815
    }
816
    
817
    // Test with smaller discontinuity threshold
818
    const char* json_config_small_threshold = 
1✔
819
        "[\n"
820
        "{\n"
821
        "    \"transformations\": {\n"
822
        "        \"metadata\": {\n"
823
        "            \"name\": \"Hilbert Phase Small Threshold\",\n"
824
        "            \"description\": \"Test Hilbert phase with small discontinuity threshold\",\n"
825
        "            \"version\": \"1.0\"\n"
826
        "        },\n"
827
        "        \"steps\": [\n"
828
        "            {\n"
829
        "                \"step_id\": \"1\",\n"
830
        "                \"transform_name\": \"Hilbert Phase\",\n"
831
        "                \"phase\": \"analysis\",\n"
832
        "                \"input_key\": \"test_signal\",\n"
833
        "                \"output_key\": \"phase_signal_small_threshold\",\n"
834
        "                \"parameters\": {\n"
835
        "                    \"low_frequency\": 5.0,\n"
836
        "                    \"high_frequency\": 15.0,\n"
837
        "                    \"discontinuity_threshold\": 10\n"
838
        "                }\n"
839
        "            }\n"
840
        "        ]\n"
841
        "    }\n"
842
        "}\n"
843
        "]";
844
    
845
    std::filesystem::path json_filepath_small_threshold = test_dir / "pipeline_config_small_threshold.json";
1✔
846
    {
847
        std::ofstream json_file(json_filepath_small_threshold);
1✔
848
        REQUIRE(json_file.is_open());
1✔
849
        json_file << json_config_small_threshold;
1✔
850
        json_file.close();
1✔
851
    }
1✔
852
    
853
    // Execute the small threshold pipeline
854
    auto data_info_list_small_threshold = load_data_from_json_config(&dm, json_filepath_small_threshold.string());
1✔
855
    
856
    // Verify the small threshold results
857
    auto result_phase_small_threshold = dm.getData<AnalogTimeSeries>("phase_signal_small_threshold");
3✔
858
    REQUIRE(result_phase_small_threshold != nullptr);
1✔
859
    REQUIRE(!result_phase_small_threshold->getAnalogTimeSeries().empty());
1✔
860
    
861
    auto const& phase_values_small_threshold = result_phase_small_threshold->getAnalogTimeSeries();
1✔
862
    for (auto const& phase : phase_values_small_threshold) {
201✔
863
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
864
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
865
    }
866
    
867
    // Cleanup
868
    try {
869
        std::filesystem::remove_all(test_dir);
1✔
UNCOV
870
    } catch (const std::exception& e) {
×
UNCOV
871
        std::cerr << "Warning: Cleanup failed: " << e.what() << std::endl;
×
UNCOV
872
    }
×
873
}
2✔
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