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

paulmthompson / WhiskerToolbox / 18762825348

23 Oct 2025 09:42PM UTC coverage: 72.822% (+0.3%) from 72.522%
18762825348

push

github

paulmthompson
add boolean digital interval logic test

693 of 711 new or added lines in 5 files covered. (97.47%)

718 existing lines in 10 files now uncovered.

54997 of 75522 relevant lines covered (72.82%)

45740.9 hits per line

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

99.1
/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]") {
11✔
19
    std::vector<float> values;
11✔
20
    std::vector<TimeFrameIndex> times;
11✔
21
    std::shared_ptr<AnalogTimeSeries> ats;
11✔
22
    std::shared_ptr<AnalogTimeSeries> result_phase;
11✔
23
    HilbertPhaseParams params;
11✔
24
    int volatile progress_val = -1;
11✔
25
    int volatile call_count = 0;
11✔
26
    ProgressCallback cb = [&](int p) {
22✔
27
        progress_val = p;
7✔
28
        call_count++;
7✔
29
    };
11✔
30

31
    SECTION("Simple sine wave - known phase relationship") {
11✔
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
        // lowFrequency parameter removed (was stub)
49
        // highFrequency parameter removed (was stub)
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
    }
11✔
71

72
    SECTION("Cosine wave - phase should be shifted by π/2 from sine") {
11✔
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
        // lowFrequency parameter removed (was stub)
88
        // highFrequency parameter removed (was stub)
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
    }
11✔
102

103
    SECTION("Complex signal with multiple frequencies") {
11✔
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
        // lowFrequency parameter removed (was stub)
121
        // highFrequency parameter removed (was stub)
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
    }
11✔
140

141
    SECTION("Discontinuous time series - chunked processing") {
11✔
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
        // lowFrequency parameter removed (was stub)
155
        // highFrequency parameter removed (was stub)
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
    }
11✔
179

180
    SECTION("Multiple discontinuities") {
11✔
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
        // lowFrequency parameter removed (was stub)
192
        // highFrequency parameter removed (was stub)
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
    }
11✔
208

209
    SECTION("Progress callback detailed check") {
11✔
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
        // lowFrequency parameter removed (was stub)
218
        // highFrequency parameter removed (was stub)
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
    }
12✔
243

244
    SECTION("Default parameters") {
11✔
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
    }
12✔
266

267
    SECTION("Amplitude extraction - simple sine wave") {
11✔
268
        // Create a simple sine wave with known amplitude
269
        constexpr float amplitude = 2.5f;
1✔
270
        constexpr float frequency = 1.0f;       // 1 Hz
1✔
271
        constexpr size_t sample_rate = 100;     // 100 Hz
1✔
272
        constexpr size_t duration_samples = 200;// 2 seconds
1✔
273

274
        values.reserve(duration_samples);
1✔
275
        times.reserve(duration_samples);
1✔
276

277
        for (size_t i = 0; i < duration_samples; ++i) {
201✔
278
            float t = static_cast<float>(i) / sample_rate;
200✔
279
            values.push_back(amplitude * std::sin(2.0f * std::numbers::pi_v<float> * frequency * t));
200✔
280
            times.push_back(TimeFrameIndex(i));
200✔
281
        }
282

283
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
284
        // lowFrequency parameter removed (was stub)
285
        // highFrequency parameter removed (was stub)
286
        params.outputType = HilbertPhaseParams::OutputType::Amplitude;
1✔
287

288
        auto result_amplitude = hilbert_phase(ats.get(), params);
1✔
289
        REQUIRE(result_amplitude != nullptr);
1✔
290
        REQUIRE(!result_amplitude->getAnalogTimeSeries().empty());
1✔
291

292
        auto const & amplitude_values = result_amplitude->getAnalogTimeSeries();
1✔
293

294
        // Amplitude should be non-negative and close to the expected amplitude
295
        for (auto const & amp: amplitude_values) {
201✔
296
            REQUIRE(amp >= 0.0f);
200✔
297
        }
298

299
        // Check that most amplitude values are close to the expected amplitude (within 20% tolerance)
300
        size_t count_within_tolerance = 0;
1✔
301
        for (auto const & amp: amplitude_values) {
201✔
302
            if (amp > 0.1f && std::abs(amp - amplitude) < amplitude * 0.2f) {
200✔
303
                count_within_tolerance++;
200✔
304
            }
305
        }
306
        // At least 70% of non-zero values should be close to expected amplitude
307
        REQUIRE(count_within_tolerance > amplitude_values.size() * 0.7);
1✔
308
    }
12✔
309

