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

paulmthompson / WhiskerToolbox / 15664585996

15 Jun 2025 03:13PM UTC coverage: 66.58% (+1.4%) from 65.21%
15664585996

push

github

paulmthompson
Refactor min/max statistical functions with NaN error handling and TimeFrameIndex support

- Replace 0.0f return values with std::numeric_limits<float>::quiet_NaN() for invalid operations
  * Consistent with IEEE 754 standards and mathematical library conventions
  * Matches existing DataAggregation.cpp pattern of using NaN for missing data
  * Enables proper error detection with std::isnan()

- Create layered architecture for min/max functions:
  * Core template implementations: calculate_min_impl/calculate_max_impl(Iterator, Iterator)
  * Supporting vector implementations: calculate_min_impl/calculate_max_impl(vector, start, end)
  * Span-based operations: calculate_min/calculate_max(std::span<const float>)
  * High-level interfaces:
    - calculate_min/calculate_max(AnalogTimeSeries) - entire series
    - calculate_min/calculate_max(AnalogTimeSeries, start, end) - index-based range
    - calculate_min_in_time_range/calculate_max_in_time_range(AnalogTimeSeries, start_time, end_time) - NEW TimeFrameIndex-based

- Add comprehensive test coverage:
  * Span-based operations with empty/partial spans
  * TimeFrameIndex range operations for sparse and dense storage
  * Boundary approximation when exact times don't exist
  * Consistency verification between all calculation methods
  * Edge cases: negative values, identical values, invalid ranges
  * Vector implementation testing with various range conditions

- Maintain zero-copy efficiency using std::span for range operations
- Ensure all methods return identical results for equivalent data ranges

205 of 207 new or added lines in 4 files covered. (99.03%)

13 existing lines in 2 files now uncovered.

8405 of 12624 relevant lines covered (66.58%)

1215.17 hits per line

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

92.13
/src/WhiskerToolbox/DataManager/utils/DataAggregation/DataAggregation.cpp
1
#include "DataAggregation.hpp"
2

3
#include "AnalogTimeSeries/utils/statistics.hpp"
4

5
#include <algorithm>
6
#include <cmath>
7
#include <optional>
8
#include <stdexcept>
9

