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

paulmthompson / WhiskerToolbox / 15566405961

10 Jun 2025 05:39PM UTC coverage: 62.158% (+6.9%) from 55.215%
15566405961

push

github

paulmthompson
add interface for interval grouping transformation

6851 of 11022 relevant lines covered (62.16%)

605.76 hits per line

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

91.2
/src/WhiskerToolbox/DataManager/transforms/AnalogTimeSeries/analog_interval_threshold.cpp
1
#include "analog_interval_threshold.hpp"
2

3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
5
#include "DigitalTimeSeries/interval_data.hpp"
6

7
#include <algorithm>
8
#include <cmath>
9
#include <iostream>
10
#include <vector>
11

12

13
std::shared_ptr<DigitalIntervalSeries> interval_threshold(
26✔
14
        AnalogTimeSeries const * analog_time_series,
15
        IntervalThresholdParams const & thresholdParams) {
16
    return interval_threshold(analog_time_series, thresholdParams, [](int) {});
26✔
17
}
18

19
std::shared_ptr<DigitalIntervalSeries> interval_threshold(
35✔
20
        AnalogTimeSeries const * analog_time_series,
21
        IntervalThresholdParams const & thresholdParams,
22
        ProgressCallback progressCallback) {
23
    auto interval_series = std::make_shared<DigitalIntervalSeries>();
35✔
24

25
    // Input validation
26
    if (!analog_time_series) {
35✔
27
        std::cerr << "interval_threshold: Input AnalogTimeSeries is null" << std::endl;
2✔
28
        return interval_series;
2✔
29
    }
30

31
    auto const & timestamps = analog_time_series->getTimeSeries();
33✔
32
    auto const & values = analog_time_series->getAnalogTimeSeries();
33✔
33

34
    if (timestamps.empty()) {
33✔
35
        std::cerr << "interval_threshold: Input time series is empty" << std::endl;
1✔
36
        return interval_series;
1✔
37
    }
38

39
    if (progressCallback) {
32✔
40
        progressCallback(10);
27✔
41
    }
42

43
    auto const threshold = static_cast<float>(thresholdParams.thresholdValue);
32✔
44
    double const minDuration = thresholdParams.minDuration;
32✔
45
    std::vector<Interval> intervals;
32✔
46

47
    // Variables to track interval state
48
    bool in_interval = false;
32✔
49
    int64_t interval_start = 0;
32✔
50
    double last_interval_end = -thresholdParams.lockoutTime - 1.0;// Initialize to allow first interval
32✔
51
    int64_t last_valid_time = 0;                                  // Track the last time where we know the interval state
32✔
52

53
    auto addIntervalIfValid = [&intervals, minDuration](int64_t start, int64_t end) {
32✔
54
        // Check if the interval meets the minimum duration requirement
55
        if (static_cast<double>(end - start + 1) >= minDuration) {
56✔
56
            intervals.push_back({start, end});
53✔
57
        }
58
    };
56✔
59

60
    if (progressCallback) {
32✔
61
        progressCallback(20);
27✔
62
    }
63

64
    // Lambda to check if value meets threshold criteria
65
    auto meetsThreshold = [&thresholdParams, threshold](float value) -> bool {
32✔
66
        switch (thresholdParams.direction) {
200✔
67
            case IntervalThresholdParams::ThresholdDirection::POSITIVE:
156✔
68
                return value > threshold;
156✔
69
            case IntervalThresholdParams::ThresholdDirection::NEGATIVE:
30✔
70
                return value < threshold;
30✔
71
            case IntervalThresholdParams::ThresholdDirection::ABSOLUTE:
14✔
72
                return std::abs(value) > threshold;
14✔
73
            default:
×
74
                return false;
×
75
        }
76
    };
32✔
77

78
    // Check if zero meets threshold (for missing data handling)
79
    bool const zeroMeetsThreshold = meetsThreshold(0.0f);
32✔
80

81
    // Calculate typical time step to detect actual gaps (not just large regular intervals)
82
    size_t const total_samples = timestamps.size();
32✔
83
    size_t typical_time_step = 1;
32✔
84
    if (total_samples > 1) {
32✔
85
        // Use the first time difference as the expected step size
86
        typical_time_step = timestamps[1] - timestamps[0];
30✔
87

88
        // Validate this against a few more samples if available
89
        if (total_samples > 2) {
30✔
90
            size_t second_step = timestamps[2] - timestamps[1];
30✔
91
            // If steps are very different, fall back to minimum step size
92
            if (second_step != typical_time_step) {
30✔
93
                typical_time_step = std::min(typical_time_step, second_step);
6✔
94
            }
95
        }
96
    }
97

98
    // Process the time series
99
    for (size_t i = 0; i < total_samples; ++i) {
200✔
100
        if (progressCallback && i % 1000 == 0) {
168✔
101
            int const progress = 20 + static_cast<int>((i * 70) / total_samples);
27✔
102
            progressCallback(progress);
27✔
103
        }
104

105
        // Initialize last_valid_time for first sample
106
        if (i == 0) {
168✔
107
            last_valid_time = static_cast<int64_t>(timestamps[i]);
32✔
108
        }
109

110
        // Handle missing data gaps if treating missing as zero
111
        if (i > 0 && thresholdParams.missingDataMode == IntervalThresholdParams::MissingDataMode::TREAT_AS_ZERO) {
168✔
112
            size_t const prev_time = timestamps[i - 1];
20✔
113
            size_t const curr_time = timestamps[i];
20✔
114
            size_t const actual_step = curr_time - prev_time;
20✔
115

116
            // Check if there's a gap (more than 1.5x the typical time step)
117
            if (actual_step > (typical_time_step * 3 / 2)) {
20✔
118
                // There's a gap - handle missing data as zeros
119
                if (in_interval && !zeroMeetsThreshold) {
3✔
120
                    // We're in an interval but zeros don't meet threshold - end the interval at the gap start
121
                    auto const gap_start = static_cast<int64_t>(prev_time);
2✔
122
                    addIntervalIfValid(interval_start, gap_start);
2✔
123
                    last_interval_end = static_cast<double>(gap_start);
2✔
124
                    in_interval = false;
2✔
125
                } else if (!in_interval && zeroMeetsThreshold) {
3✔
126
                    // We're not in an interval but zeros meet threshold - start interval in the gap
127
                    auto const gap_start = static_cast<int64_t>(prev_time + typical_time_step);
×
128
                    if (static_cast<double>(gap_start) - last_interval_end >= thresholdParams.lockoutTime) {
×
129
                        interval_start = gap_start;
×
130
                        in_interval = true;
×
131
                    }
132
                }
133

134
                // If we're in an interval and zeros meet threshold, update last_valid_time to end of gap
135
                if (in_interval && zeroMeetsThreshold) {
3✔
136
                    last_valid_time = static_cast<int64_t>(curr_time - typical_time_step);
1✔
137
                } else {
138
                    last_valid_time = static_cast<int64_t>(prev_time);
2✔
139
                }
140
            } else {
141
                last_valid_time = static_cast<int64_t>(prev_time);
17✔
142
            }
143
        } else {
20✔
144
            if (i > 0) {
148✔
145
                last_valid_time = static_cast<int64_t>(timestamps[i - 1]);
116✔
146
            }
147
        }
148

149
        bool const threshold_met = meetsThreshold(values[i]);
168✔
150

151
        if (threshold_met && !in_interval) {
168✔
152
            // Start of a new interval
153
            if (static_cast<double>(timestamps[i]) - last_interval_end >= thresholdParams.lockoutTime) {
114✔
154
                interval_start = static_cast<int64_t>(timestamps[i]);
56✔
155
                in_interval = true;
56✔
156
            }
157
        } else if (!threshold_met && in_interval) {
110✔
158
            // End of current interval
159
            int64_t const interval_end = (i > 0) ? last_valid_time : interval_start;
39✔
160
            addIntervalIfValid(interval_start, interval_end);
39✔
161
            last_interval_end = static_cast<double>(interval_end);
39✔
162
            in_interval = false;
39✔
163
        }
164

165
        // Update last_valid_time to current timestamp
166
        last_valid_time = static_cast<int64_t>(timestamps[i]);
168✔
167
    }
168

169
    // Handle case where signal still meets threshold at the end
170
    if (in_interval) {
32✔
171
        addIntervalIfValid(interval_start, static_cast<int64_t>(timestamps.back()));
15✔
172
    }
173

174
    if (progressCallback) {
32✔
175
        progressCallback(90);
27✔
176
    }
177

178
    interval_series->setData(intervals);
32✔
179

180
    if (progressCallback) {
32✔
181
        progressCallback(100);
27✔
182
    }
183

184
    return interval_series;
32✔
185
}
32✔
186