310
    SECTION("Amplitude extraction - amplitude modulated signal") {
11✔
311
        // Create an amplitude modulated signal
312
        constexpr float carrier_freq = 10.0f;
1✔
313
        constexpr float modulation_freq = 1.0f;
1✔
314
        constexpr size_t sample_rate = 100;
1✔
315
        constexpr size_t duration_samples = 200;
1✔
316

317
        values.reserve(duration_samples);
1✔
318
        times.reserve(duration_samples);
1✔
319

320
        for (size_t i = 0; i < duration_samples; ++i) {
201✔
321
            float t = static_cast<float>(i) / sample_rate;
200✔
322
            float envelope = 1.0f + 0.5f * std::sin(2.0f * std::numbers::pi_v<float> * modulation_freq * t);
200✔
323
            float carrier = std::sin(2.0f * std::numbers::pi_v<float> * carrier_freq * t);
200✔
324
            values.push_back(envelope * carrier);
200✔
325
            times.push_back(TimeFrameIndex(i));
200✔
326
        }
327

328
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
329
        // lowFrequency parameter removed (was stub)
330
        // highFrequency parameter removed (was stub)
331
        params.outputType = HilbertPhaseParams::OutputType::Amplitude;
1✔
332

333
        auto result_amplitude = hilbert_phase(ats.get(), params);
1✔
334
        REQUIRE(result_amplitude != nullptr);
1✔
335
        REQUIRE(!result_amplitude->getAnalogTimeSeries().empty());
1✔
336

337
        auto const & amplitude_values = result_amplitude->getAnalogTimeSeries();
1✔
338

339
        // Amplitude should be non-negative
340
        for (auto const & amp: amplitude_values) {
201✔
341
            REQUIRE(amp >= 0.0f);
200✔
342
        }
343

344
        // The amplitude should vary between approximately 0.5 and 1.5 (the envelope)
345
        float min_amp = *std::min_element(amplitude_values.begin(), amplitude_values.end());
1✔
346
        float max_amp = *std::max_element(amplitude_values.begin(), amplitude_values.end());
1✔
347
        
348
        // Filter out zeros for this check
349
        std::vector<float> non_zero_amps;
1✔
350
        for (auto amp : amplitude_values) {
201✔
351
            if (amp > 0.1f) {
200✔
352
                non_zero_amps.push_back(amp);
200✔
353
            }
354
        }
355
        
356
        if (!non_zero_amps.empty()) {
1✔
357
            min_amp = *std::min_element(non_zero_amps.begin(), non_zero_amps.end());
1✔
358
            max_amp = *std::max_element(non_zero_amps.begin(), non_zero_amps.end());
1✔
359
            REQUIRE(min_amp < 1.0f);  // Should have values below 1
1✔
360
            REQUIRE(max_amp > 1.0f);  // Should have values above 1
1✔
361
        }
362
    }
12✔
363

364
    SECTION("Amplitude extraction with discontinuities") {
11✔
365
        // Create a discontinuous signal with varying amplitudes
366
        values = {1.0f, 0.5f, -1.0f, 0.5f, 2.0f, 1.0f, -2.0f, 1.0f};
1✔
367
        times = {TimeFrameIndex(0),
2✔
368
                 TimeFrameIndex(1),
369
                 TimeFrameIndex(2),
370
                 TimeFrameIndex(3),
371
                 TimeFrameIndex(2000),
372
                 TimeFrameIndex(2001),
373
                 TimeFrameIndex(2002),
374
                 TimeFrameIndex(2003)};
1✔
375

376
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
377
        // lowFrequency parameter removed (was stub)
378
        // highFrequency parameter removed (was stub)
379
        params.discontinuityThreshold = 100;
1✔
380
        params.outputType = HilbertPhaseParams::OutputType::Amplitude;
1✔
381

382
        auto result_amplitude = hilbert_phase(ats.get(), params);
1✔
383
        REQUIRE(result_amplitude != nullptr);
1✔
384
        REQUIRE(!result_amplitude->getAnalogTimeSeries().empty());
1✔
385

386
        auto const & amplitude_values = result_amplitude->getAnalogTimeSeries();
1✔
387
        REQUIRE(amplitude_values.size() == times.back().getValue() + 1);
1✔
388

389
        // All amplitude values should be non-negative
390
        for (auto const & amp: amplitude_values) {
2,005✔
391
            REQUIRE(amp >= 0.0f);
2,004✔
392
        }
393
    }
12✔
394