10
namespace DataAggregation {
11

12
int64_t calculateOverlapDuration(Interval const & a, Interval const & b) {
11✔
13
    const int64_t overlap_start = std::max(a.start, b.start);
11✔
14
    const int64_t overlap_end = std::min(a.end, b.end);
11✔
15

16
    if (overlap_start <= overlap_end) {
11✔
17
        return overlap_end - overlap_start + 1;
9✔
18
    }
19
    return 0;
2✔
20
}
21

22
int findOverlappingIntervalIndex(Interval const & target_interval,
18✔
23
                                 std::vector<Interval> const & reference_intervals,
24
                                 OverlapStrategy strategy) {
25
    std::vector<int> overlapping_indices;
18✔
26

27
    // Find all overlapping intervals using existing is_overlapping function
28
    for (size_t i = 0; i < reference_intervals.size(); ++i) {
63✔
29
        if (is_overlapping(target_interval, reference_intervals[i])) {
45✔
30
            overlapping_indices.push_back(static_cast<int>(i));
24✔
31
        }
32
    }
33

34
    if (overlapping_indices.empty()) {
18✔
35
        return -1;// No overlap found
1✔
36
    }
37

38
    switch (strategy) {
17✔
39
        case OverlapStrategy::First:
12✔
40
            return overlapping_indices.front();
12✔
41

42
        case OverlapStrategy::Last:
1✔
43
            return overlapping_indices.back();
1✔
44

45
        case OverlapStrategy::MaxOverlap: {
4✔
46
            int best_index = overlapping_indices[0];
4✔
47
            int64_t max_overlap = calculateOverlapDuration(target_interval, reference_intervals[best_index]);
4✔
48

49
            for (size_t i = 1; i < overlapping_indices.size(); ++i) {
6✔
50
                const int current_index = overlapping_indices[i];
2✔
51
                const int64_t current_overlap = calculateOverlapDuration(target_interval, reference_intervals[current_index]);
2✔
52

53
                if (current_overlap > max_overlap) {
2✔
54
                    max_overlap = current_overlap;
1✔
55
                    best_index = current_index;
1✔
56
                }
57
            }
58
            return best_index;
4✔
59
        }
60

61
        default:
×
62
            throw std::invalid_argument("Unknown overlap strategy");
×
63
    }
64
}
18✔
65

66
double applyTransformation(Interval const & interval,
115✔
67
                           TransformationConfig const & config,
68
                           std::map<std::string, std::vector<Interval>> const & reference_intervals,
69
                           std::map<std::string, std::shared_ptr<AnalogTimeSeries>> const & reference_analog,
70
                           std::map<std::string, std::shared_ptr<PointData>> const & reference_points) {
71
    switch (config.type) {
115✔
72
        case TransformationType::IntervalStart:
22✔
73
            return static_cast<double>(interval.start);
22✔
74

75
        case TransformationType::IntervalEnd:
13✔
76
            return static_cast<double>(interval.end);
13✔
77

78
        case TransformationType::IntervalDuration:
10✔
79
            return static_cast<double>(interval.end - interval.start + 1);
10✔
80

81
        case TransformationType::IntervalID: {
19✔
82
            auto it = reference_intervals.find(config.reference_data_key);
19✔
83
            if (it == reference_intervals.end()) {
19✔
84
                return std::nan("");// Reference data not found
1✔
85
            }
86

87
            const int overlap_index = findOverlappingIntervalIndex(interval, it->second, config.overlap_strategy);
18✔
88
            if (overlap_index == -1) {
18✔
89
                return std::nan("");// No overlap found
1✔
90
            }
91

92
            return static_cast<double>(overlap_index);// 0-based indexing
17✔
93
        }
94

95
        case TransformationType::IntervalCount: {
9✔
96
            auto it = reference_intervals.find(config.reference_data_key);
9✔
97
            if (it == reference_intervals.end()) {
9✔
98
                return std::nan("");// Reference data not found
1✔
99
            }
100

101
            // Count all overlapping intervals
102
            int count = 0;
8✔
103
            for (auto const & ref_interval: it->second) {
29✔
104
                if (is_overlapping(interval, ref_interval)) {
21✔
105
                    count++;
10✔
106
                }
107
            }
108

109
            return static_cast<double>(count);
8✔
110
        }
111

112
        case TransformationType::AnalogMean: {
14✔
113
            auto it = reference_analog.find(config.reference_data_key);
14✔
114
            if (it == reference_analog.end() || !it->second) {
14✔
115
                return std::nan("");// Reference data not found
2✔
116
            }
117

118
            return calculate_mean_in_time_range(*it->second, TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
12✔
119
        }
120

121
        case TransformationType::AnalogMin: {
8✔
122
            auto it = reference_analog.find(config.reference_data_key);
8✔
123
            if (it == reference_analog.end() || !it->second) {
8✔
UNCOV
124
                return std::nan("");// Reference data not found
×
125
            }
126

127
            return calculate_min_in_time_range(*it->second, TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
8✔
128
        }
129

130
        case TransformationType::AnalogMax: {
7✔
131
            auto it = reference_analog.find(config.reference_data_key);
7✔
132
            if (it == reference_analog.end() || !it->second) {
7✔
UNCOV
133
                return std::nan("");// Reference data not found
×
134
            }
135

136
            return calculate_max_in_time_range(*it->second, TimeFrameIndex(interval.start), TimeFrameIndex(interval.end));
7✔
137
        }
138

139
        case TransformationType::AnalogStdDev: {
3✔
140
            auto it = reference_analog.find(config.reference_data_key);
3✔
141
            if (it == reference_analog.end() || !it->second) {
3✔
UNCOV
142
                return std::nan("");// Reference data not found
×
143
            }
144

145
            // Get data within the time interval
146
            auto start_index = it->second->findDataArrayIndexGreaterOrEqual(TimeFrameIndex(interval.start));
3✔
147
            auto end_index = it->second->findDataArrayIndexLessOrEqual(TimeFrameIndex(interval.end));
3✔
148

149
            if (!start_index.has_value() || !end_index.has_value()) {
3✔
150
                return std::nan("");// No data found in interval
×
151
            }
152

153
            return calculate_std_dev(*it->second, start_index.value().getValue(), end_index.value().getValue() + 1); // exclusive end
3✔
154
        }
155

156
        case TransformationType::PointMeanX: {
6✔
157
            auto it = reference_points.find(config.reference_data_key);
6✔
158
            if (it == reference_points.end() || !it->second) {
6✔
159
                return std::nan("");// Reference data not found
2✔
160
            }
161

162
            // Collect all X coordinates within the interval
163
            std::vector<float> x_values;
4✔
164
            for (int64_t t = interval.start; t <= interval.end; ++t) {
15✔
165
                const auto& points = it->second->getPointsAtTime(static_cast<int>(t));
11✔
166
                for (const auto& point : points) {
21✔
167
                    x_values.push_back(point.x);
10✔
168
                }
169
            }
170

171
            if (x_values.empty()) {
4✔
172
                return std::nan("");// No points found in interval
1✔
173
            }
174

175
            // Calculate mean
176
            double sum = 0.0;
3✔
177
            for (float x : x_values) {
13✔
178
                sum += static_cast<double>(x);
10✔
179
            }
180
            return sum / static_cast<double>(x_values.size());
3✔
181
        }
4✔
182

183
        case TransformationType::PointMeanY: {
4✔
184
            auto it = reference_points.find(config.reference_data_key);
4✔
185
            if (it == reference_points.end() || !it->second) {
4✔
UNCOV
186
                return std::nan("");// Reference data not found
×
187
            }
188

189
            // Collect all Y coordinates within the interval
190
            std::vector<float> y_values;
4✔
191
            for (int64_t t = interval.start; t <= interval.end; ++t) {
15✔
192
                const auto& points = it->second->getPointsAtTime(static_cast<int>(t));
11✔
193
                for (const auto& point : points) {
21✔
194
                    y_values.push_back(point.y);
10✔
195
                }
196
            }
197

198
            if (y_values.empty()) {
4✔
199
                return std::nan("");// No points found in interval
1✔
200
            }
201

202
            // Calculate mean
203
            double sum = 0.0;
3✔
204
            for (float y : y_values) {
13✔
205
                sum += static_cast<double>(y);
10✔
206
            }
207
            return sum / static_cast<double>(y_values.size());
3✔
208
        }
4✔
209

UNCOV
210
        default:
×
UNCOV
211
            throw std::invalid_argument("Unknown transformation type");
×
212
    }
213
}
214

215
std::vector<std::vector<double>> aggregateData(
31✔
216
        std::vector<Interval> const & row_intervals,
217
        std::vector<TransformationConfig> const & transformations,
218
        std::map<std::string, std::vector<Interval>> const & reference_intervals,
219
        std::map<std::string, std::shared_ptr<AnalogTimeSeries>> const & reference_analog,
220
        std::map<std::string, std::shared_ptr<PointData>> const & reference_points) {
221

222
    std::vector<std::vector<double>> result;
31✔
223
    result.reserve(row_intervals.size());
31✔
224

225
    for (auto const & interval: row_intervals) {
76✔
226
        std::vector<double> row;
45✔
227
        row.reserve(transformations.size());
45✔
228

229
        for (auto const & transformation: transformations) {
160✔
230
            const double value = applyTransformation(interval, transformation, reference_intervals, reference_analog, reference_points);
115✔
231
            row.push_back(value);
115✔
232
        }
233

234
        result.push_back(std::move(row));
45✔
235
    }
45✔
236

237
    return result;
31✔
UNCOV
238
}
×
239

240
}// namespace DataAggregation
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