187
///////////////////////////////////////////////////////////////////////////////
188

189
std::string IntervalThresholdOperation::getName() const {
1✔
190
    return "Threshold Interval Detection";
3✔
191
}
192

193
std::type_index IntervalThresholdOperation::getTargetInputTypeIndex() const {
1✔
194
    return typeid(std::shared_ptr<AnalogTimeSeries>);
1✔
195
}
196

197
bool IntervalThresholdOperation::canApply(DataTypeVariant const & dataVariant) const {
2✔
198
    if (!std::holds_alternative<std::shared_ptr<AnalogTimeSeries>>(dataVariant)) {
2✔
199
        return false;
×
200
    }
201

202
    auto const * ptr_ptr = std::get_if<std::shared_ptr<AnalogTimeSeries>>(&dataVariant);
2✔
203
    return ptr_ptr && *ptr_ptr;
2✔
204
}
205

206
DataTypeVariant IntervalThresholdOperation::execute(
5✔
207
        DataTypeVariant const & dataVariant,
208
        TransformParametersBase const * transformParameters) {
209
    return execute(dataVariant, transformParameters, nullptr);
5✔
210
}
211

212
DataTypeVariant IntervalThresholdOperation::execute(
6✔
213
        DataTypeVariant const & dataVariant,
214
        TransformParametersBase const * transformParameters,
215
        ProgressCallback progressCallback) {
216

217
    auto const * ptr_ptr = std::get_if<std::shared_ptr<AnalogTimeSeries>>(&dataVariant);
6✔
218

219
    if (!ptr_ptr || !(*ptr_ptr)) {
6✔
220
        std::cerr << "IntervalThresholdOperation::execute: Invalid input data variant" << std::endl;
×
221
        return {};
×
222
    }
223

224
    AnalogTimeSeries const * analog_raw_ptr = (*ptr_ptr).get();
6✔
225

226
    IntervalThresholdParams currentParams;
6✔
227

228
    if (transformParameters != nullptr) {
6✔
229
        auto const * specificParams =
230
                dynamic_cast<IntervalThresholdParams const *>(transformParameters);
5✔
231

232
        if (specificParams) {
5✔
233
            currentParams = *specificParams;
4✔
234
        } else {
235
            std::cerr << "IntervalThresholdOperation::execute: Incompatible parameter type, using defaults" << std::endl;
1✔
236
        }
237
    }
238

239
    std::shared_ptr<DigitalIntervalSeries> result = interval_threshold(
6✔
240
            analog_raw_ptr, currentParams, progressCallback);
6✔
241

242
    if (!result) {
6✔
243
        std::cerr << "IntervalThresholdOperation::execute: Interval detection failed" << std::endl;
×
244
        return {};
×
245
    }
246

247
    return result;
6✔
248
}
6✔
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