• 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

79.32
/src/DataManager/transforms/AnalogTimeSeries/AnalogHilbertPhase/analog_hilbert_phase.cpp
1
#include "analog_hilbert_phase.hpp"
2

3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "utils/armadillo_wrap/analog_armadillo.hpp"
5
#include "utils/filter/FilterFactory.hpp"
6

7
#include <armadillo>
8

9
#include <algorithm>
10
#include <complex>
11
#include <iostream>
12
#include <numbers>
13
#include <numeric>//std::iota
14
#include <span>
15
#include <vector>
16

17
/**
18
     * @brief Represents a continuous chunk of data in the time series
19
     */
20
struct DataChunk {
21
    DataArrayIndex start_idx;         // Start index in original timestamps
22
    DataArrayIndex end_idx;           // End index in original timestamps (exclusive)
23
    TimeFrameIndex output_start;      // Start position in output array
24
    TimeFrameIndex output_end;        // End position in output array (exclusive)
25
    std::vector<float> values;        // Values for this chunk
26
    std::vector<TimeFrameIndex> times;// Timestamps for this chunk
27
};
28

29
/**
30
     * @brief Detects discontinuities in the time series and splits into continuous chunks
31
     * @param analog_time_series Input time series
32
     * @param threshold Maximum gap size before considering it a discontinuity
33
     * @return Vector of continuous data chunks
34
     */
35
std::vector<DataChunk> detectChunks(AnalogTimeSeries const * analog_time_series, size_t threshold) {
28✔
36
    std::vector<DataChunk> chunks;
28✔
37

38
    auto const & timestamps = analog_time_series->getTimeSeries();
28✔
39
    auto const & values = analog_time_series->getAnalogTimeSeries();
28✔
40

41
    if (timestamps.empty()) {
28✔
UNCOV
42
        return chunks;
×
43
    }
44

45
    DataArrayIndex chunk_start = DataArrayIndex(0);
28✔
46
    TimeFrameIndex last_time = timestamps[0];
28✔
47

48
    for (DataArrayIndex i = DataArrayIndex(1); i < DataArrayIndex(timestamps.size()); ++i) {
152,115✔
49
        TimeFrameIndex current_time = timestamps[i.getValue()];
152,087✔
50
        TimeFrameIndex gap = current_time - last_time;
152,087✔
51

52
        // If gap is larger than threshold, end current chunk and start new one
53
        if (gap.getValue() > static_cast<int64_t>(threshold)) {
152,087✔
54

55
            std::vector<float> chunk_values;
11✔
56
            std::vector<TimeFrameIndex> chunk_times;
11✔
57
            chunk_values.reserve(i - chunk_start);
11✔
58
            chunk_times.reserve(i - chunk_start);
11✔
59
            for (DataArrayIndex j = chunk_start; j < i; ++j) {
55✔
60
                chunk_values.push_back(values[j.getValue()]);
44✔
61
                chunk_times.push_back(timestamps[j.getValue()]);
44✔
62
            }
63

64
            // End chunk at last valid time + 1 (exclusive)
65
            DataChunk chunk{.start_idx = chunk_start,
11✔
66
                            .end_idx = i,
67
                            .output_start = timestamps[chunk_start.getValue()],
11✔
68
                            .output_end = last_time + TimeFrameIndex(1),
11✔
69
                            .values = std::move(chunk_values),
11✔
70
                            .times = std::move(chunk_times)};
33✔
71

72
            chunks.push_back(std::move(chunk));
11✔
73
            chunk_start = i;
11✔
74
        }
11✔
75
        last_time = current_time;
152,087✔
76
    }
77

78
    // Add final chunk
79
    std::vector<float> final_chunk_values;
28✔
80
    std::vector<TimeFrameIndex> final_chunk_times;
28✔
81
    final_chunk_values.reserve(timestamps.size() - chunk_start.getValue());
28✔
82
    final_chunk_times.reserve(timestamps.size() - chunk_start.getValue());
28✔
83
    for (DataArrayIndex j = chunk_start; j < DataArrayIndex(timestamps.size()); ++j) {
152,099✔
84
        final_chunk_values.push_back(values[j.getValue()]);
152,071✔
85
        final_chunk_times.push_back(timestamps[j.getValue()]);
152,071✔
86
    }
87

88
    DataChunk final_chunk{.start_idx = chunk_start,
28✔
89
                          .end_idx = DataArrayIndex(timestamps.size()),
90
                          .output_start = timestamps[chunk_start.getValue()],
28✔
91
                          .output_end = timestamps.back() + TimeFrameIndex(1),
28✔
92
                          .values = std::move(final_chunk_values),
28✔
93
                          .times = std::move(final_chunk_times)};
84✔
94

95
    chunks.push_back(std::move(final_chunk));
28✔
96

97
    return chunks;
28✔
98
}
28✔
99

