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

paulmthompson / WhiskerToolbox / 18477247352

13 Oct 2025 08:18PM UTC coverage: 72.391% (+0.4%) from 71.943%
18477247352

push

github

web-flow
Merge pull request #140 from paulmthompson/kdtree

Jules PR

164 of 287 new or added lines in 3 files covered. (57.14%)

350 existing lines in 9 files now uncovered.

51889 of 71679 relevant lines covered (72.39%)

63071.54 hits per line

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

99.38
/src/DataManager/transforms/Lines/Line_Angle/line_angle.test.cpp
1
#include "transforms/Lines/Line_Angle/line_angle.hpp"
2
#include "Lines/Line_Data.hpp"
3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "AnalogTimeSeries/utils/statistics.hpp"
5

6
#include <catch2/catch_test_macros.hpp>
7
#include <catch2/matchers/catch_matchers_floating_point.hpp>
8
#include <memory>
9
#include <cmath>
10
#include <nlohmann/json.hpp>
11

12
TEST_CASE("Line angle calculation - Core functionality", "[line][angle][transform]") {
12✔
13
    auto line_data = std::make_shared<LineData>();
12✔
14

15
    SECTION("Calculating angle from empty line data") {
12✔
16
        auto params = std::make_unique<LineAngleParameters>();
1✔
17
        auto result = line_angle(line_data.get(), params.get());
1✔
18

19
        REQUIRE(result->getAnalogTimeSeries().empty());
1✔
20
        REQUIRE(result->getTimeSeries().empty());
1✔
21
    }
13✔
22

23
    SECTION("Direct angle calculation - Horizontal line") {
12✔
24
        // Create a horizontal line
25
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
26
        std::vector<float> y_coords = {1.0f, 1.0f, 1.0f, 1.0f};
3✔
27
        line_data->addAtTime(TimeFrameIndex(10), x_coords, y_coords);
1✔
28

29
        // Position at 33% (approx. between points 1 and 2)
30
        auto params = std::make_unique<LineAngleParameters>();
1✔
31
        params->position = 0.33f;
1✔
32
        params->method = AngleCalculationMethod::DirectPoints;
1✔
33

34
        auto result = line_angle(line_data.get(), params.get());
1✔
35

36
        auto const & values = result->getAnalogTimeSeries();
1✔
37
        auto const & times = result->getTimeSeries();
1✔
38

39
        REQUIRE(times.size() == 1);
1✔
40
        REQUIRE(values.size() == 1);
1✔
41
        REQUIRE(times[0] == TimeFrameIndex(10));
1✔
42
        // Angle should be 0 degrees (horizontal line points right)
43
        REQUIRE_THAT(values[0], Catch::Matchers::WithinAbs(0.0f, 0.001f));
1✔
44
    }
13✔
45

46
    SECTION("Direct angle calculation - Vertical line") {
12✔
47
        // Create a vertical line pointing up
48
        std::vector<float> x_coords = {1.0f, 1.0f, 1.0f, 1.0f};
3✔
49
        std::vector<float> y_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
50
        line_data->addAtTime(TimeFrameIndex(20), x_coords, y_coords);
1✔
51

52
        // Position at 25% (at point 1)
53
        auto params = std::make_unique<LineAngleParameters>();
1✔
54
        params->position = 0.25f;
1✔
55
        params->method = AngleCalculationMethod::DirectPoints;
1✔
56

57
        auto result = line_angle(line_data.get(), params.get());
1✔
58

59
        auto const & values = result->getAnalogTimeSeries();
1✔
60
        auto const & times = result->getTimeSeries();
1✔
61

62
        REQUIRE(times.size() == 1);
1✔
63
        REQUIRE(values.size() == 1);
1✔
64
        REQUIRE(times[0] == TimeFrameIndex(20));
1✔
65
        // Angle should be 90 degrees (vertical line points up)
66
        REQUIRE_THAT(values[0], Catch::Matchers::WithinAbs(90.0f, 0.001f));
1✔
67
    }
13✔
68

69
    SECTION("Direct angle calculation - Diagonal line (45 degrees)") {
12✔
70
        // Create a diagonal line (45 degrees)
71
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
72
        std::vector<float> y_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
73
        line_data->addAtTime(TimeFrameIndex(30), x_coords, y_coords);
1✔
74

75
        // Position at 50% (at point 2)
76
        auto params = std::make_unique<LineAngleParameters>();
1✔
77
        params->position = 0.50f;
1✔
78
        params->method = AngleCalculationMethod::DirectPoints;
1✔
79

80
        auto result = line_angle(line_data.get(), params.get());
1✔
81

82
        auto const & values = result->getAnalogTimeSeries();
1✔
83
        auto const & times = result->getTimeSeries();
1✔
84

85
        REQUIRE(times.size() == 1);
1✔
86
        REQUIRE(values.size() == 1);
1✔
87
        REQUIRE(times[0] == TimeFrameIndex(30));
1✔
88
        // Angle should be 45 degrees
89
        REQUIRE_THAT(values[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
90
    }
13✔
91

92
    SECTION("Direct angle calculation - Multiple time points") {
12✔
93
        // Create lines at different time points with different angles
94

95
        // Horizontal line at time 40
96
        std::vector<float> x1 = {0.0f, 1.0f, 2.0f};
3✔
97
        std::vector<float> y1 = {1.0f, 1.0f, 1.0f};
3✔
98
        line_data->addAtTime(TimeFrameIndex(40), x1, y1);
1✔
99

100
        // Vertical line at time 50
101
        std::vector<float> x2 = {1.0f, 1.0f, 1.0f};
3✔
102
        std::vector<float> y2 = {0.0f, 1.0f, 2.0f};
3✔
103
        line_data->addAtTime(TimeFrameIndex(50), x2, y2);
1✔
104

105
        // 45-degree line at time 60
106
        std::vector<float> x3 = {0.0f, 1.0f, 2.0f};
3✔
107
        std::vector<float> y3 = {0.0f, 1.0f, 2.0f};
3✔
108
        line_data->addAtTime(TimeFrameIndex(60), x3, y3);
1✔
109

110
        // Position at 50%
111
        auto params = std::make_unique<LineAngleParameters>();
1✔
112
        params->position = 0.5f;
1✔
113
        params->method = AngleCalculationMethod::DirectPoints;
1✔
114

115
        auto result = line_angle(line_data.get(), params.get());
1✔
116

117
        auto const & values = result->getAnalogTimeSeries();
1✔
118
        auto const & times = result->getTimeSeries();
1✔
119

120
        REQUIRE(times.size() == 3);
1✔
121
        REQUIRE(values.size() == 3);
1✔
122

123
        // Find time indices and check angles
124
        auto time40_idx = std::distance(times.begin(), std::find(times.begin(), times.end(), TimeFrameIndex(40)));
2✔
125
        auto time50_idx = std::distance(times.begin(), std::find(times.begin(), times.end(), TimeFrameIndex(50)));
2✔
126
        auto time60_idx = std::distance(times.begin(), std::find(times.begin(), times.end(), TimeFrameIndex(60)));
2✔
127

128
        // Horizontal line: 0 degrees
129
        REQUIRE_THAT(values[time40_idx], Catch::Matchers::WithinAbs(0.0f, 0.001f));
1✔
130
        // Vertical line: 90 degrees
131
        REQUIRE_THAT(values[time50_idx], Catch::Matchers::WithinAbs(90.0f, 0.001f));
1✔
132
        // 45-degree line: 45 degrees
133
        REQUIRE_THAT(values[time60_idx], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
134
    }
13✔
135

136
    SECTION("Polynomial angle calculation - Simple line") {
12✔
137
        // Create a curve (points on a parabola)
138
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f};
3✔
139
        std::vector<float> y_coords = {0.0f, 1.0f, 4.0f, 9.0f, 16.0f, 25.0f};
3✔
140
        line_data->addAtTime(TimeFrameIndex(70), x_coords, y_coords);
1✔
141

142
        // Position at 40% with polynomial fitting
143
        auto params = std::make_unique<LineAngleParameters>();
1✔
144
        params->position = 0.4f; // Around x=3 (37% of length of line)
1✔
145
        params->method = AngleCalculationMethod::PolynomialFit;
1✔
146
        params->polynomial_order = 2; // Use a quadratic fit for this parabola
1✔
147

148
        auto result = line_angle(line_data.get(), params.get());
1✔
149

150
        auto const & values = result->getAnalogTimeSeries();
1✔
151
        auto const & times = result->getTimeSeries();
1✔
152

153
        REQUIRE(times.size() == 1);
1✔
154
        REQUIRE(values.size() == 1);
1✔
155
        REQUIRE(times[0] == TimeFrameIndex(70));
1✔
156

157
        // For a parabola y = x², the derivative is 2x. So slope at x=3 is 6
158
        // Angle of atan(6,1) is approximately 80.537 degrees. 
159
        REQUIRE(values[0] > 75.0f);
1✔
160
        REQUIRE(values[0] < 85.0f);
1✔
161
    }
13✔
162

163
    SECTION("Different polynomial orders") {
12✔
164
        // Create a smooth curve
165
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f};
3✔
166
        std::vector<float> y_coords = {0.0f, 0.5f, 1.8f, 3.9f, 6.8f, 10.5f, 15.0f, 20.3f};
3✔
167
        line_data->addAtTime(TimeFrameIndex(80), x_coords, y_coords);
1✔
168

169
        // Test with different polynomial orders
170
        auto position = 0.5f; // Middle of the line
1✔
171

172
        // First order (linear fit)
173
        auto params1 = std::make_unique<LineAngleParameters>();
1✔
174
        params1->position = position;
1✔
175
        params1->method = AngleCalculationMethod::PolynomialFit;
1✔
176
        params1->polynomial_order = 1;
1✔
177
        auto result1 = line_angle(line_data.get(), params1.get());
1✔
178

179
        // Third order
180
        auto params3 = std::make_unique<LineAngleParameters>();
1✔
181
        params3->position = position;
1✔
182
        params3->method = AngleCalculationMethod::PolynomialFit;
1✔
183
        params3->polynomial_order = 3;
1✔
184
        auto result3 = line_angle(line_data.get(), params3.get());
1✔
185

186
        // Fifth order
187
        auto params5 = std::make_unique<LineAngleParameters>();
1✔
188
        params5->position = position;
1✔
189
        params5->method = AngleCalculationMethod::PolynomialFit;
1✔
190
        params5->polynomial_order = 5;
1✔
191
        auto result5 = line_angle(line_data.get(), params5.get());
1✔
192

193
        // Each result should have proper data structure
194
        REQUIRE(result1->getTimeSeries().size() == 1);
1✔
195
        REQUIRE(result3->getTimeSeries().size() == 1);
1✔
196
        REQUIRE(result5->getTimeSeries().size() == 1);
1✔
197

198
        // Higher order polynomials should better capture local curvature
199
        // For this curve, we expect higher orders to yield different angles
200
        // from lower orders (exact values will depend on the curve)
201
        auto angle1 = result1->getAnalogTimeSeries()[0];
1✔
202
        auto angle3 = result3->getAnalogTimeSeries()[0];
1✔
203
        auto angle5 = result5->getAnalogTimeSeries()[0];
1✔
204

205
        // We don't check exact values, but ensure they're reasonable angles
206
        REQUIRE(angle1 >= -180.0f);
1✔
207
        REQUIRE(angle1 <= 180.0f);
1✔
208
        REQUIRE(angle3 >= -180.0f);
1✔
209
        REQUIRE(angle3 <= 180.0f);
1✔
210
        REQUIRE(angle5 >= -180.0f);
1✔
211
        REQUIRE(angle5 <= 180.0f);
1✔
212

213
        // The angles should be different because of the different polynomial orders
214
        REQUIRE((std::abs(angle1 - angle3) > 1.0f || std::abs(angle1 - angle5) > 1.0f));
1✔
215
    }
13✔
216

217
    SECTION("Verify returned AnalogTimeSeries structure") {
12✔
218
        // Add a simple line
219
        std::vector<float> x = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
220
        std::vector<float> y = {0.0f, 0.0f, 0.0f, 0.0f};
3✔
221
        line_data->addAtTime(TimeFrameIndex(100), x, y);
1✔
222

223
        auto params = std::make_unique<LineAngleParameters>();
1✔
224
        params->position = 0.5f;
1✔
225
        auto result = line_angle(line_data.get(), params.get());
1✔
226

227
        // Verify it's a proper AnalogTimeSeries
228
        REQUIRE(result != nullptr);
1✔
229
        REQUIRE(result->getAnalogTimeSeries().size() == 1);
1✔
230
        REQUIRE(result->getTimeSeries().size() == 1);
1✔
231

232
        // Statistics should work on the result
233
        float angle = result->getAnalogTimeSeries()[0];
1✔
234
        REQUIRE(calculate_mean(*result.get()) == angle);
1✔
235
        REQUIRE(calculate_min(*result.get()) == angle);
1✔
236
        REQUIRE(calculate_max(*result.get()) == angle);
1✔
237
    }
13✔
238

239
    SECTION("Reference vector - Horizontal reference") {
12✔
240
        // Create a 45-degree line
241
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
242
        std::vector<float> y_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
243
        line_data->addAtTime(TimeFrameIndex(110), x_coords, y_coords);
1✔
244

245
        // Default reference (1,0) - horizontal reference
246
        auto params1 = std::make_unique<LineAngleParameters>();
1✔
247
        params1->position = 0.5f;
1✔
248
        params1->reference_x = 1.0f;
1✔
249
        params1->reference_y = 0.0f;
1✔
250
        auto result1 = line_angle(line_data.get(), params1.get());
1✔
251

252
        // The angle should be 45 degrees (default reference is horizontal)
253
        REQUIRE_THAT(result1->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
254
    }
13✔
255

256
    SECTION("Reference vector - Vertical reference") {
12✔
257
        // Create a 45-degree line
258
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
259
        std::vector<float> y_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
260
        line_data->addAtTime(TimeFrameIndex(120), x_coords, y_coords);
1✔
261

262
        // Vertical reference (0,1)
263
        auto params2 = std::make_unique<LineAngleParameters>();
1✔
264
        params2->position = 0.5f;
1✔
265
        params2->reference_x = 0.0f;
1✔
266
        params2->reference_y = 1.0f;
1✔
267
        auto result2 = line_angle(line_data.get(), params2.get());
1✔
268

269
        // The angle should be -45 degrees (angle from vertical to 45-degree line)
270
        REQUIRE_THAT(result2->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(-45.0f, 0.001f));
1✔
271
    }
13✔
272

273
    SECTION("Reference vector - 45-degree reference") {
12✔
274
        // Create a horizontal line
275
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
276
        std::vector<float> y_coords = {1.0f, 1.0f, 1.0f, 1.0f};
3✔
277
        line_data->addAtTime(TimeFrameIndex(130), x_coords, y_coords);
1✔
278

279
        // 45-degree reference (1,1)
280
        auto params3 = std::make_unique<LineAngleParameters>();
1✔
281
        params3->position = 0.5f;
1✔
282
        params3->reference_x = 1.0f;
1✔
283
        params3->reference_y = 1.0f;
1✔
284
        auto result3 = line_angle(line_data.get(), params3.get());
1✔
285

286
        // The angle should be -45 degrees (angle from 45-degree to horizontal)
287
        REQUIRE_THAT(result3->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(-45.0f, 0.001f));
1✔
288
    }
13✔
289

290
    SECTION("Reference vector with polynomial fit") {
12✔
291
        // Create a parabolic curve
292
        std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f};
3✔
293
        std::vector<float> y_coords = {0.0f, 1.0f, 4.0f, 9.0f, 16.0f};
3✔
294
        line_data->addAtTime(TimeFrameIndex(140), x_coords, y_coords);
1✔
295

296
        // Use default reference (1,0) with polynomial fit
297
        auto params1 = std::make_unique<LineAngleParameters>();
1✔
298
        params1->position = 0.5f;
1✔
299
        params1->method = AngleCalculationMethod::PolynomialFit;
1✔
300
        params1->polynomial_order = 2;
1✔
301
        auto result1 = line_angle(line_data.get(), params1.get());
1✔
302

303
        // Now use vertical reference (0,1) with same polynomial fit
304
        auto params2 = std::make_unique<LineAngleParameters>();
1✔
305
        params2->position = 0.5f;
1✔
306
        params2->method = AngleCalculationMethod::PolynomialFit;
1✔
307
        params2->polynomial_order = 2;
1✔
308
        params2->reference_x = 0.0f;
1✔
309
        params2->reference_y = 1.0f;
1✔
310
        auto result2 = line_angle(line_data.get(), params2.get());
1✔
311

312
        // The difference between the two angles should be approximately 90 degrees
313
        float angle1 = result1->getAnalogTimeSeries()[0];
1✔
314
        float angle2 = result2->getAnalogTimeSeries()[0];
1✔
315

316
        // Adjust for angle wrapping
317
        float angle_diff = angle1 - angle2;
1✔
318
        if (angle_diff > 180.0f) angle_diff -= 360.0f;
1✔
319
        if (angle_diff <= -180.0f) angle_diff += 360.0f;
1✔
320

321
        REQUIRE_THAT(std::abs(angle_diff), Catch::Matchers::WithinAbs(90.0f, 5.0f));
1✔
322
    }
13✔
323
}
24✔
324

325
TEST_CASE("Line angle calculation - Edge cases and error handling", "[line][angle][transform][edge]") {
10✔
326
    auto line_data = std::make_shared<LineData>();
10✔
327

328
    SECTION("Line with only one point") {
10✔
329
        // Add a line with just one point (should skip this line)
330
        std::vector<float> x = {1.0f};
3✔
331
        std::vector<float> y = {1.0f};
3✔
332
        line_data->addAtTime(TimeFrameIndex(10), x, y);
1✔
333

334
        auto params = std::make_unique<LineAngleParameters>();
1✔
335
        params->position = 0.5f;
1✔
336
        auto result = line_angle(line_data.get(), params.get());
1✔
337

338
        // Should return an empty result
339
        REQUIRE(result->getAnalogTimeSeries().empty());
1✔
340
        REQUIRE(result->getTimeSeries().empty());
1✔
341
    }
11✔
342

343
    SECTION("Position out of range") {
10✔
344
        // Create a normal line
345
        std::vector<float> x = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
346
        std::vector<float> y = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
347
        line_data->addAtTime(TimeFrameIndex(20), x, y);
1✔
348

349
        // Test position below 0
350
        auto params_low = std::make_unique<LineAngleParameters>();
1✔
351
        params_low->position = -0.5f;  // Should clamp to 0.0
1✔
352
        auto result_low = line_angle(line_data.get(), params_low.get());
1✔
353

354
        // Test position above 1
355
        auto params_high = std::make_unique<LineAngleParameters>();
1✔
356
        params_high->position = 1.5f;  // Should clamp to 1.0
1✔
357
        auto result_high = line_angle(line_data.get(), params_high.get());
1✔
358

359
        // Both should work and use clamped positions
360
        REQUIRE(result_low->getAnalogTimeSeries().size() == 1);
1✔
361
        REQUIRE(result_high->getAnalogTimeSeries().size() == 1);
1✔
362

363
        // Low position (0.0) should be the angle from the first point to itself, which is not defined
364
        // but implementation should handle this gracefully
365
        float low_angle = result_low->getAnalogTimeSeries()[0];
1✔
366
        REQUIRE((std::isnan(low_angle) || (-180.0f <= low_angle && low_angle <= 180.0f)));
1✔
367

368
        // High position (1.0) should be the angle from first to last point (45 degrees)
369
        float high_angle = result_high->getAnalogTimeSeries()[0];
1✔
370
        REQUIRE_THAT(high_angle, Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
371
    }
11✔
372

373
    SECTION("Multiple lines at same timestamp") {
10✔
374
        // Create two lines at the same timestamp
375
        std::vector<float> x1 = {0.0f, 1.0f, 2.0f};
3✔
376
        std::vector<float> y1 = {0.0f, 0.0f, 0.0f};  // Horizontal: 0 degrees
3✔
377
        line_data->addAtTime(TimeFrameIndex(30), x1, y1);
1✔
378

379
        std::vector<float> x2 = {0.0f, 0.0f, 0.0f};
3✔
380
        std::vector<float> y2 = {0.0f, 1.0f, 2.0f};  // Vertical: 90 degrees
3✔
381
        line_data->addAtTime(TimeFrameIndex(30), x2, y2);
1✔
382

383
        auto params = std::make_unique<LineAngleParameters>();
1✔
384
        params->position = 0.5f;
1✔
385
        auto result = line_angle(line_data.get(), params.get());
1✔
386

387
        // Only the first line should be used
388
        REQUIRE(result->getAnalogTimeSeries().size() == 1);
1✔
389
        REQUIRE(result->getTimeSeries().size() == 1);
1✔
390
        REQUIRE_THAT(result->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(0.0f, 0.001f));
1✔
391
    }
11✔
392

393
    SECTION("Polynomial fit with too few points") {
10✔
394
        // Create a line with fewer points than polynomial order
395
        std::vector<float> x = {0.0f, 1.0f};
3✔
396
        std::vector<float> y = {0.0f, 1.0f};
3✔
397
        line_data->addAtTime(TimeFrameIndex(40), x, y);
1✔
398

399
        // Try to fit a 3rd order polynomial (requires at least 4 points)
400
        auto params = std::make_unique<LineAngleParameters>();
1✔
401
        params->position = 0.5f;
1✔
402
        params->method = AngleCalculationMethod::PolynomialFit;
1✔
403
        params->polynomial_order = 3;
1✔
404
        auto result = line_angle(line_data.get(), params.get());
1✔
405

406
        // Should fall back to direct method instead of failing
407
        REQUIRE(result->getAnalogTimeSeries().size() == 1);
1✔
408
        REQUIRE(result->getTimeSeries().size() == 1);
1✔
409
        // 45 degree angle with direct method
410
        REQUIRE_THAT(result->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
411
    }
11✔
412

413
    SECTION("Polynomial fit with collinear points") {
10✔
414
        // Create a vertical line where x values are all the same
415
        std::vector<float> x = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
3✔
416
        std::vector<float> y = {0.0f, 1.0f, 2.0f, 3.0f, 4.0f};
3✔
417
        line_data->addAtTime(TimeFrameIndex(50), x, y);
1✔
418

419
        // Try polynomial fit which may be numerically unstable
420
        auto params = std::make_unique<LineAngleParameters>();
1✔
421
        params->position = 0.5f;
1✔
422
        params->method = AngleCalculationMethod::PolynomialFit;
1✔
423
        params->polynomial_order = 3;
1✔
424
        auto result = line_angle(line_data.get(), params.get());
1✔
425

426
        // Should still produce a result (either falling back to direct method or
427
        // producing a reasonable angle through the polynomial fit)
428
        REQUIRE(result->getAnalogTimeSeries().size() == 1);
1✔
429
        REQUIRE(result->getTimeSeries().size() == 1);
1✔
430

431
        float angle = result->getAnalogTimeSeries()[0];
1✔
432
        // Should be close to 90 degrees (vertical line)
433
        REQUIRE(((angle > 80.0f && angle < 100.0f) || (angle < -80.0f && angle > -100.0f)));
1✔
434
    }
11✔
435

436
    SECTION("Null parameters") {
10✔
437
        // Create a simple line
438
        std::vector<float> x = {0.0f, 1.0f, 2.0f};
3✔
439
        std::vector<float> y = {0.0f, 1.0f, 2.0f};
3✔
440
        line_data->addAtTime(TimeFrameIndex(60), x, y);
1✔
441

442
        // Call with null parameters
443
        auto result = line_angle(line_data.get(), nullptr);
1✔
444

445
        // Should use default parameters
446
        REQUIRE(result->getAnalogTimeSeries().size() == 1);
1✔
447
        REQUIRE(result->getTimeSeries().size() == 1);
1✔
448
        // Default is 0.2 position with direct method, expect about 45 degrees
449
        REQUIRE_THAT(result->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
450
    }
11✔
451

452
    SECTION("Large number of points") {
10✔
453
        // Create a line with many points
454
        std::vector<float> x;
1✔
455
        std::vector<float> y;
1✔
456
        for (int i = 0; i < 1000; ++i) {
1,001✔
457
            x.push_back(static_cast<float>(i));
1,000✔
458
            y.push_back(static_cast<float>(i));  // 45-degree line
1,000✔
459
        }
460
        line_data->addAtTime(TimeFrameIndex(70), x, y);
1✔
461

462
        // Test both methods
463
        auto params_direct = std::make_unique<LineAngleParameters>();
1✔
464
        params_direct->position = 0.5f;
1✔
465
        params_direct->method = AngleCalculationMethod::DirectPoints;
1✔
466
        auto result_direct = line_angle(line_data.get(), params_direct.get());
1✔
467

468
        auto params_poly = std::make_unique<LineAngleParameters>();
1✔
469
        params_poly->position = 0.5f;
1✔
470
        params_poly->method = AngleCalculationMethod::PolynomialFit;
1✔
471
        params_poly->polynomial_order = 3;
1✔
472
        auto result_poly = line_angle(line_data.get(), params_poly.get());
1✔
473

474
        // Both should handle large number of points
475
        REQUIRE(result_direct->getAnalogTimeSeries().size() == 1);
1✔
476
        REQUIRE(result_poly->getAnalogTimeSeries().size() == 1);
1✔
477

478
        // Both should produce close to 45 degrees
479
        REQUIRE_THAT(result_direct->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
480
        REQUIRE_THAT(result_poly->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 1.0f));
1✔
481
    }
11✔
482

483
    SECTION("Zero reference vector") {
10✔
484
        // Create a simple line
485
        std::vector<float> x = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
486
        std::vector<float> y = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
487
        line_data->addAtTime(TimeFrameIndex(80), x, y);
1✔
488

489
        // Try a zero reference vector (should default to (1,0))
490
        auto params = std::make_unique<LineAngleParameters>();
1✔
491
        params->position = 0.5f;
1✔
492
        params->reference_x = 0.0f;
1✔
493
        params->reference_y = 0.0f;
1✔
494
        auto result = line_angle(line_data.get(), params.get());
1✔
495

496
        // The angle should be the same as with the default reference
497
        REQUIRE_THAT(result->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
498
    }
11✔
499

500
    SECTION("Normalizing reference vector") {
10✔
501
        // Create a simple line
502
        std::vector<float> x = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
503
        std::vector<float> y = {0.0f, 0.0f, 0.0f, 0.0f};
3✔
504
        line_data->addAtTime(TimeFrameIndex(90), x, y);
1✔
505

506
        // Use an unnormalized reference vector
507
        auto params1 = std::make_unique<LineAngleParameters>();
1✔
508
        params1->position = 0.5f;
1✔
509
        params1->reference_x = 0.0f;
1✔
510
        params1->reference_y = 2.0f;  // Magnitude of 2
1✔
511
        auto result1 = line_angle(line_data.get(), params1.get());
1✔
512

513
        // Use the same reference vector but normalized
514
        auto params2 = std::make_unique<LineAngleParameters>();
1✔
515
        params2->position = 0.5f;
1✔
516
        params2->reference_x = 0.0f;
1✔
517
        params2->reference_y = 1.0f;  // Normalized to magnitude of 1
1✔
518
        auto result2 = line_angle(line_data.get(), params2.get());
1✔
519

520
        // Both should give the same angle (reference direction is what matters, not magnitude)
521
        REQUIRE_THAT(result1->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(result2->getAnalogTimeSeries()[0], 0.001f));
1✔
522
    }
11✔
523

524
    SECTION("Specific problematic 2-point lines with negative reference vector") {
10✔
525
        // Test Line 1: (565, 253), (408, 277)
526
        std::vector<float> x1 = {565.0f, 408.0f};
3✔
527
        std::vector<float> y1 = {253.0f, 277.0f};
3✔
528
        line_data->addAtTime(TimeFrameIndex(200), x1, y1);
1✔
529

530
        // Test Line 2: (567, 252), (434, 265)
531
        std::vector<float> x2 = {567.0f, 434.0f};
3✔
532
        std::vector<float> y2 = {252.0f, 265.0f};
3✔
533
        line_data->addAtTime(TimeFrameIndex(210), x2, y2);
1✔
534

535
        // Test with reference vector (-1, 0) at 80% position
536
        auto params_80 = std::make_unique<LineAngleParameters>();
1✔
537
        params_80->position = 0.8f;
1✔
538
        params_80->reference_x = -1.0f;
1✔
539
        params_80->reference_y = 0.0f;
1✔
540
        params_80->method = AngleCalculationMethod::DirectPoints;
1✔
541
        auto result_80 = line_angle(line_data.get(), params_80.get());
1✔
542

543
        // Test with reference vector (-1, 0) at 100% position
544
        auto params_100 = std::make_unique<LineAngleParameters>();
1✔
545
        params_100->position = 1.0f;
1✔
546
        params_100->reference_x = -1.0f;
1✔
547
        params_100->reference_y = 0.0f;
1✔
548
        params_100->method = AngleCalculationMethod::DirectPoints;
1✔
549
        auto result_100 = line_angle(line_data.get(), params_100.get());
1✔
550

551
        // Verify we get results for both lines
552
        REQUIRE(result_80->getAnalogTimeSeries().size() == 2);
1✔
553
        REQUIRE(result_80->getTimeSeries().size() == 2);
1✔
554
        REQUIRE(result_100->getAnalogTimeSeries().size() == 2);
1✔
555
        REQUIRE(result_100->getTimeSeries().size() == 2);
1✔
556

557
        // Check that results are not +/- 180 degrees (the problematic values)
558
        for (size_t i = 0; i < result_80->getAnalogTimeSeries().size(); ++i) {
3✔
559
            float angle_80 = result_80->getAnalogTimeSeries()[i];
2✔
560
            float angle_100 = result_100->getAnalogTimeSeries()[i];
2✔
561
            
562
            // Results should not be exactly 180 or -180
563
            REQUIRE(angle_80 != 180.0f);
2✔
564
            REQUIRE(angle_80 != -180.0f);
2✔
565
            REQUIRE(angle_100 != 180.0f);
2✔
566
            REQUIRE(angle_100 != -180.0f);
2✔
567
            
568
            // Results should be within valid angle range
569
            REQUIRE(angle_80 >= -180.0f);
2✔
570
            REQUIRE(angle_80 <= 180.0f);
2✔
571
            REQUIRE(angle_100 >= -180.0f);
2✔
572
            REQUIRE(angle_100 <= 180.0f);
2✔
573
            
574
            // Print the actual values for debugging
575
            std::cout << "Line " << (i+1) << " at 80%: " << angle_80 << " degrees" << std::endl;
2✔
576
            std::cout << "Line " << (i+1) << " at 100%: " << angle_100 << " degrees" << std::endl;
2✔
577
        }
578

579
        // Test with polynomial fit method as well
580
        auto params_poly_80 = std::make_unique<LineAngleParameters>();
1✔
581
        params_poly_80->position = 0.8f;
1✔
582
        params_poly_80->reference_x = -1.0f;
1✔
583
        params_poly_80->reference_y = 0.0f;
1✔
584
        params_poly_80->method = AngleCalculationMethod::PolynomialFit;
1✔
585
        params_poly_80->polynomial_order = 1; // Linear fit for 2 points
1✔
586
        auto result_poly_80 = line_angle(line_data.get(), params_poly_80.get());
1✔
587

588
        // Polynomial fit should fall back to direct method for 2 points
589
        REQUIRE(result_poly_80->getAnalogTimeSeries().size() == 2);
1✔
590
        for (size_t i = 0; i < result_poly_80->getAnalogTimeSeries().size(); ++i) {
3✔
591
            float angle_poly = result_poly_80->getAnalogTimeSeries()[i];
2✔
592
            REQUIRE(angle_poly != 180.0f);
2✔
593
            REQUIRE(angle_poly != -180.0f);
2✔
594
            std::cout << "Line " << (i+1) << " polynomial at 80%: " << angle_poly << " degrees" << std::endl;
2✔
595
        }
596
    }
11✔
597
}
20✔
598

599
#include "DataManager.hpp"
600
#include "IO/LoaderRegistry.hpp"
601
#include "transforms/TransformPipeline.hpp"
602
#include "transforms/TransformRegistry.hpp"
603

604
#include <filesystem>
605
#include <fstream>
606
#include <iostream>
607

608
TEST_CASE("Data Transform: Line Angle - JSON pipeline", "[transforms][line_angle][json]") {
1✔
609
    const nlohmann::json json_config = {
1✔
610
        {"steps", {{
611
            {"step_id", "line_angle_step_1"},
612
            {"transform_name", "Calculate Line Angle"},
613
            {"input_key", "TestLine.line1"},
614
            {"output_key", "LineAngles"},
615
            {"parameters", {
616
                {"position", 0.5},
1✔
617
                {"method", "Direct Points"},
618
                {"polynomial_order", 3},
1✔
619
                {"reference_x", 1.0},
1✔
620
                {"reference_y", 0.0}
1✔
621
            }}
622
        }}}
623
    };
50✔
624

625
    DataManager dm;
1✔
626
    TransformRegistry registry;
1✔
627

628
    auto time_frame = std::make_shared<TimeFrame>();
1✔
629
    dm.setTime(TimeKey("default"), time_frame);
1✔
630

631
    // Create test line data - 45-degree line
632
    auto line_data = std::make_shared<LineData>();
1✔
633
    std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
634
    std::vector<float> y_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
635
    line_data->addAtTime(TimeFrameIndex(100), x_coords, y_coords);
1✔
636
    line_data->setTimeFrame(time_frame);
1✔
637
    dm.setData("TestLine.line1", line_data, TimeKey("default"));
3✔
638

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

643
    // Verify the results
644
    auto angle_series = dm.getData<AnalogTimeSeries>("LineAngles");
3✔
645
    REQUIRE(angle_series != nullptr);
1✔
646
    REQUIRE(angle_series->getAnalogTimeSeries().size() == 1);
1✔
647
    REQUIRE(angle_series->getTimeSeries().size() == 1);
1✔
648
    REQUIRE(angle_series->getTimeSeries()[0] == TimeFrameIndex(100));
1✔
649
    // Should be 45 degrees for a 45-degree line
650
    REQUIRE_THAT(angle_series->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(45.0f, 0.001f));
1✔
651
}
54✔
652

653
#include "transforms/ParameterFactory.hpp"
654
#include "transforms/TransformRegistry.hpp"
655

656
TEST_CASE("Data Transform: Line Angle - Parameter Factory", "[transforms][line_angle][factory]") {
1✔
657
    auto& factory = ParameterFactory::getInstance();
1✔
658
    factory.initializeDefaultSetters();
1✔
659

660
    auto params_base = std::make_unique<LineAngleParameters>();
1✔
661
    REQUIRE(params_base != nullptr);
1✔
662

663
    const nlohmann::json params_json = {
1✔
664
        {"position", 0.75},
1✔
665
        {"method", "Polynomial Fit"},
666
        {"polynomial_order", 5},
1✔
667
        {"reference_x", 0.0},
1✔
668
        {"reference_y", 1.0}
1✔
669
    };
22✔
670

671
    for (auto const& [key, val] : params_json.items()) {
6✔
672
        factory.setParameter("Calculate Line Angle", params_base.get(), key, val, nullptr);
15✔
673
    }
1✔
674

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

678
    REQUIRE(params->position == 0.75f);
1✔
679
    REQUIRE(params->method == AngleCalculationMethod::PolynomialFit);
1✔
680
    REQUIRE(params->polynomial_order == 5);
1✔
681
    REQUIRE(params->reference_x == 0.0f);
1✔
682
    REQUIRE(params->reference_y == 1.0f);
1✔
683
}
22✔
684

685
TEST_CASE("Data Transform: Line Angle - load_data_from_json_config", "[transforms][line_angle][json_config]") {
1✔
686
    // Create DataManager and populate it with LineData in code
687
    DataManager dm;
1✔
688

689
    // Create a TimeFrame for our data
690
    auto time_frame = std::make_shared<TimeFrame>();
1✔
691
    dm.setTime(TimeKey("default"), time_frame);
1✔
692
    
693
    // Create test line data in code - horizontal line
694
    auto test_line = std::make_shared<LineData>();
1✔
695
    std::vector<float> x_coords = {0.0f, 1.0f, 2.0f, 3.0f};
3✔
696
    std::vector<float> y_coords = {1.0f, 1.0f, 1.0f, 1.0f}; // Horizontal line
3✔
697
    test_line->addAtTime(TimeFrameIndex(100), x_coords, y_coords);
1✔
698
    test_line->setTimeFrame(time_frame);
1✔
699
    
700
    // Store the line data in DataManager with a known key
701
    dm.setData("test_line", test_line, TimeKey("default"));
3✔
702
    
703
    // Create JSON configuration for transformation pipeline using unified format
704
    const char* json_config = 
1✔
705
        "[\n"
706
        "{\n"
707
        "    \"transformations\": {\n"
708
        "        \"metadata\": {\n"
709
        "            \"name\": \"Line Angle Pipeline\",\n"
710
        "            \"description\": \"Test line angle calculation on line data\",\n"
711
        "            \"version\": \"1.0\"\n"
712
        "        },\n"
713
        "        \"steps\": [\n"
714
        "            {\n"
715
        "                \"step_id\": \"1\",\n"
716
        "                \"transform_name\": \"Calculate Line Angle\",\n"
717
        "                \"phase\": \"analysis\",\n"
718
        "                \"input_key\": \"test_line\",\n"
719
        "                \"output_key\": \"line_angles\",\n"
720
        "                \"parameters\": {\n"
721
        "                    \"position\": 0.5,\n"
722
        "                    \"method\": \"Direct Points\",\n"
723
        "                    \"polynomial_order\": 3,\n"
724
        "                    \"reference_x\": 1.0,\n"
725
        "                    \"reference_y\": 0.0\n"
726
        "                }\n"
727
        "            }\n"
728
        "        ]\n"
729
        "    }\n"
730
        "}\n"
731
        "]";
732
    
733
    // Create temporary directory and write JSON config to file
734
    std::filesystem::path test_dir = std::filesystem::temp_directory_path() / "line_angle_pipeline_test";
1✔
735
    std::filesystem::create_directories(test_dir);
1✔
736
    
737
    std::filesystem::path json_filepath = test_dir / "pipeline_config.json";
1✔
738
    {
739
        std::ofstream json_file(json_filepath);
1✔
740
        REQUIRE(json_file.is_open());
1✔
741
        json_file << json_config;
1✔
742
        json_file.close();
1✔
743
    }
1✔
744
    
745
    // Execute the transformation pipeline using load_data_from_json_config
746
    auto data_info_list = load_data_from_json_config(&dm, json_filepath.string());
1✔
747
    
748
    // Verify the transformation was executed and results are available
749
    auto result_angles = dm.getData<AnalogTimeSeries>("line_angles");
3✔
750
    REQUIRE(result_angles != nullptr);
1✔
751
    
752
    // Verify the line angle results - horizontal line should have 0 degrees
753
    REQUIRE(result_angles->getAnalogTimeSeries().size() == 1);
1✔
754
    REQUIRE(result_angles->getTimeSeries().size() == 1);
1✔
755
    REQUIRE(result_angles->getTimeSeries()[0] == TimeFrameIndex(100));
1✔
756
    REQUIRE_THAT(result_angles->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(0.0f, 0.001f));
1✔
757
    
758
    // Test another pipeline with different parameters (polynomial fit)
759
    const char* json_config_poly = 
1✔
760
        "[\n"
761
        "{\n"
762
        "    \"transformations\": {\n"
763
        "        \"metadata\": {\n"
764
        "            \"name\": \"Line Angle with Polynomial Fit\",\n"
765
        "            \"description\": \"Test line angle calculation with polynomial fitting\",\n"
766
        "            \"version\": \"1.0\"\n"
767
        "        },\n"
768
        "        \"steps\": [\n"
769
        "            {\n"
770
        "                \"step_id\": \"1\",\n"
771
        "                \"transform_name\": \"Calculate Line Angle\",\n"
772
        "                \"phase\": \"analysis\",\n"
773
        "                \"input_key\": \"test_line\",\n"
774
        "                \"output_key\": \"line_angles_poly\",\n"
775
        "                \"parameters\": {\n"
776
        "                    \"position\": 0.5,\n"
777
        "                    \"method\": \"Polynomial Fit\",\n"
778
        "                    \"polynomial_order\": 2,\n"
779
        "                    \"reference_x\": 1.0,\n"
780
        "                    \"reference_y\": 0.0\n"
781
        "                }\n"
782
        "            }\n"
783
        "        ]\n"
784
        "    }\n"
785
        "}\n"
786
        "]";
787
    
788
    std::filesystem::path json_filepath_poly = test_dir / "pipeline_config_poly.json";
1✔
789
    {
790
        std::ofstream json_file(json_filepath_poly);
1✔
791
        REQUIRE(json_file.is_open());
1✔
792
        json_file << json_config_poly;
1✔
793
        json_file.close();
1✔
794
    }
1✔
795
    
796
    // Execute the polynomial fit pipeline
797
    auto data_info_list_poly = load_data_from_json_config(&dm, json_filepath_poly.string());
1✔
798
    
799
    // Verify the polynomial fit results
800
    auto result_angles_poly = dm.getData<AnalogTimeSeries>("line_angles_poly");
3✔
801
    REQUIRE(result_angles_poly != nullptr);
1✔
802
    
803
    // For a horizontal line, polynomial fit should also give 0 degrees
804
    REQUIRE(result_angles_poly->getAnalogTimeSeries().size() == 1);
1✔
805
    REQUIRE(result_angles_poly->getTimeSeries().size() == 1);
1✔
806
    REQUIRE_THAT(result_angles_poly->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(0.0f, 0.001f));
1✔
807
    
808
    // Test with different reference vector
809
    const char* json_config_ref = 
1✔
810
        "[\n"
811
        "{\n"
812
        "    \"transformations\": {\n"
813
        "        \"metadata\": {\n"
814
        "            \"name\": \"Line Angle with Vertical Reference\",\n"
815
        "            \"description\": \"Test line angle calculation with vertical reference\",\n"
816
        "            \"version\": \"1.0\"\n"
817
        "        },\n"
818
        "        \"steps\": [\n"
819
        "            {\n"
820
        "                \"step_id\": \"1\",\n"
821
        "                \"transform_name\": \"Calculate Line Angle\",\n"
822
        "                \"phase\": \"analysis\",\n"
823
        "                \"input_key\": \"test_line\",\n"
824
        "                \"output_key\": \"line_angles_ref\",\n"
825
        "                \"parameters\": {\n"
826
        "                    \"position\": 0.5,\n"
827
        "                    \"method\": \"Direct Points\",\n"
828
        "                    \"polynomial_order\": 3,\n"
829
        "                    \"reference_x\": 0.0,\n"
830
        "                    \"reference_y\": 1.0\n"
831
        "                }\n"
832
        "            }\n"
833
        "        ]\n"
834
        "    }\n"
835
        "}\n"
836
        "]";
837
    
838
    std::filesystem::path json_filepath_ref = test_dir / "pipeline_config_ref.json";
1✔
839
    {
840
        std::ofstream json_file(json_filepath_ref);
1✔
841
        REQUIRE(json_file.is_open());
1✔
842
        json_file << json_config_ref;
1✔
843
        json_file.close();
1✔
844
    }
1✔
845
    
846
    // Execute the reference vector pipeline
847
    auto data_info_list_ref = load_data_from_json_config(&dm, json_filepath_ref.string());
1✔
848
    
849
    // Verify the reference vector results
850
    auto result_angles_ref = dm.getData<AnalogTimeSeries>("line_angles_ref");
3✔
851
    REQUIRE(result_angles_ref != nullptr);
1✔
852
    
853
    // With vertical reference (0,1), horizontal line should be -90 degrees
854
    REQUIRE(result_angles_ref->getAnalogTimeSeries().size() == 1);
1✔
855
    REQUIRE(result_angles_ref->getTimeSeries().size() == 1);
1✔
856
    REQUIRE_THAT(result_angles_ref->getAnalogTimeSeries()[0], Catch::Matchers::WithinAbs(-90.0f, 0.001f));
1✔
857
    
858
    // Cleanup
859
    try {
860
        std::filesystem::remove_all(test_dir);
1✔
UNCOV
861
    } catch (const std::exception& e) {
×
UNCOV
862
        std::cerr << "Warning: Cleanup failed: " << e.what() << std::endl;
×
UNCOV
863
    }
×
864
}
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

© 2025 Coveralls, Inc