395
    SECTION("Windowed processing - long signal") {
11✔
396
        // Create a long sine wave to test windowed processing
397
        constexpr float amplitude = 2.0f;
1✔
398
        constexpr float frequency = 5.0f;
1✔
399
        constexpr size_t sample_rate = 1000; // 1 kHz
1✔
400
        constexpr size_t duration_samples = 150000; // 150 seconds of data
1✔
401

402
        values.reserve(duration_samples);
1✔
403
        times.reserve(duration_samples);
1✔
404

405
        for (size_t i = 0; i < duration_samples; ++i) {
150,001✔
406
            float t = static_cast<float>(i) / sample_rate;
150,000✔
407
            values.push_back(amplitude * std::sin(2.0f * std::numbers::pi_v<float> * frequency * t));
150,000✔
408
            times.push_back(TimeFrameIndex(i));
150,000✔
409
        }
410

411
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
412
        
413
        // Configure for windowed processing
414
        // lowFrequency parameter removed (was stub)
415
        // highFrequency parameter removed (was stub)
416
        params.outputType = HilbertPhaseParams::OutputType::Amplitude;
1✔
417
        params.maxChunkSize = 10000; // Process in 10k sample chunks
1✔
418
        params.overlapFraction = 0.25;
1✔
419
        params.useWindowing = false; // Disable Hann windowing - overlap-add handles edges
1✔
420

421
        auto result_amplitude = hilbert_phase(ats.get(), params);
1✔
422
        REQUIRE(result_amplitude != nullptr);
1✔
423
        REQUIRE(!result_amplitude->getAnalogTimeSeries().empty());
1✔
424

425
        auto const & amplitude_values = result_amplitude->getAnalogTimeSeries();
1✔
426

427
        // Amplitude should be non-negative and close to expected amplitude
428
        for (auto const & amp: amplitude_values) {
150,001✔
429
            REQUIRE(amp >= 0.0f);
150,000✔
430
        }
431

432
        // Check that most amplitude values are close to the expected amplitude
433
        size_t count_within_tolerance = 0;
1✔
434
        size_t count_non_zero = 0;
1✔
435
        for (auto const & amp: amplitude_values) {
150,001✔
436
            if (amp > 0.1f) {
150,000✔
437
                count_non_zero++;
150,000✔
438
                if (std::abs(amp - amplitude) < amplitude * 0.3f) {
150,000✔
439
                    count_within_tolerance++;
149,979✔
440
                }
441
            }
442
        }
443
        // At least 95% of non-zero values should be close to expected amplitude
444
        // (overlap-add with edge discarding handles boundaries cleanly)
445
        REQUIRE(count_non_zero > 0);
1✔
446
        REQUIRE(static_cast<double>(count_within_tolerance) / count_non_zero > 0.95);
1✔
447
    }
12✔
448
}
22✔
449

450

451
TEST_CASE("Data Transform: Hilbert Phase - Error and Edge Cases", "[transforms][analog_hilbert_phase]") {
10✔
452
    std::shared_ptr<AnalogTimeSeries> ats;
10✔
453
    std::shared_ptr<AnalogTimeSeries> result_phase;
10✔
454
    HilbertPhaseParams params;
10✔
455
    int volatile progress_val = -1;
10✔
456
    int volatile call_count = 0;
10✔
457
    ProgressCallback cb = [&](int p) {
20✔
UNCOV
458
        progress_val = p;
×
UNCOV
459
        call_count++;
×
460
    };
10✔
461

462
    SECTION("Null input AnalogTimeSeries") {
10✔
463
        ats = nullptr;
1✔
464
        // lowFrequency parameter removed (was stub)
465
        // highFrequency parameter removed (was stub)
466

467
        result_phase = hilbert_phase(ats.get(), params);
1✔
468
        REQUIRE(result_phase != nullptr);
1✔
469
        REQUIRE(result_phase->getAnalogTimeSeries().empty());
1✔
470

471
        // Test with progress callback
472
        progress_val = -1;
1✔
473
        call_count = 0;
1✔
474
        result_phase = hilbert_phase(ats.get(), params, cb);
1✔
475
        REQUIRE(result_phase != nullptr);
1✔
476
        REQUIRE(result_phase->getAnalogTimeSeries().empty());
1✔
477
        // Progress callback should not be called for null input
478
        REQUIRE(call_count == 0);
1✔
479
    }
10✔
480

481
    SECTION("Empty time series") {
10✔
482
        std::vector<float> empty_values;
1✔
483
        std::vector<TimeFrameIndex> empty_times;
1✔
484
        ats = std::make_shared<AnalogTimeSeries>(empty_values, empty_times);
1✔
485
        // lowFrequency parameter removed (was stub)
486
        // highFrequency parameter removed (was stub)
487

488
        result_phase = hilbert_phase(ats.get(), params);
1✔
489
        REQUIRE(result_phase != nullptr);
1✔
490
        REQUIRE(result_phase->getAnalogTimeSeries().empty());
1✔
491
    }
11✔
492

493
    SECTION("Single sample") {
10✔
494
        std::vector<float> values = {1.0f};
3✔
495
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0)};
3✔
496
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
497
        // lowFrequency parameter removed (was stub)
498
        // highFrequency parameter removed (was stub)
499

500
        result_phase = hilbert_phase(ats.get(), params);
1✔
501
        REQUIRE(result_phase != nullptr);
1✔
502
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
503

504
        // Single sample should produce a single phase value
505
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
506
        REQUIRE(phase_values.size() == 1);
1✔
507
        REQUIRE(phase_values[0] >= -std::numbers::pi_v<float>);
1✔
508
        REQUIRE(phase_values[0] <= std::numbers::pi_v<float>);
1✔
509
    }
11✔
510

511
    SECTION("Invalid frequency parameters - negative frequencies") {
10✔
512
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
513
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
514
                                             TimeFrameIndex(25),
515
                                             TimeFrameIndex(50),
516
                                             TimeFrameIndex(75)};
3✔
517
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
518

519
        // lowFrequency parameter removed (was stub) - Invalid negative frequency test no longer applicable
520
        // highFrequency parameter removed (was stub)
521

522
        result_phase = hilbert_phase(ats.get(), params);
1✔
523
        REQUIRE(result_phase != nullptr);
1✔
524
        // Should still produce output despite invalid parameters
525
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
526
    }
11✔
527

528
    SECTION("Invalid frequency parameters - frequencies too high") {
10✔
529
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
530
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
531
                                             TimeFrameIndex(25),
532
                                             TimeFrameIndex(50),
533
                                             TimeFrameIndex(75)};
3✔
534
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
535

536
        // lowFrequency parameter removed (was stub)
537
        // highFrequency parameter removed (was stub) - High frequency test no longer applicable
538

539
        result_phase = hilbert_phase(ats.get(), params);
1✔
540
        REQUIRE(result_phase != nullptr);
1✔
541
        // Should still produce output despite invalid parameters
542
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
543
    }
11✔
544

545
    SECTION("Invalid frequency parameters - low >= high") {
10✔
546
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
547
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
548
                                             TimeFrameIndex(25),
549
                                             TimeFrameIndex(50),
550
                                             TimeFrameIndex(75)};
3✔
551
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
552

553
        // lowFrequency parameter removed (was stub)
554
        // highFrequency parameter removed (was stub) - Low > High test no longer applicable
555

556
        result_phase = hilbert_phase(ats.get(), params);
1✔
557
        REQUIRE(result_phase != nullptr);
1✔
558
        // Should still produce output despite invalid parameters
559
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
560
    }
11✔
561

562
    SECTION("Time series with NaN values") {
10✔
563
        std::vector<float> values = {1.0f, std::numeric_limits<float>::quiet_NaN(), -1.0f, 0.0f};
3✔
564
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
565
                                             TimeFrameIndex(25),
566
                                             TimeFrameIndex(50),
567
                                             TimeFrameIndex(75)};
3✔
568
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
569
        // lowFrequency parameter removed (was stub)
570
        // highFrequency parameter removed (was stub)
571

572
        result_phase = hilbert_phase(ats.get(), params);
1✔
573
        REQUIRE(result_phase != nullptr);
1✔
574
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
575

576
        // NaN values should be replaced with 0, so computation should succeed
577
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
578
        for (auto const & phase: phase_values) {
77✔
579
            REQUIRE(!std::isnan(phase));
76✔
580
            REQUIRE(phase >= -std::numbers::pi_v<float>);
76✔
581
            REQUIRE(phase <= std::numbers::pi_v<float>);
76✔
582
        }
583
    }
11✔
584

585
    SECTION("Irregular timestamp spacing") {
10✔
586
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f, 1.0f};
3✔
587
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
588
                                             TimeFrameIndex(1),
589
                                             TimeFrameIndex(10),
590
                                             TimeFrameIndex(11),
591
                                             TimeFrameIndex(100)};// Irregular spacing
3✔
592
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
593
        // lowFrequency parameter removed (was stub)
594
        // highFrequency parameter removed (was stub)
595

596
        result_phase = hilbert_phase(ats.get(), params);
1✔
597
        REQUIRE(result_phase != nullptr);
1✔
598
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
599

600
        // Should handle irregular spacing gracefully
601
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
602
        REQUIRE(phase_values.size() == times.back().getValue() + 1);// Continuous output
1✔
603
    }
11✔
604

605
    SECTION("Very small discontinuity threshold") {
10✔
606
        // Test with threshold smaller than natural gaps
607
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
608
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
609
                                             TimeFrameIndex(5),
610
                                             TimeFrameIndex(10),
611
                                             TimeFrameIndex(15)};// Gaps of 5
3✔
612
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
613

614
        // lowFrequency parameter removed (was stub)
615
        // highFrequency parameter removed (was stub)
616
        params.discontinuityThreshold = 2;// Smaller than gaps
1✔
617

618
        result_phase = hilbert_phase(ats.get(), params);
1✔
619
        REQUIRE(result_phase != nullptr);
1✔
620
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
621

622
        // Should create multiple small chunks
623
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
624
        REQUIRE(phase_values.size() == times.back().getValue() + 1);
1✔
625
    }
11✔
626

627
    SECTION("Very large discontinuity threshold") {
10✔
628
        // Test with threshold larger than any gaps
629
        std::vector<float> values = {1.0f, 0.0f, -1.0f, 0.0f};
3✔
630
        std::vector<TimeFrameIndex> times = {TimeFrameIndex(0),
1✔
631
                                             TimeFrameIndex(100),
632
                                             TimeFrameIndex(200),
633
                                             TimeFrameIndex(300)};// Large gaps
3✔
634
        ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
635

636
        // lowFrequency parameter removed (was stub)
637
        // highFrequency parameter removed (was stub)
638
        params.discontinuityThreshold = 1000;// Larger than gaps
1✔
639

640
        result_phase = hilbert_phase(ats.get(), params);
1✔
641
        REQUIRE(result_phase != nullptr);
1✔
642
        REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
643

644
        // Should process as single chunk
645
        auto const & phase_values = result_phase->getAnalogTimeSeries();
1✔
646
        REQUIRE(phase_values.size() == times.back().getValue() + 1);
1✔
647
    }
11✔
648
}
20✔
649

650

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

656
    double sampling_rate = 1000.0;// 1kHz
3✔
657
    double freq = 10.0;           // 10Hz sine wave
3✔
658

659
    // Create data with three segments:
660
    // 1. Dense segment with small gaps (should be interpolated)
661
    // 2. Large gap (should not be interpolated)
662
    // 3. Another dense segment with small gaps
663

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

668
        double t = i / sampling_rate;
24✔
669
        data.push_back(static_cast<float>(std::sin(2.0 * M_PI * freq * t)));
24✔
670
        times.push_back(TimeFrameIndex(i));
24✔
671
    }
672

673
    // Large gap (100 samples)
674

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

679
        double t = i / sampling_rate;
21✔
680
        data.push_back(static_cast<float>(std::sin(2.0 * M_PI * freq * t)));
21✔
681
        times.push_back(TimeFrameIndex(i));
21✔
682
    }
683

684
    AnalogTimeSeries series(data, times);
3✔
685

686
    // Configure Hilbert transform parameters
687
    HilbertPhaseParams params;
3✔
688
    // lowFrequency parameter removed (was stub)
689
    // highFrequency parameter removed (was stub)
690
    params.discontinuityThreshold = 10;// Allow interpolation for gaps <= 10 samples
3✔
691

692
    // Apply transform
693
    auto result = hilbert_phase(&series, params);
3✔
694

695
    REQUIRE(result != nullptr);
3✔
696

697
    // Get time indices from result
698
    auto result_times = result->getTimeSeries();
3✔
699
    auto original_times = series.getTimeSeries();
3✔
700

701
    SECTION("Original time points are preserved") {
3✔
702
        // Every original time point should exist in the result
703
        for (auto const & original_time: original_times) {
16✔
704
            INFO("Checking preservation of time index " << original_time.getValue());
15✔
705
            bool found = false;
15✔
706
            for (auto const & result_time: result_times) {
862✔
707
                if (result_time.getValue() == original_time.getValue()) {
862✔
708
                    found = true;
15✔
709
                    break;
15✔
710
                }
711
            }
712
            REQUIRE(found);
15✔
713
        }
15✔
714
    }
3✔
715

716
    SECTION("Small gaps are interpolated") {
3✔
717
        // Check first segment (0-10)
718
        // Should have points for 0,1,2,3,4,5,6,7,8,9,10
719
        for (int i = 0; i <= 10; i++) {
12✔
720
            INFO("Checking interpolation at time " << i);
11✔
721
            bool found = false;
11✔
722
            for (auto const & time: result_times) {
66✔
723
                if (time.getValue() == i) {
66✔
724
                    found = true;
11✔
725
                    break;
11✔
726
                }
727
            }
728
            REQUIRE(found);
11✔
729
        }
11✔
730
    }
3✔
731

732
    /*
733
    TODO: Fix this test
734
    SECTION("Large gaps are not interpolated") {
735
        // Check that points in the large gap (11-109) are not present
736
        for (int i = 11; i < 110; i++) {
737
            INFO("Checking gap at time " << i);
738
            bool found = false;
739
            for (auto const& time : result_times) {
740
                if (time.getValue() == i) {
741
                    found = true;
742
                    break;
743
                }
744
            }
745
            REQUIRE_FALSE(found);
746
        }
747
    }
748
    */
749

750
    SECTION("Data values at original times are preserved") {
3✔
751
        // Get the original and transformed data
752
        auto original_data = series.getAnalogTimeSeries();
1✔
753
        auto result_data = result->getAnalogTimeSeries();
1✔
754

755
        // For each original point, find its corresponding point in the result
756
        for (size_t i = 0; i < original_times.size(); i++) {
16✔
757
            auto time = original_times[i].getValue();
15✔
758
            auto original_value = original_data[i];
15✔
759

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

764
            REQUIRE(it != result_times.end());
15✔
765

766
            size_t result_idx = std::distance(result_times.begin(), it);
15✔
767
            // Note: We don't check exact equality because the Hilbert transform
768
            // modifies values, but we can check the value exists
769
            REQUIRE(std::isfinite(result_data[result_idx]));
15✔
770
        }
771
    }
4✔
772
}
6✔
773

774
#include "DataManager.hpp"
775
#include "IO/LoaderRegistry.hpp"
776
#include "transforms/TransformPipeline.hpp"
777
#include "transforms/TransformRegistry.hpp"
778
#include "transforms/ParameterFactory.hpp"
779

780
#include <filesystem>
781
#include <fstream>
782
#include <iostream>
783

784
TEST_CASE("Data Transform: Analog Hilbert Phase - JSON pipeline", "[transforms][analog_hilbert_phase][json]") {
1✔
785
    const nlohmann::json json_config = {
1✔
786
        {"steps", {{
787
            {"step_id", "hilbert_phase_step_1"},
788
            {"transform_name", "Hilbert Phase"},
789
            {"input_key", "TestSignal.channel1"},
790
            {"output_key", "PhaseSignal"},
791
            {"parameters", {
792
                {"low_frequency", 5.0},
1✔
793
                {"high_frequency", 15.0},
1✔
794
                {"discontinuity_threshold", 1000}
1✔
795
            }}
796
        }}}
797
    };
42✔
798

799
    DataManager dm;
1✔
800
    TransformRegistry registry;
1✔
801

802
    auto time_frame = std::make_shared<TimeFrame>();
1✔
803
    dm.setTime(TimeKey("default"), time_frame);
1✔
804

805
    // Create test analog data - a sine wave
806
    constexpr size_t sample_rate = 100;
1✔
807
    constexpr size_t duration_samples = 200;
1✔
808
    std::vector<float> values;
1✔
809
    std::vector<TimeFrameIndex> times;
1✔
810
    
811
    values.reserve(duration_samples);
1✔
812
    times.reserve(duration_samples);
1✔
813
    
814
    for (size_t i = 0; i < duration_samples; ++i) {
201✔
815
        float t = static_cast<float>(i) / sample_rate;
200✔
816
        values.push_back(std::sin(2.0f * std::numbers::pi_v<float> * 10.0f * t)); // 10 Hz sine wave
200✔
817
        times.push_back(TimeFrameIndex(i));
200✔
818
    }
819
    
820
    auto ats = std::make_shared<AnalogTimeSeries>(values, times);
1✔
821
    ats->setTimeFrame(time_frame);
1✔
822
    dm.setData("TestSignal.channel1", ats, TimeKey("default"));
3✔
823

824
    TransformPipeline pipeline(&dm, &registry);
1✔
825
    pipeline.loadFromJson(json_config);
1✔
826
    pipeline.execute();
1✔
827

828
    // Verify the results
829
    auto phase_series = dm.getData<AnalogTimeSeries>("PhaseSignal");
3✔
830
    REQUIRE(phase_series != nullptr);
1✔
831
    REQUIRE(!phase_series->getAnalogTimeSeries().empty());
1✔
832
    
833
    // Check that phase values are in the expected range [-π, π]
834
    auto const& phase_values = phase_series->getAnalogTimeSeries();
1✔
835
    for (auto const& phase : phase_values) {
201✔
836
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
837
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
838
    }
839
}
45✔
840

841
TEST_CASE("Data Transform: Analog Hilbert Phase - Parameter Factory", "[transforms][analog_hilbert_phase][factory]") {
1✔
842
    auto& factory = ParameterFactory::getInstance();
1✔
843
    factory.initializeDefaultSetters();
1✔
844

845
    auto params_base = std::make_unique<HilbertPhaseParams>();
1✔
846
    REQUIRE(params_base != nullptr);
1✔
847

848
    const nlohmann::json params_json = {
1✔
849
        {"discontinuity_threshold", 500},
1✔
850
        {"filter_low_freq", 2.5},
1✔
851
        {"filter_high_freq", 25.0},
1✔
852
        {"apply_bandpass_filter", true}
853
    };
18✔
854

855
    for (auto const& [key, val] : params_json.items()) {
5✔
856
        factory.setParameter("Hilbert Phase", params_base.get(), key, val, nullptr);
12✔
857
    }
1✔
858

859
    auto* params = dynamic_cast<HilbertPhaseParams*>(params_base.get());
1✔
860
    REQUIRE(params != nullptr);
1✔
861

862
    REQUIRE(params->discontinuityThreshold == 500);
1✔
863
    REQUIRE(params->filterLowFreq == 2.5);
1✔
864
    REQUIRE(params->filterHighFreq == 25.0);
1✔
865
    REQUIRE(params->applyBandpassFilter == true);
1✔
866
}
18✔
867

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

872
    // Create a TimeFrame for our data
873
    auto time_frame = std::make_shared<TimeFrame>();
1✔
874
    dm.setTime(TimeKey("default"), time_frame);
1✔
875
    
876
    // Create test analog data - a sine wave
877
    constexpr size_t sample_rate = 100;
1✔
878
    constexpr size_t duration_samples = 200;
1✔
879
    std::vector<float> values;
1✔
880
    std::vector<TimeFrameIndex> times;
1✔
881
    
882
    values.reserve(duration_samples);
1✔
883
    times.reserve(duration_samples);
1✔
884
    
885
    for (size_t i = 0; i < duration_samples; ++i) {
201✔
886
        float t = static_cast<float>(i) / sample_rate;
200✔
887
        values.push_back(std::sin(2.0f * std::numbers::pi_v<float> * 10.0f * t)); // 10 Hz sine wave
200✔
888
        times.push_back(TimeFrameIndex(i));
200✔
889
    }
890
    
891
    auto test_analog = std::make_shared<AnalogTimeSeries>(values, times);
1✔
892
    test_analog->setTimeFrame(time_frame);
1✔
893
    
894
    // Store the analog data in DataManager with a known key
895
    dm.setData("test_signal", test_analog, TimeKey("default"));
3✔
896
    
897
    // Create JSON configuration for transformation pipeline using unified format
898
    const char* json_config = 
1✔
899
        "[\n"
900
        "{\n"
901
        "    \"transformations\": {\n"
902
        "        \"metadata\": {\n"
903
        "            \"name\": \"Hilbert Phase Pipeline\",\n"
904
        "            \"description\": \"Test Hilbert phase calculation on analog signal\",\n"
905
        "            \"version\": \"1.0\"\n"
906
        "        },\n"
907
        "        \"steps\": [\n"
908
        "            {\n"
909
        "                \"step_id\": \"1\",\n"
910
        "                \"transform_name\": \"Hilbert Phase\",\n"
911
        "                \"phase\": \"analysis\",\n"
912
        "                \"input_key\": \"test_signal\",\n"
913
        "                \"output_key\": \"phase_signal\",\n"
914
        "                \"parameters\": {\n"
915
        "                    \"low_frequency\": 5.0,\n"
916
        "                    \"high_frequency\": 15.0,\n"
917
        "                    \"discontinuity_threshold\": 1000\n"
918
        "                }\n"
919
        "            }\n"
920
        "        ]\n"
921
        "    }\n"
922
        "}\n"
923
        "]";
924
    
925
    // Create temporary directory and write JSON config to file
926
    std::filesystem::path test_dir = std::filesystem::temp_directory_path() / "analog_hilbert_phase_pipeline_test";
1✔
927
    std::filesystem::create_directories(test_dir);
1✔
928
    
929
    std::filesystem::path json_filepath = test_dir / "pipeline_config.json";
1✔
930
    {
931
        std::ofstream json_file(json_filepath);
1✔
932
        REQUIRE(json_file.is_open());
1✔
933
        json_file << json_config;
1✔
934
        json_file.close();
1✔
935
    }
1✔
936
    
937
    // Execute the transformation pipeline using load_data_from_json_config
938
    auto data_info_list = load_data_from_json_config(&dm, json_filepath.string());
1✔
939
    
940
    // Verify the transformation was executed and results are available
941
    auto result_phase = dm.getData<AnalogTimeSeries>("phase_signal");
3✔
942
    REQUIRE(result_phase != nullptr);
1✔
943
    REQUIRE(!result_phase->getAnalogTimeSeries().empty());
1✔
944
    
945
    // Verify the phase calculation results
946
    auto const& phase_values = result_phase->getAnalogTimeSeries();
1✔
947
    for (auto const& phase : phase_values) {
201✔
948
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
949
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
950
    }
951
    
952
    // Test another pipeline with different parameters (different frequency band)
953
    const char* json_config_wideband = 
1✔
954
        "[\n"
955
        "{\n"
956
        "    \"transformations\": {\n"
957
        "        \"metadata\": {\n"
958
        "            \"name\": \"Hilbert Phase Wideband\",\n"
959
        "            \"description\": \"Test Hilbert phase with wider frequency band\",\n"
960
        "            \"version\": \"1.0\"\n"
961
        "        },\n"
962
        "        \"steps\": [\n"
963
        "            {\n"
964
        "                \"step_id\": \"1\",\n"