100
/**
101
 * @brief Creates a Hann window of specified length
102
 * @param length Length of the window
103
 * @return Vector containing Hann window values
104
 */
UNCOV
105
std::vector<float> createHannWindow(size_t length) {
×
106
    std::vector<float> window(length);
×
UNCOV
107
    for (size_t i = 0; i < length; ++i) {
×
UNCOV
108
        window[i] = 0.5f * (1.0f - std::cos(2.0f * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(length - 1)));
×
109
    }
UNCOV
110
    return window;
×
111
}
112

113
/**
114
 * @brief Splits a large chunk into smaller overlapping sub-chunks for efficient processing
115
 * @param chunk The data chunk to split
116
 * @param maxChunkSize Maximum size of each sub-chunk
117
 * @param overlapFraction Fraction of overlap between sub-chunks (0.0 to 0.5)
118
 * @return Vector of sub-chunks with overlap information
119
 */
120
struct SubChunk {
121
    std::vector<float> values;
122
    std::vector<TimeFrameIndex> times;
123
    size_t valid_start_idx{0};  // Index where valid (non-edge) data starts
124
    size_t valid_end_idx{0};    // Index where valid (non-edge) data ends (exclusive)
125
    TimeFrameIndex output_start{TimeFrameIndex(0)}; // Time corresponding to valid_start_idx
126
};
127

128
std::vector<SubChunk> splitIntoOverlappingChunks(
1✔
129
    std::vector<float> const & values,
130
    std::vector<TimeFrameIndex> const & times,
131
    size_t maxChunkSize,
132
    double overlapFraction) {
133
    
134
    std::vector<SubChunk> subchunks;
1✔
135
    
136
    if (values.empty() || maxChunkSize == 0 || values.size() <= maxChunkSize) {
1✔
137
        // No need to split - return single chunk with full validity
UNCOV
138
        SubChunk single;
×
UNCOV
139
        single.values = values;
×
UNCOV
140
        single.times = times;
×
UNCOV
141
        single.valid_start_idx = 0;
×
UNCOV
142
        single.valid_end_idx = values.size();
×
UNCOV
143
        single.output_start = times.empty() ? TimeFrameIndex(0) : times[0];
×
UNCOV
144
        subchunks.push_back(std::move(single));
×
UNCOV
145
        return subchunks;
×
UNCOV
146
    }
×
147
    
148
    // Calculate overlap size and step size
149
    size_t overlap_size = static_cast<size_t>(static_cast<double>(maxChunkSize) * std::clamp(overlapFraction, 0.0, 0.5));
1✔
150
    size_t step_size = maxChunkSize - overlap_size;
1✔
151
    size_t edge_discard = overlap_size / 2; // Discard this many samples from each edge
1✔
152
    
153
    for (size_t start_idx = 0; start_idx < values.size(); start_idx += step_size) {
20✔
154
        size_t end_idx = std::min(start_idx + maxChunkSize, values.size());
20✔
155
        
156
        // Copy data for this sub-chunk
157
        std::vector<float> sub_values;
20✔
158
        std::vector<TimeFrameIndex> sub_times;
20✔
159
        sub_values.reserve(end_idx - start_idx);
20✔
160
        sub_times.reserve(end_idx - start_idx);
20✔
161
        for (size_t i = start_idx; i < end_idx; ++i) {
197,520✔
162
            sub_values.push_back(values[i]);
197,500✔
163
            sub_times.push_back(times[i]);
197,500✔
164
        }
165
        
166
        // Determine valid region (exclude edges where windowing causes artifacts)
167
        size_t valid_start_idx = 0;
20✔
168
        size_t valid_end_idx = 0;
20✔
169
        if (start_idx == 0) {
20✔
170
            // First chunk: keep from beginning, discard end edge
171
            valid_start_idx = 0;
1✔
172
            valid_end_idx = (end_idx < values.size()) ? (sub_values.size() - edge_discard) : sub_values.size();
1✔
173
        } else if (end_idx >= values.size()) {
19✔
174
            // Last chunk: discard start edge, keep to end
175
            valid_start_idx = edge_discard;
1✔
176
            valid_end_idx = sub_values.size();
1✔
177
        } else {
178
            // Middle chunk: discard both edges
179
            valid_start_idx = edge_discard;
18✔
180
            valid_end_idx = sub_values.size() - edge_discard;
18✔
181
        }
182
        
183
        TimeFrameIndex output_start_time = sub_times[valid_start_idx];
20✔
184
        
185
        SubChunk sub;
20✔
186
        sub.values = std::move(sub_values);
20✔
187
        sub.times = std::move(sub_times);
20✔
188
        sub.valid_start_idx = valid_start_idx;
20✔
189
        sub.valid_end_idx = valid_end_idx;
20✔
190
        sub.output_start = output_start_time;
20✔
191
        
192
        subchunks.push_back(std::move(sub));
20✔
193
        
194
        // If we've reached the end, stop
195
        if (end_idx >= values.size()) {
20✔
196
            break;
1✔
197
        }
198
    }
22✔
199
    
200
    return subchunks;
1✔
UNCOV
201
}
×
202