965
        "                \"transform_name\": \"Hilbert Phase\",\n"
966
        "                \"phase\": \"analysis\",\n"
967
        "                \"input_key\": \"test_signal\",\n"
968
        "                \"output_key\": \"phase_signal_wideband\",\n"
969
        "                \"parameters\": {\n"
970
        "                    \"low_frequency\": 1.0,\n"
971
        "                    \"high_frequency\": 50.0,\n"
972
        "                    \"discontinuity_threshold\": 1000\n"
973
        "                }\n"
974
        "            }\n"
975
        "        ]\n"
976
        "    }\n"
977
        "}\n"
978
        "]";
979
    
980
    std::filesystem::path json_filepath_wideband = test_dir / "pipeline_config_wideband.json";
1✔
981
    {
982
        std::ofstream json_file(json_filepath_wideband);
1✔
983
        REQUIRE(json_file.is_open());
1✔
984
        json_file << json_config_wideband;
1✔
985
        json_file.close();
1✔
986
    }
1✔
987
    
988
    // Execute the wideband pipeline
989
    auto data_info_list_wideband = load_data_from_json_config(&dm, json_filepath_wideband.string());
1✔
990
    
991
    // Verify the wideband results
992
    auto result_phase_wideband = dm.getData<AnalogTimeSeries>("phase_signal_wideband");
3✔
993
    REQUIRE(result_phase_wideband != nullptr);
1✔
994
    REQUIRE(!result_phase_wideband->getAnalogTimeSeries().empty());
1✔
995
    
996
    auto const& phase_values_wideband = result_phase_wideband->getAnalogTimeSeries();
1✔
997
    for (auto const& phase : phase_values_wideband) {
201✔
998
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
999
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
1000
    }
1001
    
1002
    // Test with smaller discontinuity threshold
1003
    const char* json_config_small_threshold = 
1✔
1004
        "[\n"
1005
        "{\n"
1006
        "    \"transformations\": {\n"
1007
        "        \"metadata\": {\n"
1008
        "            \"name\": \"Hilbert Phase Small Threshold\",\n"
1009
        "            \"description\": \"Test Hilbert phase with small discontinuity threshold\",\n"
1010
        "            \"version\": \"1.0\"\n"
1011
        "        },\n"
1012
        "        \"steps\": [\n"
1013
        "            {\n"
1014
        "                \"step_id\": \"1\",\n"
1015
        "                \"transform_name\": \"Hilbert Phase\",\n"
1016
        "                \"phase\": \"analysis\",\n"
1017
        "                \"input_key\": \"test_signal\",\n"
1018
        "                \"output_key\": \"phase_signal_small_threshold\",\n"
1019
        "                \"parameters\": {\n"
1020
        "                    \"low_frequency\": 5.0,\n"
1021
        "                    \"high_frequency\": 15.0,\n"
1022
        "                    \"discontinuity_threshold\": 10\n"
1023
        "                }\n"
1024
        "            }\n"
1025
        "        ]\n"
1026
        "    }\n"
1027
        "}\n"
1028
        "]";
1029
    
1030
    std::filesystem::path json_filepath_small_threshold = test_dir / "pipeline_config_small_threshold.json";
1✔
1031
    {
1032
        std::ofstream json_file(json_filepath_small_threshold);
1✔
1033
        REQUIRE(json_file.is_open());
1✔
1034
        json_file << json_config_small_threshold;
1✔
1035
        json_file.close();
1✔
1036
    }
1✔
1037
    
1038
    // Execute the small threshold pipeline
1039
    auto data_info_list_small_threshold = load_data_from_json_config(&dm, json_filepath_small_threshold.string());
1✔
1040
    
1041
    // Verify the small threshold results
1042
    auto result_phase_small_threshold = dm.getData<AnalogTimeSeries>("phase_signal_small_threshold");
3✔
1043
    REQUIRE(result_phase_small_threshold != nullptr);
1✔
1044
    REQUIRE(!result_phase_small_threshold->getAnalogTimeSeries().empty());
1✔
1045
    
1046
    auto const& phase_values_small_threshold = result_phase_small_threshold->getAnalogTimeSeries();
1✔
1047
    for (auto const& phase : phase_values_small_threshold) {
201✔
1048
        REQUIRE(phase >= -std::numbers::pi_v<float>);
200✔
1049
        REQUIRE(phase <= std::numbers::pi_v<float>);
200✔
1050
    }
1051
    
1052
    // Cleanup
1053
    try {
1054
        std::filesystem::remove_all(test_dir);
1✔
UNCOV
1055
    } catch (const std::exception& e) {
×
UNCOV
1056
        std::cerr << "Warning: Cleanup failed: " << e.what() << std::endl;
×
UNCOV
1057
    }
×
1058
}
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