203
/**
204
 * @brief Creates a bandpass filter based on parameters
205
 * @param params Hilbert phase parameters containing filter settings
206
 * @return Unique pointer to the filter, or nullptr if filtering is disabled
207
 */
208
std::unique_ptr<IFilter> createBandpassFilter(HilbertPhaseParams const & params) {
39✔
209
    if (!params.applyBandpassFilter) {
39✔
210
        return nullptr;
39✔
211
    }
212
    
213
    // Create zero-phase Butterworth bandpass filter with runtime order dispatch
UNCOV
214
    switch (params.filterOrder) {
×
UNCOV
215
        case 1: return FilterFactory::createButterworthBandpass<1>(
×
UNCOV
216
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
217
        case 2: return FilterFactory::createButterworthBandpass<2>(
×
UNCOV
218
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
219
        case 3: return FilterFactory::createButterworthBandpass<3>(
×
UNCOV
220
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
221
        case 4: return FilterFactory::createButterworthBandpass<4>(
×
UNCOV
222
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
223
        case 5: return FilterFactory::createButterworthBandpass<5>(
×
UNCOV
224
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
225
        case 6: return FilterFactory::createButterworthBandpass<6>(
×
UNCOV
226
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
227
        case 7: return FilterFactory::createButterworthBandpass<7>(
×
UNCOV
228
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
229
        case 8: return FilterFactory::createButterworthBandpass<8>(
×
UNCOV
230
            params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
UNCOV
231
        default:
×
UNCOV
232
            std::cerr << "Warning: Invalid filter order " << params.filterOrder 
×
UNCOV
233
                      << ", defaulting to order 4" << std::endl;
×
234
            return FilterFactory::createButterworthBandpass<4>(
UNCOV
235
                params.filterLowFreq, params.filterHighFreq, params.samplingRate, true);
×
236
    }
237
}
238

239
/**
240
 * @brief Applies Hilbert transform to a signal vector
241
 * @param signal Input signal
242
 * @param outputType Whether to extract phase or amplitude
243
 * @param applyWindow Whether to apply Hann window
244
 * @param filter Optional bandpass filter to apply before Hilbert transform
245
 * @return Processed output (phase or amplitude)
246
 */
247
std::vector<float> applyHilbertTransform(
58✔
248
    std::vector<float> const & signal,
249
    HilbertPhaseParams::OutputType outputType,
250
    bool applyWindow,
251
    IFilter * filter = nullptr) {
252
    
253
    if (signal.empty()) {
58✔
UNCOV
254
        return {};
×
255
    }
256
    
257
    // Convert to arma::vec
258
    arma::vec arma_signal(signal.size());
58✔
259
    std::copy(signal.begin(), signal.end(), arma_signal.begin());
58✔
260
    
261
    // Apply bandpass filter first if provided
262
    if (filter != nullptr) {
58✔
UNCOV
263
        std::vector<float> filter_buffer(signal.begin(), signal.end());
×
UNCOV
264
        filter->process(std::span<float>(filter_buffer.data(), filter_buffer.size()));
×
UNCOV
265
        std::copy(filter_buffer.begin(), filter_buffer.end(), arma_signal.begin());
×
266
    }
×
267
    
268
    // Apply window if requested
269
    if (applyWindow && signal.size() > 1) {
58✔
UNCOV
270
        auto window = createHannWindow(signal.size());
×
UNCOV
271
        for (size_t i = 0; i < signal.size(); ++i) {
×
UNCOV
272
            arma_signal(i) *= static_cast<double>(window[i]);
×
273
        }
UNCOV
274
    }
×
275
    
276
    // Perform FFT
277
    arma::cx_vec X = arma::fft(arma_signal).eval();
58✔
278
    
279
    // Create analytic signal by zeroing negative frequencies
280
    arma::uword const n = X.n_elem;
58✔
281
    arma::uword const halfway = (n + 1) / 2;
58✔
282
    
283
    // Zero out negative frequencies
284
    for (arma::uword i = halfway; i < n; ++i) {
99,858✔
285
        X(i) = std::complex<double>(0.0, 0.0);
199,600✔
286
    }
287
    
288
    // Double the positive frequencies (except DC and Nyquist if present)
289
    for (arma::uword i = 1; i < halfway; ++i) {
99,814✔
290
        X(i) *= 2.0;
99,756✔
291
    }
292
    
293
    // Compute inverse FFT to get the analytic signal
294
    arma::cx_vec const analytic_signal = arma::ifft(X).eval();
58✔
295
    
296
    // Extract either phase or amplitude
297
    arma::vec output_values;
58✔
298
    if (outputType == HilbertPhaseParams::OutputType::Phase) {
58✔
299
        output_values = arma::arg(analytic_signal);
34✔
300
    } else {
301
        output_values = arma::abs(analytic_signal);
24✔
302
    }
303
    
304
    // Convert back to standard vector
305
    return arma::conv_to<std::vector<float>>::from(output_values);
58✔
306
}
58✔
307

308
/**
309
     * @brief Processes a single continuous chunk using Hilbert transform
310
     * @param chunk The data chunk to process
311
     * @param phaseParams Parameters for the calculation
312
     * @return Vector of phase or amplitude values for this chunk depending on outputType
313
     */
314
std::vector<float> processChunk(DataChunk const & chunk, HilbertPhaseParams const & phaseParams) {
39✔
315
    if (chunk.values.empty()) {
39✔
UNCOV
316
        return {};
×
317
    }
318

319
    std::cout << "Processing chunk with " << chunk.values.size() << " values" << std::endl;
39✔
320

321
    // First check for NaN values and remove them
322
    std::vector<float> clean_values;
39✔
323
    std::vector<TimeFrameIndex> clean_times;
39✔
324
    clean_values.reserve(chunk.values.size());
39✔
325
    clean_times.reserve(chunk.times.size());
39✔
326

327
    for (size_t i = 0; i < chunk.values.size(); ++i) {
152,154✔
328
        if (!std::isnan(chunk.values[i])) {
152,115✔
329
            clean_values.push_back(chunk.values[i]);
152,114✔
330
            clean_times.push_back(chunk.times[i]);
152,114✔
331
        }
332
    }
333

334
    // If all values were NaN, return empty vector
335
    if (clean_values.empty()) {
39✔
UNCOV
336
        return std::vector<float>(static_cast<size_t>(chunk.output_end.getValue() - chunk.output_start.getValue()), 0.0f);
×
337
    }
338

339
    // Check if we need to split into smaller chunks for efficiency
340
    bool useWindowedProcessing = phaseParams.maxChunkSize > 0 && clean_values.size() > phaseParams.maxChunkSize;
39✔
341
    
342
    // Create bandpass filter if requested (shared across all sub-chunks)
343
    auto filter = createBandpassFilter(phaseParams);
39✔
344
    
345
    std::vector<float> result_values;
39✔
346
    
347
    if (useWindowedProcessing) {
39✔
348
        // Split into overlapping sub-chunks
349
        std::cout << "  Using windowed processing with " << phaseParams.maxChunkSize 
1✔
350
                  << " samples per window, " << (phaseParams.overlapFraction * 100.0) << "% overlap" << std::endl;
1✔
351
        
352
        if (filter) {
1✔
353
            std::cout << "  Applying " << filter->getName() << " bandpass filter" << std::endl;
×
354
        }
355
        
356
        auto subchunks = splitIntoOverlappingChunks(clean_values, clean_times, 
1✔
357
                                                     phaseParams.maxChunkSize, 
1✔
358
                                                     phaseParams.overlapFraction);
1✔
359
        
360
        std::cout << "  Split into " << subchunks.size() << " sub-chunks" << std::endl;
1✔
361
        
362
        // Process each sub-chunk and collect valid results
363
        std::vector<float> all_results;
1✔
364
        std::vector<TimeFrameIndex> all_result_times;
1✔
365
        
366
        for (auto const & subchunk : subchunks) {
21✔
367
            // Reset filter state for each sub-chunk (zero-phase filter handles this internally)
368
            if (filter) {
20✔
UNCOV
369
                filter->reset();
×
370
            }
371
            
372
            // Apply Hilbert transform to this sub-chunk
373
            auto sub_result = applyHilbertTransform(subchunk.values, 
20✔
374
                                                    phaseParams.outputType, 
20✔
375
                                                    phaseParams.useWindowing,
20✔
376
                                                    filter.get());
20✔
377
            
378
            // Extract only the valid portion (excluding edges)
379
            for (size_t i = subchunk.valid_start_idx; i < subchunk.valid_end_idx; ++i) {
150,020✔
380
                all_results.push_back(sub_result[i]);
150,000✔
381
                all_result_times.push_back(subchunk.times[i]);
150,000✔
382
            }
383
        }
20✔
384
        
385
        result_values = std::move(all_results);
1✔
386
        clean_times = std::move(all_result_times);
1✔
387
        
388
    } else {
1✔
389
        // Process entire chunk at once (original behavior for small chunks)
390
        if (filter) {
38✔
UNCOV
391
            std::cout << "  Applying " << filter->getName() << " bandpass filter" << std::endl;
×
UNCOV
392
            filter->reset();
×
393
        }
394
        result_values = applyHilbertTransform(clean_values, phaseParams.outputType, false, filter.get());
38✔
395
    }
396

397
    // Now result_values and clean_times contain the processed data
398
    // Create output vector for this chunk and handle interpolation for small gaps
399

400
    // Create output vector for this chunk only
401
    auto chunk_size = static_cast<size_t>(chunk.output_end.getValue() - chunk.output_start.getValue());
39✔
402
    std::vector<float> output_data(chunk_size, 0.0f);
117✔
403

404
    // Fill in the original points (excluding NaN values)
405
    for (size_t i = 0; i < clean_times.size(); ++i) {
152,153✔
406
        auto output_idx = static_cast<size_t>(clean_times[i].getValue() - chunk.output_start.getValue());
152,114✔
407
        if (output_idx < output_data.size()) {
152,114✔
408
            output_data[output_idx] = result_values[i];
152,114✔
409
        }
410
    }
411

412
    // Interpolate small gaps within this chunk only
413
    for (size_t i = 1; i < clean_times.size(); ++i) {
152,114✔
414
        int64_t gap = clean_times[i].getValue() - clean_times[i - 1].getValue();
152,075✔
415
        if (gap > 1 && static_cast<size_t>(gap) <= phaseParams.discontinuityThreshold) {
152,075✔
416
            // Linear interpolation for points in between
417
            float value_start = result_values[i - 1];
42✔
418
            float value_end = result_values[i];
42✔
419

420
            // Handle phase wrapping only for phase output
421
            if (phaseParams.outputType == HilbertPhaseParams::OutputType::Phase) {
42✔
422
                if (value_end - value_start > static_cast<float>(std::numbers::pi)) {
42✔
UNCOV
423
                    value_start += 2.0f * static_cast<float>(std::numbers::pi);
×
424
                } else if (value_start - value_end > static_cast<float>(std::numbers::pi)) {
42✔
425
                    value_end += 2.0f * static_cast<float>(std::numbers::pi);
7✔
426
                }
427
            }
428

429
            for (int64_t j = 1; j < gap; ++j) {
874✔
430
                float t = static_cast<float>(j) / static_cast<float>(gap);
832✔
431
                float interpolated_value = value_start + t * (value_end - value_start);
832✔
432

433
                // Wrap back to [-π, π] only for phase output
434
                if (phaseParams.outputType == HilbertPhaseParams::OutputType::Phase) {
832✔
435
                    while (interpolated_value > static_cast<float>(std::numbers::pi)) interpolated_value -= 2.0f * static_cast<float>(std::numbers::pi);
1,047✔
436
                    while (interpolated_value <= -static_cast<float>(std::numbers::pi)) interpolated_value += 2.0f * static_cast<float>(std::numbers::pi);
832✔
437
                }
438

439
                auto output_idx = static_cast<size_t>((clean_times[i - 1].getValue() + j) - chunk.output_start.getValue());
832✔
440
                if (output_idx < output_data.size()) {
832✔
441
                    output_data[output_idx] = interpolated_value;
832✔
442
                }
443
            }
444
        }
445
    }
446

447
    return output_data;
39✔
448
}
39✔
449

450
///////////////////////////////////////////////////////////////////////////////
451

452
std::shared_ptr<AnalogTimeSeries> hilbert_phase(
23✔
453
        AnalogTimeSeries const * analog_time_series,
454
        HilbertPhaseParams const & phaseParams) {
455
    return hilbert_phase(analog_time_series, phaseParams, [](int) {});
23✔
456
}
457

458
std::shared_ptr<AnalogTimeSeries> hilbert_phase(
31✔
459
        AnalogTimeSeries const * analog_time_series,
460
        HilbertPhaseParams const & phaseParams,
461
        ProgressCallback progressCallback) {
462

463
    // Input validation
464
    if (!analog_time_series) {
31✔
465
        std::cerr << "hilbert_phase: Input AnalogTimeSeries is null" << std::endl;
2✔
466
        return std::make_shared<AnalogTimeSeries>();
2✔
467
    }
468

469
    auto const & timestamps = analog_time_series->getTimeSeries();
29✔
470
    if (timestamps.empty()) {
29✔
471
        std::cerr << "hilbert_phase: Input time series is empty" << std::endl;
1✔
472
        return std::make_shared<AnalogTimeSeries>();
1✔
473
    }
474

475
    if (progressCallback) {
28✔
476
        progressCallback(5);
27✔
477
    }
478

479
    // Detect discontinuous chunks
480
    auto chunks = detectChunks(analog_time_series, phaseParams.discontinuityThreshold);
28✔
481
    if (chunks.empty()) {
28✔
UNCOV
482
        std::cerr << "hilbert_phase: No valid chunks detected" << std::endl;
×
UNCOV
483
        return std::make_shared<AnalogTimeSeries>();
×
484
    }
485

486
    // Determine total output size based on last chunk's end
487
    auto const & last_chunk = chunks.back();
28✔
488
    auto total_size = static_cast<size_t>(last_chunk.output_end.getValue());
28✔
489

490
    // Create output vectors with proper size
491
    std::vector<float> output_data(total_size, 0.0f);
84✔
492
    std::vector<TimeFrameIndex> output_times;
28✔
493
    output_times.reserve(total_size);
28✔
494
    for (size_t i = 0; i < total_size; ++i) {
161,269✔
495
        output_times.push_back(TimeFrameIndex(static_cast<int64_t>(i)));
161,241✔
496
    }
497

498
    // Process each chunk
499
    size_t total_chunks = chunks.size();
28✔
500
    for (size_t i = 0; i < chunks.size(); ++i) {
67✔
501
        auto const & chunk = chunks[i];
39✔
502

503
        // Process chunk
504
        auto chunk_phase = processChunk(chunk, phaseParams);
39✔
505

506
        // Copy chunk results to output
507
        if (!chunk_phase.empty()) {
39✔
508
            auto start_idx = static_cast<size_t>(chunk.output_start.getValue());
39✔
509
            size_t end_idx = std::min(start_idx + chunk_phase.size(), output_data.size());
39✔
510
            std::copy(chunk_phase.begin(),
117✔
511
                      chunk_phase.begin() + static_cast<long int>(end_idx - start_idx),
78✔
512
                      output_data.begin() + static_cast<long int>(start_idx));
78✔
513
        }
514

515
        // Update progress
516
        if (progressCallback) {
39✔
517
            int progress = 5 + static_cast<int>((90.0f * static_cast<float>(i + 1)) / static_cast<float>(total_chunks));
38✔
518
            progressCallback(progress);
38✔
519
        }
520
    }
39✔
521

522
    // Create result
523
    auto result = std::make_shared<AnalogTimeSeries>(std::move(output_data), std::move(output_times));
28✔
524

525
    if (progressCallback) {
28✔
526
        progressCallback(100);
27✔
527
    }
528

529
    return result;
28✔
530
}
29✔
531

532
///////////////////////////////////////////////////////////////////////////////
533

534
std::string HilbertPhaseOperation::getName() const {
148✔
535
    return "Hilbert Phase";
444✔
536
}
537

538
std::type_index HilbertPhaseOperation::getTargetInputTypeIndex() const {
148✔
539
    return typeid(std::shared_ptr<AnalogTimeSeries>);
148✔
540
}
541

542
bool HilbertPhaseOperation::canApply(DataTypeVariant const & dataVariant) const {
4✔
543
    if (!std::holds_alternative<std::shared_ptr<AnalogTimeSeries>>(dataVariant)) {
4✔
UNCOV
544
        return false;
×
545
    }
546

547
    auto const * ptr_ptr = std::get_if<std::shared_ptr<AnalogTimeSeries>>(&dataVariant);
4✔
548
    return ptr_ptr && *ptr_ptr;
4✔
549
}
550

551
std::unique_ptr<TransformParametersBase> HilbertPhaseOperation::getDefaultParameters() const {
4✔
552
    return std::make_unique<HilbertPhaseParams>();
4✔
553
}
554

555
DataTypeVariant HilbertPhaseOperation::execute(
1✔
556
        DataTypeVariant const & dataVariant,
557
        TransformParametersBase const * transformParameters) {
558
    return execute(dataVariant, transformParameters, nullptr);
1✔
559
}
560

561
DataTypeVariant HilbertPhaseOperation::execute(
4✔
562
        DataTypeVariant const & dataVariant,
563
        TransformParametersBase const * transformParameters,
564
        ProgressCallback progressCallback) {
565

566
    auto const * ptr_ptr = std::get_if<std::shared_ptr<AnalogTimeSeries>>(&dataVariant);
4✔
567

568
    if (!ptr_ptr || !(*ptr_ptr)) {
4✔
UNCOV
569
        std::cerr << "HilbertPhaseOperation::execute: Invalid input data variant" << std::endl;
×
UNCOV
570
        return {};
×
571
    }
572

573
    AnalogTimeSeries const * analog_raw_ptr = (*ptr_ptr).get();
4✔
574

575
    HilbertPhaseParams currentParams;
4✔
576

577
    if (transformParameters != nullptr) {
4✔
578
        auto const * specificParams =
579
                dynamic_cast<HilbertPhaseParams const *>(transformParameters);
4✔
580

581
        if (specificParams) {
4✔
582
            currentParams = *specificParams;
4✔
583
        } else {
UNCOV
584
            std::cerr << "HilbertPhaseOperation::execute: Incompatible parameter type, using defaults" << std::endl;
×
585
        }
586
    }
587

588
    std::shared_ptr<AnalogTimeSeries> result = hilbert_phase(
4✔
589
            analog_raw_ptr, currentParams, progressCallback);
4✔
590

591
    if (!result) {
4✔
UNCOV
592
        std::cerr << "HilbertPhaseOperation::execute: Phase calculation failed" << std::endl;
×
UNCOV
593
        return {};
×
594
    }
595

596
    return result;
4✔
597
}
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc