3#define DOCTEST_CONFIG_IMPLEMENT
5#include "doctest_utils.h"
13 return helios::runDoctestWithValidation(argc, argv);
16DOCTEST_TEST_CASE(
"LiDAR Single Voxel Sphere Test") {
20 DOCTEST_CHECK_NOTHROW(pointcloud.
loadXML(
"plugins/lidar/xml/sphere.xml"));
29DOCTEST_TEST_CASE(
"LiDAR triangulateHitPoints Cancel Flag") {
35 auto run = [&](
bool cancel) -> std::size_t {
38 pointcloud.
loadXML(
"plugins/lidar/xml/sphere.xml");
39 int flag = cancel ? 1 : 0;
45 std::size_t baseline = run(
false);
46 std::size_t cancelled = run(
true);
47 DOCTEST_CHECK(baseline > 0);
48 DOCTEST_CHECK(cancelled == 0);
54 DOCTEST_CHECK_NOTHROW(reference.
loadXML(
"plugins/lidar/xml/sphere.xml"));
61 DOCTEST_CHECK_NOTHROW(cleared.
loadXML(
"plugins/lidar/xml/sphere.xml"));
69DOCTEST_TEST_CASE(
"LiDAR syntheticScan Progress Callback") {
74 std::vector<std::string> columnFormat;
75 for (
int i = 0; i < 2; i++) {
76 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 100, 0.0f, M_PI, 200, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
77 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
82 DOCTEST_CHECK_NOTHROW(
context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true));
84 std::vector<float> progress_values;
85 std::vector<std::string> progress_messages;
87 progress_values.push_back(progress);
88 progress_messages.push_back(message);
94 DOCTEST_CHECK(!progress_values.empty());
95 DOCTEST_CHECK(progress_values.back() == doctest::Approx(1.0f));
96 bool monotonic =
true;
97 for (
size_t i = 1; i < progress_values.size(); i++) {
98 if (progress_values[i] < progress_values[i - 1]) {
102 DOCTEST_CHECK(monotonic);
103 DOCTEST_CHECK(progress_messages.back() ==
"Synthetic scan");
106 size_t fired_count = progress_values.size();
109 DOCTEST_CHECK(progress_values.size() == fired_count);
116 for (
int i = 0; i < 3; i++) {
117 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 20, 0.0f, M_PI, 20, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
118 DOCTEST_CHECK_NOTHROW(empty_cloud.
addScan(scan));
121 std::vector<float> empty_progress;
122 empty_cloud.
setProgressCallback([&](
float progress,
const std::string &) { empty_progress.push_back(progress); });
123 DOCTEST_CHECK_NOTHROW(empty_cloud.
syntheticScan(&empty_context));
124 DOCTEST_CHECK(!empty_progress.empty());
125 DOCTEST_CHECK(empty_progress.back() == doctest::Approx(1.0f));
128DOCTEST_TEST_CASE(
"LiDAR syntheticScan Cancel Flag") {
133 std::vector<std::string> columnFormat;
135 auto run = [&](
bool cancel) -> std::size_t {
138 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 100, 0.0f, M_PI, 200, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
142 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
143 int flag = cancel ? 1 : 0;
149 std::size_t baseline = run(
false);
150 std::size_t cancelled = run(
true);
151 DOCTEST_CHECK(baseline > 0);
152 DOCTEST_CHECK(cancelled < baseline);
153 DOCTEST_CHECK(cancelled == 0);
158 auto run_record_misses = [&](
bool cancel) -> std::size_t {
161 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 100, 0.0f, M_PI, 200, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
165 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
166 int flag = cancel ? 1 : 0;
172 std::size_t baseline_misses = run_record_misses(
false);
173 std::size_t cancelled_misses = run_record_misses(
true);
174 DOCTEST_CHECK(baseline_misses > 0);
175 DOCTEST_CHECK(cancelled_misses < baseline_misses);
176 DOCTEST_CHECK(cancelled_misses == 0);
181 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 100, 0.0f, M_PI, 200, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
185 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
193DOCTEST_TEST_CASE(
"LiDAR syntheticScan Progress Pointer") {
198 std::vector<std::string> columnFormat;
202 for (
int i = 0; i < 3; i++) {
203 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 100, 0.0f, M_PI, 200, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
204 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
209 DOCTEST_CHECK_NOTHROW(
context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true));
211 volatile int progress = -1;
215 std::vector<int> observed;
216 cloud.
setProgressCallback([&](
float,
const std::string &) { observed.push_back(
static_cast<int>(progress)); });
225 DOCTEST_CHECK(observed[i] ==
static_cast<int>(i));
227 DOCTEST_CHECK(progress ==
static_cast<int>(cloud.
getScanCount()));
235 for (
int i = 0; i < 3; i++) {
236 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 20, 0.0f, M_PI, 20, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
237 DOCTEST_CHECK_NOTHROW(empty_cloud.
addScan(scan));
240 volatile int empty_progress = -1;
241 std::vector<int> empty_observed;
243 empty_cloud.
setProgressCallback([&](
float,
const std::string &) { empty_observed.push_back(
static_cast<int>(empty_progress)); });
244 DOCTEST_CHECK_NOTHROW(empty_cloud.
syntheticScan(&empty_context));
245 DOCTEST_CHECK(empty_observed.size() >= empty_cloud.
getScanCount());
247 DOCTEST_CHECK(empty_observed[i] ==
static_cast<int>(i));
249 DOCTEST_CHECK(empty_progress ==
static_cast<int>(empty_cloud.
getScanCount()));
252 volatile int stale = 42;
255 DOCTEST_CHECK_NOTHROW(empty_cloud.
syntheticScan(&empty_context));
256 DOCTEST_CHECK(stale == 42);
259DOCTEST_TEST_CASE(
"LiDAR Single Voxel Isotropic Patches Test") {
264 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
267 float thetaMin = 0.0f;
268 float thetaMax =
M_PI;
270 float phiMax = 2.0f *
M_PI;
271 float exitDiameter = 0.0f;
272 float beamDivergence = 0.0f;
273 std::vector<std::string> columnFormat;
275 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
276 DOCTEST_CHECK_NOTHROW(synthetic_1.
addScan(scan));
279 vec3 grid_center(0.0f, 0.0f, 0.5f);
280 vec3 grid_size(1.0f, 1.0f, 1.0f);
282 DOCTEST_CHECK_NOTHROW(synthetic_1.
addGrid(grid_center, grid_size, grid_divisions, 0));
287 std::vector<uint> UUIDs_1 = context_2.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
289 float LAD_exact = 0.f;
290 for (
uint UUID: UUIDs_1) {
295 float Gtheta_exact_numerator = 0.f;
296 float Gtheta_exact_denominator = 0.f;
297 for (
uint UUID: UUIDs_1) {
301 vec3 raydir = vertices.front() - scan_origin;
305 float normal_dot_ray = fabs(normal * raydir);
306 Gtheta_exact_numerator += normal_dot_ray * area;
307 Gtheta_exact_denominator += area;
310 float Gtheta_exact = 0.f;
311 if (Gtheta_exact_denominator > 0) {
312 Gtheta_exact = Gtheta_exact_numerator / Gtheta_exact_denominator;
315 DOCTEST_CHECK_NOTHROW(synthetic_1.
syntheticScan(&context_2,
false,
true));
321 DOCTEST_CHECK(LAD == LAD);
322 DOCTEST_CHECK(fabs(LAD - LAD_exact) / LAD_exact == doctest::Approx(0.0f).epsilon(0.02f));
326 DOCTEST_CHECK(Gtheta == Gtheta);
327 DOCTEST_CHECK(fabs(Gtheta - Gtheta_exact) / Gtheta_exact == doctest::Approx(0.0f).epsilon(0.05f));
330DOCTEST_TEST_CASE(
"LiDAR setExternalTriangulation Binning Test") {
339 std::vector<std::string> columnFormat;
340 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 10, 0.0f, M_PI, 10, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
341 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
355 std::vector<vec3> triangle_vertices = {a0, a1, a2, b0, b1, b2};
356 std::vector<int> scanIDs = {0, 0};
368 DOCTEST_CHECK(triA.gridcell == 0);
369 DOCTEST_CHECK(triB.gridcell == -1);
370 DOCTEST_CHECK(triA.scanID == 0);
371 DOCTEST_CHECK(triA.vertex0 == a0);
372 DOCTEST_CHECK(triA.vertex1 == a1);
373 DOCTEST_CHECK(triA.vertex2 == a2);
376DOCTEST_TEST_CASE(
"LiDAR setExternalTriangulation Error Conditions Test") {
380 std::vector<std::string> columnFormat;
381 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 10, 0.0f, M_PI, 10, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
382 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
423DOCTEST_TEST_CASE(
"LiDAR setExternalTriangulation Degenerate Dropping Test") {
427 std::vector<std::string> columnFormat;
428 ScanMetadata scan(
make_vec3(-5.0f, 0.0f, 0.5f), 10, 0.0f, M_PI, 10, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
429 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
440 std::vector<vec3> triangle_vertices = {v0, v1, v2, d0, d1, d2};
441 std::vector<int> scanIDs = {0, 0};
451DOCTEST_TEST_CASE(
"LiDAR Eight Voxel Isotropic Patches Test") {
456 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
459 float thetaMin = 0.0f;
460 float thetaMax =
M_PI;
462 float phiMax = 2.0f *
M_PI;
463 float exitDiameter = 0.0f;
464 float beamDivergence = 0.0f;
465 std::vector<std::string> columnFormat;
467 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
468 DOCTEST_CHECK_NOTHROW(synthetic_2.
addScan(scan));
471 vec3 grid_center(0.0f, 0.0f, 0.5f);
472 vec3 grid_size(1.0f, 1.0f, 1.0f);
474 DOCTEST_CHECK_NOTHROW(synthetic_2.
addGrid(grid_center, grid_size, grid_divisions, 0));
479 std::vector<uint> UUIDs_1 = context_2.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
481 std::vector<float> LAD_ex(8, 0);
482 for (
uint UUID: UUIDs_1) {
495 int ID = k * 4 + j * 2 + i;
498 LAD_ex.at(ID) += area / (gsize.
x * gsize.
y * gsize.
z);
501 DOCTEST_CHECK_NOTHROW(synthetic_2.
syntheticScan(&context_2,
false,
true));
508 RMSE += powf(LAD - LAD_ex.at(i), 2) / float(synthetic_2.
getGridCellCount());
515 DOCTEST_CHECK(RMSE == doctest::Approx(0.0f).epsilon(0.07f));
518DOCTEST_TEST_CASE(
"LiDAR Thin-Layer Vertical Symmetry Test") {
528 std::vector<uint> UUIDs = context_sym.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
531 const float origins[4][3] = {{-5, 0, 0.5f}, {0, -5, 0.5f}, {5, 0, 0.5f}, {0, 5, 0.5f}};
533 for (
float sizez: {0.5f, 0.45f, 0.41f, 0.40f, 0.35f}) {
538 for (
int sidx = 0; sidx < 4; sidx++) {
539 ScanMetadata scan(
make_vec3(origins[sidx][0], origins[sidx][1], origins[sidx][2]), 2000, 0.f, M_PI, 4000, 0.f, 2.f * M_PI, 0.f, 0.f, 0.f, 0.f, std::vector<std::string>{});
546 DOCTEST_CHECK_NOTHROW(lidar_sym.
syntheticScan(&context_sym,
true,
true));
550 float lower = 0.f, upper = 0.f;
556 DOCTEST_CHECK_MESSAGE(fabs(lad - 0.1f) > 5e-3f,
"Cell " << i <<
" pinned at the LAD floor (~0.1) at size.z=" << sizez);
571 float denom = std::max(lower, upper);
572 bool symmetric = (denom > 0.f) && (fabs(upper - lower) / denom < 0.25f);
573 DOCTEST_CHECK_MESSAGE(symmetric,
"Thin-layer LAD asymmetry at size.z=" << sizez <<
": lower=" << lower <<
" upper=" << upper);
577DOCTEST_TEST_CASE(
"LiDAR Single Voxel Anisotropic Patches Test") {
582 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
585 float thetaMin = 0.0f;
586 float thetaMax =
M_PI;
588 float phiMax = 2.0f *
M_PI;
589 float exitDiameter = 0.0f;
590 float beamDivergence = 0.0f;
591 std::vector<std::string> columnFormat;
593 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
594 DOCTEST_CHECK_NOTHROW(synthetic_3.
addScan(scan));
597 vec3 grid_center(0.0f, 0.0f, 0.5f);
598 vec3 grid_size(1.0f, 1.0f, 1.0f);
600 DOCTEST_CHECK_NOTHROW(synthetic_3.
addGrid(grid_center, grid_size, grid_divisions, 0));
605 std::vector<uint> UUIDs_1 = context_2.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_erectophile.xml",
true);
607 float LAD_exact = 0.f;
608 for (
uint UUID: UUIDs_1) {
613 float Gtheta_exact_numerator = 0.f;
614 float Gtheta_exact_denominator = 0.f;
615 for (
uint UUID: UUIDs_1) {
619 vec3 raydir = vertices.front() - scan_origin;
623 float normal_dot_ray = fabs(normal * raydir);
624 Gtheta_exact_numerator += normal_dot_ray * area;
625 Gtheta_exact_denominator += area;
628 float Gtheta_exact = 0.f;
629 if (Gtheta_exact_denominator > 0) {
630 Gtheta_exact = Gtheta_exact_numerator / Gtheta_exact_denominator;
633 DOCTEST_CHECK_NOTHROW(synthetic_3.
syntheticScan(&context_2,
false,
true));
639 DOCTEST_CHECK(LAD == LAD);
640 DOCTEST_CHECK(fabs(LAD - LAD_exact) / LAD_exact == doctest::Approx(0.0f).epsilon(0.03f));
644 DOCTEST_CHECK(Gtheta == Gtheta);
645 DOCTEST_CHECK(fabs(Gtheta - Gtheta_exact) / Gtheta_exact == doctest::Approx(0.0f).epsilon(0.05f));
648DOCTEST_TEST_CASE(
"LiDAR Synthetic Almond Tree Test") {
655 DOCTEST_CHECK_NOTHROW(synthetic_4.
loadXML(
"plugins/lidar/xml/almond.xml"));
656 DOCTEST_CHECK_NOTHROW(synthetic_4.
syntheticScan(&context_4,
false,
true));
665 std::vector<float> total_area;
666 total_area.resize(Ncells);
668 std::vector<float> Gtheta;
669 Gtheta.resize(Ncells);
671 std::vector<float> area_sum;
672 area_sum.resize(Ncells, 0.f);
673 std::vector<float> sin_sum;
674 sin_sum.resize(Ncells, 0.f);
675 std::vector<uint> cell_tri_count;
676 cell_tri_count.resize(Ncells, 0);
679 for (
int p = 0; p < UUIDs.size(); p++) {
681 uint UUID = UUIDs.at(p);
688 if (gridCell >= 0 && gridCell < Ncells) {
697 vec3 raydir = vertices.front() - origin;
703 Gtheta.at(gridCell) += fabs(normal * raydir) * area * fabs(sin(theta));
705 area_sum.at(gridCell) += area;
706 sin_sum.at(gridCell) += fabs(sin(theta));
707 cell_tri_count.at(gridCell) += 1;
713 for (
uint v = 0; v < Ncells; v++) {
714 if (cell_tri_count[v] > 0) {
715 Gtheta[v] *= float(cell_tri_count[v]) / (area_sum[v] * sin_sum[v]);
719 float RMSE_LAD = 0.f;
720 float bias_LAD = 0.f;
721 float RMSE_Gtheta = 0.f;
722 for (
uint i = 0; i < Ncells; i++) {
724 if (LAD == LAD && total_area.at(i) > 0 && total_area.at(i) == total_area.at(i)) {
725 RMSE_LAD += pow(LAD - total_area.at(i), 2) / float(Ncells);
726 bias_LAD += (LAD - total_area.at(i)) /
float(Ncells);
729 if (Gtheta_bar == Gtheta_bar && Gtheta.at(i) > 0 && Gtheta.at(i) == Gtheta.at(i)) {
730 RMSE_Gtheta += pow(Gtheta_bar - Gtheta.at(i), 2) / float(Ncells);
733 RMSE_LAD = sqrt(RMSE_LAD);
734 RMSE_Gtheta = sqrt(RMSE_Gtheta);
736 DOCTEST_CHECK(RMSE_LAD <= 0.35f);
737 DOCTEST_CHECK(bias_LAD <= 0.0f);
738 DOCTEST_CHECK(RMSE_Gtheta <= 0.15f);
739 DOCTEST_CHECK(RMSE_LAD != 0.f);
742DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Append/Overwrite Test") {
744 context_test.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
749 DOCTEST_CHECK_NOTHROW(synthetic_test.
loadXML(
"plugins/lidar/xml/synthetic_test.xml"));
752 DOCTEST_CHECK_NOTHROW(synthetic_test.
syntheticScan(&context_test));
754 DOCTEST_CHECK(hit_count_first > 0);
757 DOCTEST_CHECK_NOTHROW(synthetic_test.
syntheticScan(&context_test,
true));
759 DOCTEST_CHECK(hit_count_append == 2 * hit_count_first);
762 DOCTEST_CHECK_NOTHROW(synthetic_test.
syntheticScan(&context_test,
false));
764 DOCTEST_CHECK(hit_count_overwrite == hit_count_first);
768 DOCTEST_CHECK_NOTHROW(synthetic_test.
syntheticScan(&context_test,
false,
false,
false));
770 DOCTEST_CHECK(hit_count_overwrite2 == hit_count_first);
773 DOCTEST_CHECK_NOTHROW(synthetic_test.
syntheticScan(&context_test, 1, 0.0f,
true));
775 DOCTEST_CHECK(hit_count_append2 == 2 * hit_count_first);
778DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam Scan Geometry") {
780 vec3 scan_origin(0.f, 0.f, 1.f);
781 std::vector<float> elevation_deg = {-15.f, -13.f, -11.f, -9.f, -7.f, -5.f, -3.f, -1.f, 1.f, 3.f, 5.f, 7.f, 9.f, 11.f, 13.f, 15.f};
782 std::vector<float> beam_zenith(elevation_deg.size());
783 for (
size_t k = 0; k < elevation_deg.size(); k++) {
784 beam_zenith[k] = 0.5f * float(M_PI) - elevation_deg[k] * float(M_PI) / 180.f;
787 std::vector<std::string> columnFormat;
789 ScanMetadata scan(scan_origin, beam_zenith, Nphi, 0.f, 2.f *
float(M_PI), 0.f, 0.f, 0.f, 0.f, columnFormat);
793 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
800 DOCTEST_REQUIRE(returned_angles.size() == beam_zenith.size());
801 bool angles_match =
true;
802 for (
size_t k = 0; k < beam_zenith.size(); k++) {
803 if (fabs(returned_angles[k] - beam_zenith[k]) > 1e-5f) {
804 angles_match =
false;
807 DOCTEST_CHECK(angles_match);
811 float zmin = *std::min_element(beam_zenith.begin(), beam_zenith.end());
812 float zmax = *std::max_element(beam_zenith.begin(), beam_zenith.end());
813 DOCTEST_CHECK(theta_range.
x == doctest::Approx(zmin));
814 DOCTEST_CHECK(theta_range.
y == doctest::Approx(zmax));
817 bool rc_roundtrip_ok =
true;
818 for (
uint row = 0; row < beam_zenith.size(); row++) {
820 if (fabs(dir.
zenith - beam_zenith[row]) > 1e-4f) {
821 rc_roundtrip_ok =
false;
823 int2 rc = scan.direction2rc(dir);
824 if (rc.
x !=
int(row)) {
825 rc_roundtrip_ok =
false;
828 DOCTEST_CHECK(rc_roundtrip_ok);
831DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam Empty Channels Error") {
833 vec3 origin(0.f, 0.f, 1.f);
834 std::vector<float> empty_angles;
835 std::vector<std::string> columnFormat;
836 DOCTEST_CHECK_THROWS(
ScanMetadata(origin, empty_angles, 100, 0.f, 2.f *
float(M_PI), 0.f, 0.f, 0.f, 0.f, columnFormat));
839DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam Synthetic Scan") {
841 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
848 vec3 scan_origin(-5.f, 0.f, 0.5f);
849 std::vector<float> beam_elev;
850 std::vector<float> beam_zenith;
851 for (
int e = -24; e <= 24; e++) {
852 beam_elev.push_back(0.5f *
float(e) *
float(M_PI) / 180.f);
853 beam_zenith.push_back(0.5f *
float(M_PI) - 0.5f *
float(e) *
float(M_PI) / 180.f);
858 const uint channels =
uint(beam_elev.size());
859 const float azimuthStep_rad = (360.f / 4000.f) *
float(M_PI) / 180.f;
860 const uint Nphi = 4000;
861 const float PRF = 1.0e6f;
862 const double one_rev_duration = double(channels) * 4000.0 / double(PRF);
863 const std::vector<double> traj_t = {0.0, one_rev_duration};
864 const std::vector<vec3> traj_pos = {scan_origin, scan_origin};
866 DOCTEST_CHECK_NOTHROW(cloud.
addScanSpinning(beam_elev, azimuthStep_rad, PRF, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>()));
873 DOCTEST_CHECK(Nhits > 0);
877 uint Ntheta =
uint(beam_zenith.size());
878 bool channel_exists_all =
true;
879 bool channel_in_range =
true;
880 bool any_real_hit =
false;
881 for (
uint h = 0; h < Nhits; h++) {
883 channel_exists_all =
false;
887 if (ch < 0 || ch >=
int(Ntheta)) {
888 channel_in_range =
false;
894 DOCTEST_CHECK(channel_exists_all);
895 DOCTEST_CHECK(channel_in_range);
896 DOCTEST_CHECK(any_real_hit);
903 DOCTEST_CHECK(LAD == LAD);
904 DOCTEST_CHECK(LAD > 0.f);
907 const std::string out_dir =
"lidar_spinmb_export_tmp";
908 std::filesystem::remove_all(out_dir);
909 const std::string xml_out = out_dir +
"/scans.xml";
910 DOCTEST_CHECK_NOTHROW(cloud.
exportScans(xml_out.c_str()));
914 DOCTEST_CHECK_NOTHROW(reloaded.
loadXML(xml_out.c_str()));
921 DOCTEST_REQUIRE(reloaded_angles.size() == beam_zenith.size());
922 bool reloaded_angles_match =
true;
923 for (
size_t k = 0; k < beam_zenith.size(); k++) {
924 if (fabs(reloaded_angles[k] - beam_zenith[k]) > 1e-3f) {
925 reloaded_angles_match =
false;
928 DOCTEST_CHECK(reloaded_angles_match);
929 std::filesystem::remove_all(out_dir);
936static std::vector<RisleyPrism> makeMid40Prisms() {
937 return {
RisleyPrism(18.7481 * M_PI / 180.0, 1.51, -121.5657 * 2.0 * M_PI),
RisleyPrism(17.9634 * M_PI / 180.0, 1.51, 77.7430 * 2.0 * M_PI)};
940DOCTEST_TEST_CASE(
"LiDAR Risley Prism Pattern Geometry") {
946 const vec3 scan_origin(0.f, 0.f, 1.f);
947 const float PRF = 100000.f;
948 const double duration = 0.05;
949 const std::vector<double> traj_t = {0.0, duration};
950 const std::vector<vec3> traj_pos = {scan_origin, scan_origin};
954 DOCTEST_CHECK_NOTHROW(scanID = cloud.
addScanRisley(makeMid40Prisms(), 1.0, PRF, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>()));
963 DOCTEST_REQUIRE(prisms.size() == 2);
968 ScanMetadata sm(scan_origin, 1u, 0.f,
float(M_PI), 5000u, 0.f, 2.f *
float(M_PI), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>());
970 sm.risley_prisms = prisms;
971 sm.risley_refractive_index_air = 1.0;
972 sm.pulse_period = 1.0 / double(PRF);
973 float max_halfangle = 0.f;
974 std::vector<vec3> dirs;
976 for (
uint k = 0; k < 5000; k++) {
981 float halfangle = acosf(std::max(-1.f, std::min(1.f, d.
y)));
982 if (halfangle > max_halfangle) {
983 max_halfangle = halfangle;
988 DOCTEST_CHECK(max_halfangle < 25.f *
float(M_PI) / 180.f);
989 DOCTEST_CHECK(max_halfangle > 10.f *
float(M_PI) / 180.f);
993 int near_duplicates = 0;
994 const size_t Ncheck = 1500;
995 for (
size_t a = 0; a < Ncheck; a++) {
996 for (
size_t b = a + 1; b < Ncheck; b++) {
997 if ((dirs[a] - dirs[b]).magnitude() < 1e-6f) {
1002 DOCTEST_CHECK(near_duplicates == 0);
1005DOCTEST_TEST_CASE(
"LiDAR Risley Prism Synthetic Scan") {
1007 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
1013 const vec3 scan_origin(0.f, -5.f, 0.5f);
1014 const float PRF = 100000.f;
1015 const double duration = 0.1;
1016 const std::vector<double> traj_t = {0.0, duration};
1017 const std::vector<vec3> traj_pos = {scan_origin, scan_origin};
1021 DOCTEST_CHECK_NOTHROW(scanID = cloud.
addScanRisley(makeMid40Prisms(), 1.0, PRF, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>()));
1026 DOCTEST_CHECK(Nhits > 0);
1029 bool timestamp_all =
true;
1030 bool pulse_id_all =
true;
1031 bool no_channel =
true;
1032 bool any_real_hit =
false;
1033 for (
uint h = 0; h < Nhits; h++) {
1035 timestamp_all =
false;
1038 pulse_id_all =
false;
1044 any_real_hit =
true;
1047 DOCTEST_CHECK(timestamp_all);
1048 DOCTEST_CHECK(pulse_id_all);
1049 DOCTEST_CHECK(no_channel);
1050 DOCTEST_CHECK(any_real_hit);
1056DOCTEST_TEST_CASE(
"LiDAR Risley Prism Fail-Fast Errors") {
1060 const std::vector<double> traj_t = {0.0, 0.05};
1063 const std::vector<std::string> cf;
1066 DOCTEST_CHECK_THROWS(cloud.
addScanRisley(std::vector<RisleyPrism>(), 1.0, 100000.f, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, cf));
1068 DOCTEST_CHECK_THROWS(cloud.
addScanRisley(makeMid40Prisms(), 1.0, 0.f, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, cf));
1070 DOCTEST_CHECK_THROWS(cloud.
addScanRisley(makeMid40Prisms(), 1.0, 100000.f, std::vector<double>(), std::vector<vec3>(), std::vector<vec4>(),
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, cf));
1072 const std::vector<double> traj_t_zero = {1.0, 1.0};
1073 DOCTEST_CHECK_THROWS(cloud.
addScanRisley(makeMid40Prisms(), 1.0, 100000.f, traj_t_zero, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, cf));
1076DOCTEST_TEST_CASE(
"LiDAR Risley Prism XML Round-Trip") {
1085 const vec3 scan_origin(0.f, 0.f, 1.f);
1086 const float PRF = 100000.f;
1087 const std::vector<double> traj_t = {0.0, 0.05};
1088 const std::vector<vec3> traj_pos = {scan_origin, scan_origin};
1090 const std::vector<RisleyPrism> prisms = makeMid40Prisms();
1093 DOCTEST_CHECK_NOTHROW(scanID = cloud.
addScanRisley(prisms, 1.0, PRF, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>{
"x",
"y",
"z",
"origin_x",
"origin_y",
"origin_z"}));
1098 const std::string out_dir =
"lidar_risley_export_tmp";
1099 std::filesystem::remove_all(out_dir);
1100 const std::string xml_out = out_dir +
"/scans.xml";
1101 DOCTEST_CHECK_NOTHROW(cloud.
exportScans(xml_out.c_str()));
1105 DOCTEST_CHECK_NOTHROW(reloaded.
loadXML(xml_out.c_str()));
1111 DOCTEST_REQUIRE(reloaded_prisms.size() == prisms.size());
1112 for (
size_t k = 0; k < prisms.size(); k++) {
1113 DOCTEST_CHECK(reloaded_prisms[k].wedge_angle == doctest::Approx(prisms[k].wedge_angle).epsilon(1e-4));
1114 DOCTEST_CHECK(reloaded_prisms[k].refractive_index == doctest::Approx(prisms[k].refractive_index).epsilon(1e-4));
1115 DOCTEST_CHECK(reloaded_prisms[k].rotor_rate == doctest::Approx(prisms[k].rotor_rate).epsilon(1e-4));
1116 DOCTEST_CHECK(reloaded_prisms[k].phase == doctest::Approx(prisms[k].phase));
1118 std::filesystem::remove_all(out_dir);
1121DOCTEST_TEST_CASE(
"LiDAR Multibeam Synthetic Scan Flat Wall") {
1136 vec3 scan_origin(-5.f, 0.f, 0.5f);
1137 std::vector<float> beam_zenith;
1138 for (
int e = -12; e <= 12; e++) {
1139 beam_zenith.push_back(0.5f *
float(M_PI) - 0.5f *
float(e) *
float(M_PI) / 180.f);
1142 std::vector<std::string> columnFormat;
1143 ScanMetadata scan(scan_origin, beam_zenith, Nphi, 0.f, 2.f *
float(M_PI), 0.f, 0.f, 0.f, 0.f, columnFormat);
1144 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
1149 DOCTEST_REQUIRE(Nhits > 0);
1154 uint Ntheta =
uint(beam_zenith.size());
1155 uint total_rays = Ntheta * Nphi;
1156 uint real_returns = 0;
1157 bool ranges_ok =
true;
1158 for (
uint h = 0; h < Nhits; h++) {
1164 double dist = cloud.
getHitData(h,
"distance");
1165 if (dist < 4.9 || dist > 5.5) {
1170 DOCTEST_CHECK(real_returns > 1000);
1171 DOCTEST_CHECK(real_returns >
uint(0.08 *
float(total_rays)));
1172 DOCTEST_CHECK(ranges_ok);
1175DOCTEST_TEST_CASE(
"LiDAR Raster Synthetic Scan Flat Wall") {
1185 vec3 scan_origin(-5.f, 0.f, 0.5f);
1190 float thetaMin = 0.5f * float(M_PI) - 6.f * float(M_PI) / 180.f;
1191 float thetaMax = 0.5f * float(M_PI) + 6.f * float(M_PI) / 180.f;
1192 float phiMin = 0.5f * float(M_PI) - 20.f * float(M_PI) / 180.f;
1193 float phiMax = 0.5f * float(M_PI) + 20.f * float(M_PI) / 180.f;
1194 std::vector<std::string> columnFormat;
1195 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.f, 0.f, 0.f, 0.f, columnFormat);
1196 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
1201 DOCTEST_REQUIRE(Nhits > 0);
1203 uint real_returns = 0;
1204 bool ranges_ok =
true;
1205 for (
uint h = 0; h < Nhits; h++) {
1209 double dist = cloud.
getHitData(h,
"distance");
1210 if (dist < 4.9 || dist > 5.4) {
1215 DOCTEST_CHECK(real_returns >
uint(0.5f *
float(Ntheta * Nphi)));
1216 DOCTEST_CHECK(ranges_ok);
1219DOCTEST_TEST_CASE(
"LiDAR Stratified Gaussian Footprint Sampler Invariants") {
1226 auto build_wall = [](
Context &ctx) {
1230 const vec3 scan_origin(-5.f, 0.f, 0.f);
1231 const float standoff = 5.f;
1232 const float aim_phi = 0.5f * float(M_PI);
1233 const float aim_theta = 0.5f * float(M_PI);
1234 const float beam_div = 0.5e-3f;
1235 const float exit_diam = 0.05f;
1236 const int rays_per_pulse = 200;
1237 const float pulse_threshold = 0.3f;
1246 std::vector<std::string> columnFormat;
1247 ScanMetadata scan(scan_origin, 1, aim_theta, aim_theta, 1, aim_phi, aim_phi, exit_diam, beam_div, 0.f , 0.f , columnFormat);
1248 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
1252 DOCTEST_REQUIRE(Nhits >= 1);
1254 DOCTEST_CHECK(Nhits == 1);
1256 double dist = cloud.
getHitData(0,
"distance");
1257 DOCTEST_CHECK(fabs(p.
x) < 1e-3f);
1258 DOCTEST_CHECK(fabs(p.
y) < 1e-3f);
1259 DOCTEST_CHECK(fabs(p.
z) < 1e-3f);
1260 DOCTEST_CHECK(dist == doctest::Approx(standoff).epsilon(0.001));
1266 auto check_fan = [&](
float bdiv,
float ediam) {
1271 std::vector<std::string> columnFormat;
1272 const float half = 4.f * float(M_PI) / 180.f;
1273 ScanMetadata scan(scan_origin, 9, aim_theta - half, aim_theta + half, 9, aim_phi - half, aim_phi + half, ediam, bdiv, 0.f, 0.f, columnFormat);
1274 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
1277 DOCTEST_REQUIRE(Nhits > 0);
1278 bool ranges_ok =
true;
1279 for (
uint h = 0; h < Nhits; h++) {
1283 double dist = cloud.
getHitData(h,
"distance");
1286 if (dist < standoff - 1e-2 || dist > standoff / cosf(sqrtf(2.f) * half) + 5e-2) {
1290 if (fabs(p.
x) > 1e-2f || !std::isfinite(p.
x) || !std::isfinite(p.
y) || !std::isfinite(p.
z)) {
1294 DOCTEST_CHECK(ranges_ok);
1296 check_fan(beam_div, 0.f);
1297 check_fan(0.f, exit_diam);
1298 check_fan(0.f, 0.f);
1303 auto run_seeded = [&](
uint seed, std::vector<vec3> &pts, std::vector<double> &ranges) {
1305 context.seedRandomGenerator(seed);
1309 std::vector<std::string> columnFormat;
1310 const float half = 4.f * float(M_PI) / 180.f;
1311 ScanMetadata scan(scan_origin, 12, aim_theta - half, aim_theta + half, 12, aim_phi - half, aim_phi + half, exit_diam, beam_div, 0.02f , 1e-4f , columnFormat);
1317 for (
uint h = 0; h < Nhits; h++) {
1319 ranges.push_back(cloud.
getHitData(h,
"distance"));
1322 std::vector<vec3> pts_a, pts_b;
1323 std::vector<double> ranges_a, ranges_b;
1324 run_seeded(12345u, pts_a, ranges_a);
1325 run_seeded(12345u, pts_b, ranges_b);
1326 DOCTEST_REQUIRE(pts_a.size() == pts_b.size());
1327 DOCTEST_REQUIRE(!pts_a.empty());
1328 bool identical =
true;
1329 for (
size_t i = 0; i < pts_a.size(); i++) {
1330 if ((pts_a[i] - pts_b[i]).magnitude() > 1e-6f || fabs(ranges_a[i] - ranges_b[i]) > 1e-6) {
1334 DOCTEST_CHECK(identical);
1338DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Pulse-Shape Deviation") {
1345 const vec3 scan_origin(-5.f, 0.f, 0.f);
1346 const float aim_phi = 0.5f * float(M_PI);
1347 const float aim_theta = 0.5f * float(M_PI);
1348 const float beam_div = 15.e-3f;
1349 const float exit_diam = 0.05f;
1350 const int rays_per_pulse = 400;
1351 const float pulse_width = 0.5f;
1356 std::vector<std::string> columnFormat;
1357 ScanMetadata scan(scan_origin, 1, aim_theta, aim_theta, 1, aim_phi, aim_phi, exit_diam, beam_div, 0.f , 0.f , columnFormat);
1361 DOCTEST_REQUIRE(Nhits >= 1);
1363 for (
uint h = 0; h < Nhits; h++) {
1368 DOCTEST_FAIL(
"no real return produced");
1373 double deviation_perpendicular;
1378 deviation_perpendicular = scan_single_wall(
context);
1380 DOCTEST_CHECK(deviation_perpendicular >= 0.0);
1381 DOCTEST_CHECK(deviation_perpendicular < 0.05);
1385 double deviation_tilted;
1389 const float c = cosf(75.f *
float(M_PI) / 180.f);
1390 const float s = sinf(75.f *
float(M_PI) / 180.f);
1392 auto rot = [&](
float y,
float z) {
return make_vec3(-y * s, y * c, z); };
1393 context.addTriangle(rot(-2.f, -2.f), rot(2.f, -2.f), rot(2.f, 2.f), RGB::green);
1394 context.addTriangle(rot(-2.f, -2.f), rot(2.f, 2.f), rot(-2.f, 2.f), RGB::green);
1395 deviation_tilted = scan_single_wall(
context);
1397 DOCTEST_CHECK(deviation_tilted > deviation_perpendicular);
1398 DOCTEST_CHECK(deviation_tilted > 0.05);
1401DOCTEST_TEST_CASE(
"LiDAR Default Detection Threshold") {
1419 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
1422 DOCTEST_CHECK(thr == doctest::Approx(0.05f));
1425 auto count_returns = [&](
float threshold) ->
uint {
1430 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
1437 uint n_zero = count_returns(0.f);
1438 uint n_default = count_returns(0.05f);
1439 uint n_high = count_returns(0.5f);
1440 DOCTEST_CHECK(n_zero == 3);
1441 DOCTEST_CHECK(n_default <= n_zero);
1442 DOCTEST_CHECK(n_high < n_zero);
1443 DOCTEST_CHECK(n_high >= 1);
1446DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Beam Chunking Equivalence") {
1455 vec3 scan_origin(-5.f, 0.f, 0.5f);
1458 float thetaMin = 0.5f * float(M_PI) - 6.f * float(M_PI) / 180.f;
1459 float thetaMax = 0.5f * float(M_PI) + 6.f * float(M_PI) / 180.f;
1460 float phiMin = 0.5f * float(M_PI) - 20.f * float(M_PI) / 180.f;
1461 float phiMax = 0.5f * float(M_PI) + 20.f * float(M_PI) / 180.f;
1462 std::vector<std::string> columnFormat;
1464 const int rays_per_pulse = 10;
1465 const float pulse_distance_threshold = 0.3f;
1470 ScanMetadata scan_single(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.f , 0.f , 0.f , 0.f , columnFormat);
1471 DOCTEST_CHECK_NOTHROW(cloud_single.
addScan(scan_single));
1474 DOCTEST_REQUIRE(Nhits_single > 0);
1481 ScanMetadata scan_chunked(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.f, 0.f, 0.f, 0.f, columnFormat);
1482 DOCTEST_CHECK_NOTHROW(cloud_chunked.
addScan(scan_chunked));
1487 DOCTEST_CHECK(Nhits_chunked == Nhits_single);
1488 if (Nhits_chunked == Nhits_single) {
1489 bool all_match =
true;
1490 for (
uint h = 0; h < Nhits_single; h++) {
1495 if (std::fabs(cloud_chunked.
getHitData(h,
"distance") - cloud_single.
getHitData(h,
"distance")) > 1e-6) {
1501 if ((p_single - p_chunked).magnitude() > 1e-5f) {
1506 DOCTEST_CHECK(all_match);
1515DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Beam Chunking Equivalence (GPU dispatch)") {
1527 const int gy = 20, gz = 20;
1528 const float ymin = -3.f, ymax = 3.f, zmin = -2.5f, zmax = 3.5f;
1529 const float dy = (ymax - ymin) /
float(gy);
1530 const float dz = (zmax - zmin) /
float(gz);
1531 for (
int iy = 0; iy < gy; iy++) {
1532 for (
int iz = 0; iz < gz; iz++) {
1533 float y = ymin + iy * dy;
1534 float z = zmin + iz * dz;
1540 vec3 scan_origin(-5.f, 0.f, 0.5f);
1543 float thetaMin = 0.5f * float(M_PI) - 6.f * float(M_PI) / 180.f;
1544 float thetaMax = 0.5f * float(M_PI) + 6.f * float(M_PI) / 180.f;
1545 float phiMin = 0.5f * float(M_PI) - 20.f * float(M_PI) / 180.f;
1546 float phiMax = 0.5f * float(M_PI) + 20.f * float(M_PI) / 180.f;
1547 std::vector<std::string> columnFormat;
1549 const int rays_per_pulse = 100;
1550 const float pulse_distance_threshold = 0.3f;
1555 ScanMetadata scan_single(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.f, 0.f, 0.f, 0.f, columnFormat);
1556 DOCTEST_CHECK_NOTHROW(cloud_single.
addScan(scan_single));
1559 DOCTEST_REQUIRE(Nhits_single > 0);
1566 ScanMetadata scan_chunked(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.f, 0.f, 0.f, 0.f, columnFormat);
1567 DOCTEST_CHECK_NOTHROW(cloud_chunked.
addScan(scan_chunked));
1571 DOCTEST_CHECK(Nhits_chunked == Nhits_single);
1572 if (Nhits_chunked == Nhits_single) {
1573 size_t mismatches = 0;
1574 for (
uint h = 0; h < Nhits_single; h++) {
1579 if (std::fabs(cloud_chunked.
getHitData(h,
"distance") - cloud_single.
getHitData(h,
"distance")) > 1e-3) {
1587 DOCTEST_CHECK(mismatches == 0);
1591DOCTEST_TEST_CASE(
"LiDAR GPU Availability Query") {
1597 bool lidar_available =
false;
1598 DOCTEST_CHECK_NOTHROW(lidar_available = lidarcloud.
isGPUAvailable());
1603 bool lidar_enabled =
false;
1605 DOCTEST_CHECK(lidar_enabled == lidar_available);
1608DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam XML Load Geometry") {
1613 std::string xml_path =
"plugins/lidar/xml/.tmp_spinning_multibeam_test.xml";
1615 std::ofstream f(xml_path);
1618 f <<
" <scanPattern> spinning_multibeam </scanPattern>\n";
1619 f <<
" <beamElevationAngles> -15 -10 -5 0 5 10 15 </beamElevationAngles>\n";
1620 f <<
" <azimuthStep> 0.5 </azimuthStep>\n";
1621 f <<
" <PRF> 100000 </PRF>\n";
1622 f <<
" <trajectory>\n";
1623 f <<
" <pose> 0.0 0 0 1 0 0 0 1 </pose>\n";
1624 f <<
" <pose> 0.0504 0 0 1 0 0 0 1 </pose>\n";
1625 f <<
" </trajectory>\n";
1632 DOCTEST_CHECK_NOTHROW(cloud.
loadXML(xml_path.c_str()));
1641 DOCTEST_REQUIRE(angles.size() == 7);
1642 DOCTEST_CHECK(angles[3] == doctest::Approx(0.5f *
float(M_PI)));
1644 std::remove(xml_path.c_str());
1647DOCTEST_TEST_CASE(
"LiDAR TreeQSM Loading Test") {
1653 std::vector<uint> tube_UUIDs;
1654 uint radial_subdivisions = 6;
1655 DOCTEST_CHECK_NOTHROW(tube_UUIDs = lidar.
loadTreeQSM(&context_treeqsm,
"plugins/lidar/data/cylinder_tree_QSM_test.txt", radial_subdivisions));
1658 DOCTEST_CHECK(tube_UUIDs.size() > 0);
1661 for (
uint UUID: tube_UUIDs) {
1663 DOCTEST_CHECK(context_treeqsm.
getObjectType(UUID) == helios::OBJECT_TYPE_TUBE);
1667 for (
uint UUID: tube_UUIDs) {
1672 context_treeqsm.
getObjectData(UUID,
"branch_order", branch_order);
1673 DOCTEST_CHECK(branch_order >= 0);
1676 context_treeqsm.
getObjectData(UUID,
"branch_id", branch_id);
1677 DOCTEST_CHECK(branch_id >= 0);
1682 std::vector<uint> tube_UUIDs2;
1683 DOCTEST_CHECK_NOTHROW(tube_UUIDs2 = lidar.
loadTreeQSM(&context_treeqsm2,
"plugins/lidar/data/cylinder_tree_QSM_test.txt", radial_subdivisions,
""));
1684 DOCTEST_CHECK(tube_UUIDs2.size() == tube_UUIDs.size());
1688 DOCTEST_CHECK_THROWS(lidar.
loadTreeQSM(&context_error,
"nonexistent_file.txt", radial_subdivisions));
1692 std::vector<uint> tube_UUIDs3;
1693 uint different_subdivisions = 8;
1694 DOCTEST_CHECK_NOTHROW(tube_UUIDs3 = lidar.
loadTreeQSM(&context_treeqsm3,
"plugins/lidar/data/cylinder_tree_QSM_test.txt", different_subdivisions));
1695 DOCTEST_CHECK(tube_UUIDs3.size() == tube_UUIDs.size());
1698 for (
uint UUID: tube_UUIDs) {
1700 DOCTEST_CHECK(primitive_UUIDs.size() > 0);
1703 for (
uint prim_UUID: primitive_UUIDs) {
1704 DOCTEST_CHECK(context_treeqsm.
getPrimitiveType(prim_UUID) == helios::PRIMITIVE_TYPE_TRIANGLE);
1709DOCTEST_TEST_CASE(
"LiDAR TreeQSM Colormap Loading Test") {
1715 std::vector<uint> tube_UUIDs;
1716 uint radial_subdivisions = 6;
1717 std::string colormap_name =
"hot";
1718 DOCTEST_CHECK_NOTHROW(tube_UUIDs = lidar.
loadTreeQSMColormap(&context_colormap,
"plugins/lidar/data/cylinder_tree_QSM_test.txt", radial_subdivisions, colormap_name));
1721 DOCTEST_CHECK(tube_UUIDs.size() > 0);
1724 for (
uint UUID: tube_UUIDs) {
1726 DOCTEST_CHECK(context_colormap.
getObjectType(UUID) == helios::OBJECT_TYPE_TUBE);
1730 for (
uint UUID: tube_UUIDs) {
1735 context_colormap.
getObjectData(UUID,
"branch_order", branch_order);
1736 DOCTEST_CHECK(branch_order >= 0);
1739 context_colormap.
getObjectData(UUID,
"branch_id", branch_id);
1740 DOCTEST_CHECK(branch_id >= 0);
1745 std::vector<uint> tube_UUIDs2;
1746 std::string colormap_name2 =
"cool";
1747 DOCTEST_CHECK_NOTHROW(tube_UUIDs2 = lidar.
loadTreeQSMColormap(&context_colormap2,
"plugins/lidar/data/cylinder_tree_QSM_test.txt", radial_subdivisions, colormap_name2));
1748 DOCTEST_CHECK(tube_UUIDs2.size() == tube_UUIDs.size());
1752 DOCTEST_CHECK_THROWS(lidar.
loadTreeQSMColormap(&context_error2,
"nonexistent_file.txt", radial_subdivisions, colormap_name));
1755 for (
uint UUID: tube_UUIDs) {
1757 DOCTEST_CHECK(primitive_UUIDs.size() > 0);
1760 for (
uint prim_UUID: primitive_UUIDs) {
1761 DOCTEST_CHECK(context_colormap.
getPrimitiveType(prim_UUID) == helios::PRIMITIVE_TYPE_TRIANGLE);
1767 std::string invalid_colormap =
"invalid_colormap_name";
1768 DOCTEST_CHECK_THROWS(lidar.
loadTreeQSMColormap(&context_colormap3,
"plugins/lidar/data/cylinder_tree_QSM_test.txt", radial_subdivisions, invalid_colormap));
1771DOCTEST_TEST_CASE(
"LiDAR Collision Detection Integration Test") {
1788 const int Npulse = 2;
1792 std::vector<helios::vec3> directions = {
1801 float hit_t[N * Npulse];
1802 float hit_fnorm[N * Npulse];
1803 int hit_ID[N * Npulse];
1806 for (
size_t i = 0; i < N * Npulse; i++) {
1813 std::vector<helios::vec3> ray_origins(N * Npulse, scan_origin);
1816 DOCTEST_CHECK_NOTHROW(lidar.
performUnifiedRayTracing(&test_context, N, Npulse, ray_origins.data(), directions.data(), hit_t, hit_fnorm, hit_ID));
1819 bool found_hit =
false;
1820 bool found_miss =
false;
1821 for (
size_t i = 0; i < N * Npulse; i++) {
1822 if (hit_t[i] < 1000.0f) {
1825 DOCTEST_CHECK(hit_t[i] > 0.0f);
1826 DOCTEST_CHECK(hit_t[i] < 100.0f);
1828 DOCTEST_CHECK(hit_ID[i] >= 0);
1830 DOCTEST_CHECK(std::isfinite(hit_fnorm[i]));
1833 DOCTEST_CHECK(hit_ID[i] == -1);
1838 DOCTEST_CHECK(found_hit);
1839 DOCTEST_CHECK(found_miss);
1842DOCTEST_TEST_CASE(
"LiDAR Data Format Conversion Test") {
1851 const int Npulse = 1;
1853 std::vector<helios::vec3> test_directions = {
1861 float hit_t[N * Npulse];
1862 float hit_fnorm[N * Npulse];
1863 int hit_ID[N * Npulse];
1869 std::vector<helios::vec3> ray_origins(N * Npulse, origin);
1872 DOCTEST_CHECK_NOTHROW(lidar.
performUnifiedRayTracing(&test_context, N, Npulse, ray_origins.data(), test_directions.data(), hit_t, hit_fnorm, hit_ID));
1875 for (
size_t i = 0; i < N * Npulse; i++) {
1876 DOCTEST_CHECK(std::isfinite(hit_t[i]));
1877 DOCTEST_CHECK(std::isfinite(hit_fnorm[i]));
1878 DOCTEST_CHECK(hit_ID[i] >= -1);
1882DOCTEST_TEST_CASE(
"LiDAR Edge Cases and Error Conditions Test") {
1903 DOCTEST_CHECK_NOTHROW(lidar.
performUnifiedRayTracing(&test_context, 1, 1, ray_origins, &direction, hit_t, hit_fnorm, hit_ID));
1906 DOCTEST_CHECK(std::isfinite(hit_t[0]));
1907 DOCTEST_CHECK(std::isfinite(hit_fnorm[0]));
1908 DOCTEST_CHECK(hit_ID[0] >= -1);
1911DOCTEST_TEST_CASE(
"LiDAR Collision Detection Memory Management Test") {
1925 float hit_t, hit_fnorm;
1931 DOCTEST_CHECK_NOTHROW(lidar.
performUnifiedRayTracing(&test_context, 1, 1, ray_origins, &direction, &hit_t, &hit_fnorm, &hit_ID));
1934 DOCTEST_CHECK(std::isfinite(hit_t));
1935 DOCTEST_CHECK(std::isfinite(hit_fnorm));
1936 DOCTEST_CHECK(hit_ID >= -1);
1939DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Integration Test") {
1945 DOCTEST_CHECK_NOTHROW(synthetic_scan_test.
loadXML(
"plugins/lidar/xml/synthetic_test.xml"));
1949 std::vector<uint> patch_UUIDs = scan_context.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
1950 DOCTEST_CHECK(patch_UUIDs.size() > 0);
1953 DOCTEST_CHECK_NOTHROW(synthetic_scan_test.
syntheticScan(&scan_context));
1957 DOCTEST_CHECK(hit_count > 0);
1960 for (
uint i = 0; i < std::min(hit_count, 10u); i++) {
1964 DOCTEST_CHECK(std::isfinite(hit_pos.
x));
1965 DOCTEST_CHECK(std::isfinite(hit_pos.
y));
1966 DOCTEST_CHECK(std::isfinite(hit_pos.
z));
1969 DOCTEST_CHECK(fabs(hit_pos.
x) < 100.0f);
1970 DOCTEST_CHECK(fabs(hit_pos.
y) < 100.0f);
1971 DOCTEST_CHECK(fabs(hit_pos.
z) < 100.0f);
1975 DOCTEST_CHECK(std::isfinite(ray_dir.
zenith));
1976 DOCTEST_CHECK(std::isfinite(ray_dir.
azimuth));
1977 DOCTEST_CHECK(std::isfinite(ray_dir.
radius));
1988 if (cell_count > 0) {
1990 DOCTEST_CHECK(std::isfinite(leaf_area_density));
1991 DOCTEST_CHECK(leaf_area_density >= 0.0f);
1995DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Range Noise Test") {
2011 vec3 scan_origin(0.0f, 0.0f, 5.0f);
2014 float thetaMin = 0.9f * float(M_PI);
2015 float thetaMax = float(M_PI);
2016 float phiMin = 0.0f;
2017 float phiMax = 2.0f * float(M_PI);
2018 float exitDiameter = 0.0f;
2019 float beamDivergence = 0.0f;
2020 std::vector<std::string> columnFormat;
2022 const float sigma_range = 0.05f;
2028 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2029 DOCTEST_CHECK_NOTHROW(lidar_clean.
addScan(scan));
2033 DOCTEST_CHECK(hit_count > 0);
2034 uint target_hits = 0;
2035 for (
uint i = 0; i < hit_count; i++) {
2038 DOCTEST_CHECK(fabs(p.
z - 0.0f) < 1e-4f);
2041 DOCTEST_CHECK(target_hits > 0);
2046 auto run_noisy = [&](
uint seed, std::vector<vec3> &points) {
2049 context.seedRandomGenerator(seed);
2050 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, sigma_range, 0.0f, columnFormat);
2055 for (
uint i = 0; i < hit_count; i++) {
2057 if (fabs(p.
z) < 1.0f) {
2058 points.push_back(p);
2063 std::vector<vec3> points;
2064 run_noisy(12345u, points);
2065 DOCTEST_CHECK(points.size() > 0);
2071 double z_mean = 0.0;
2072 for (
const vec3 &p: points) {
2075 z_mean /= double(points.size());
2078 for (
const vec3 &p: points) {
2079 z_var += (p.
z - z_mean) * (p.
z - z_mean);
2081 double z_std = std::sqrt(z_var /
double(points.size()));
2084 DOCTEST_CHECK(z_std > 0.5 * sigma_range);
2085 DOCTEST_CHECK(z_std < 2.0 * sigma_range);
2091 std::vector<vec3> clean_points;
2095 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2098 clean_points.clear();
2100 for (
uint i = 0; i < hc; i++) {
2102 if (fabs(p.
z) < 1.0f) {
2103 clean_points.push_back(p);
2108 DOCTEST_REQUIRE(clean_points.size() == points.size());
2109 double mean_abs_dz = 0.0;
2110 double mean_abs_dxy = 0.0;
2111 for (
size_t i = 0; i < points.size(); i++) {
2112 mean_abs_dz += fabs(
double(points[i].z - clean_points[i].z));
2113 double dx = double(points[i].x - clean_points[i].x);
2114 double dy = double(points[i].y - clean_points[i].y);
2115 mean_abs_dxy += std::sqrt(dx * dx + dy * dy);
2117 mean_abs_dz /= double(points.size());
2118 mean_abs_dxy /= double(points.size());
2123 DOCTEST_CHECK(mean_abs_dz > 0.5 * sigma_range);
2124 DOCTEST_CHECK(mean_abs_dxy < mean_abs_dz);
2125 DOCTEST_CHECK(mean_abs_dxy < 0.34 * mean_abs_dz);
2128 std::vector<vec3> points_repeat;
2129 run_noisy(12345u, points_repeat);
2130 DOCTEST_CHECK(points_repeat.size() == points.size());
2131 bool identical =
true;
2132 for (
size_t i = 0; i < points.size() && i < points_repeat.size(); i++) {
2133 if (fabs(points[i].z - points_repeat[i].z) > 1e-6f) {
2138 DOCTEST_CHECK(identical);
2141 std::vector<vec3> points_other;
2142 run_noisy(99999u, points_other);
2143 bool any_different =
false;
2144 for (
size_t i = 0; i < points.size() && i < points_other.size(); i++) {
2145 if (fabs(points[i].z - points_other[i].z) > 1e-6f) {
2146 any_different =
true;
2150 DOCTEST_CHECK(any_different);
2153DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Range-Normalized Intensity Test") {
2159 const float rho = 0.45f;
2162 const uint Ntheta = 30;
2163 const uint Nphi = 30;
2164 const float thetaMin = 0.97f * float(M_PI);
2165 const float thetaMax = float(M_PI);
2166 const float phiMin = 0.0f;
2167 const float phiMax = 2.0f * float(M_PI);
2168 const float exitDiameter = 0.0f;
2169 const float beamDivergence = 0.0f;
2172 std::vector<std::string> columnFormat = {
"reflectivity_lidar"};
2177 auto mean_target_intensity = [&](
float scan_height) ->
float {
2183 context.setPrimitiveData(target,
"reflectivity_lidar", rho);
2187 ScanMetadata scan(
make_vec3(0, 0, scan_height), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2192 DOCTEST_REQUIRE(hit_count > 0);
2196 for (
uint i = 0; i < hit_count; i++) {
2198 if (fabs(p.
z - 0.0f) > 1e-3f) {
2204 DOCTEST_REQUIRE(n > 0);
2205 return float(
sum /
double(n));
2208 const float intensity_near = mean_target_intensity(5.0f);
2209 const float intensity_far = mean_target_intensity(20.0f);
2212 DOCTEST_CHECK(intensity_near == doctest::Approx(intensity_far).epsilon(0.02));
2214 DOCTEST_CHECK(intensity_near == doctest::Approx(rho).epsilon(0.02));
2221DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Reflectance (dB) Test") {
2227 const float rho = 0.45f;
2228 const float expected_dB = 10.0f * log10f(rho);
2230 const uint Ntheta = 30;
2231 const uint Nphi = 30;
2232 const float thetaMin = 0.97f * float(M_PI);
2233 const float thetaMax = float(M_PI);
2234 const float phiMin = 0.0f;
2235 const float phiMax = 2.0f * float(M_PI);
2239 auto scan_reflectance = [&](
float scan_height,
bool request_reflectance,
uint &reflectance_field_count) ->
float {
2243 context.setPrimitiveData(target,
"reflectivity_lidar", rho);
2245 std::vector<std::string> columnFormat = {
"reflectivity_lidar"};
2246 if (request_reflectance) {
2247 columnFormat.push_back(
"reflectance");
2252 ScanMetadata scan(
make_vec3(0, 0, scan_height), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
2257 DOCTEST_REQUIRE(hit_count > 0);
2259 double sum_dB = 0.0;
2261 reflectance_field_count = 0;
2262 for (
uint i = 0; i < hit_count; i++) {
2264 if (fabs(p.
z - 0.0f) > 1e-3f) {
2270 reflectance_field_count++;
2272 double intensity = lidar.
getHitData(i,
"intensity");
2273 double reflectance = lidar.
getHitData(i,
"reflectance");
2274 DOCTEST_CHECK(reflectance == doctest::Approx(10.0 * log10(fabs(intensity))).epsilon(1e-4));
2275 sum_dB += reflectance;
2281 return float(sum_dB /
double(n));
2284 uint count_near = 0, count_far = 0, count_off = 0;
2285 const float dB_near = scan_reflectance(5.0f,
true, count_near);
2286 const float dB_far = scan_reflectance(20.0f,
true, count_far);
2289 DOCTEST_REQUIRE(count_near > 0);
2290 DOCTEST_CHECK(dB_near == doctest::Approx(expected_dB).epsilon(0.05));
2292 DOCTEST_CHECK(dB_near == doctest::Approx(dB_far).epsilon(0.05));
2295 scan_reflectance(5.0f,
false, count_off);
2296 DOCTEST_CHECK(count_off == 0);
2299DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Object Data Labeling Test") {
2305 const uint Ntheta = 30;
2306 const uint Nphi = 30;
2307 const float thetaMin = 0.97f * float(M_PI);
2308 const float thetaMax = float(M_PI);
2309 const float phiMin = 0.0f;
2310 const float phiMax = 2.0f * float(M_PI);
2312 const float object_value = 7.0f;
2318 context.setObjectData(objID,
"object_field", object_value);
2322 std::vector<std::string> columnFormat = {
"object_field"};
2323 ScanMetadata scan(
make_vec3(0, 0, 5), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
2328 DOCTEST_REQUIRE(hit_count > 0);
2331 for (
uint i = 0; i < hit_count; i++) {
2333 if (fabs(p.
z - 0.0f) > 1e-3f) {
2336 DOCTEST_CHECK(lidar.
getHitData(i,
"object_field") == doctest::Approx(object_value));
2339 DOCTEST_REQUIRE(n > 0);
2342DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Primitive-over-Object Data Precedence Test") {
2347 const uint Ntheta = 30;
2348 const uint Nphi = 30;
2349 const float thetaMin = 0.97f * float(M_PI);
2350 const float thetaMax = float(M_PI);
2351 const float phiMin = 0.0f;
2352 const float phiMax = 2.0f * float(M_PI);
2354 const float primitive_value = 3.0f;
2355 const float object_value = 9.0f;
2360 context.setObjectData(objID,
"shared_field", object_value);
2362 std::vector<uint> tile_UUIDs =
context.getObjectPrimitiveUUIDs(objID);
2363 context.setPrimitiveData(tile_UUIDs,
"shared_field", primitive_value);
2367 std::vector<std::string> columnFormat = {
"shared_field"};
2368 ScanMetadata scan(
make_vec3(0, 0, 5), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
2373 DOCTEST_REQUIRE(hit_count > 0);
2376 for (
uint i = 0; i < hit_count; i++) {
2378 if (fabs(p.
z - 0.0f) > 1e-3f) {
2381 DOCTEST_CHECK(lidar.
getHitData(i,
"shared_field") == doctest::Approx(primitive_value));
2384 DOCTEST_REQUIRE(n > 0);
2387DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Angular Jitter Test") {
2399 vec3 scan_origin(0.0f, 0.0f, 5.0f);
2402 float thetaMin = 0.9f * float(M_PI);
2403 float thetaMax = float(M_PI);
2404 float phiMin = 0.0f;
2405 float phiMax = 2.0f * float(M_PI);
2406 float exitDiameter = 0.0f;
2407 float beamDivergence = 0.0f;
2408 float rangeNoise = 0.0f;
2409 std::vector<std::string> columnFormat;
2411 const float sigma_angle = 0.01f;
2414 std::vector<vec3> clean_points;
2418 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2422 DOCTEST_CHECK(hc > 0);
2423 for (
uint i = 0; i < hc; i++) {
2425 if (fabs(p.
z) < 1.0f) {
2426 DOCTEST_CHECK(fabs(p.
z) < 1e-4f);
2427 clean_points.push_back(p);
2433 std::vector<vec3> points;
2437 context.seedRandomGenerator(2024u);
2438 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, rangeNoise, sigma_angle, columnFormat);
2442 for (
uint i = 0; i < hc; i++) {
2444 if (fabs(p.
z) < 1.0f) {
2445 points.push_back(p);
2450 DOCTEST_REQUIRE(points.size() == clean_points.size());
2454 double mean_abs_dz = 0.0;
2455 double mean_lateral = 0.0;
2456 for (
size_t i = 0; i < points.size(); i++) {
2457 mean_abs_dz += fabs(
double(points[i].z - clean_points[i].z));
2458 double dx = double(points[i].x - clean_points[i].x);
2459 double dy = double(points[i].y - clean_points[i].y);
2460 mean_lateral += std::sqrt(dx * dx + dy * dy);
2462 mean_abs_dz /= double(points.size());
2463 mean_lateral /= double(points.size());
2467 const double expected_lateral = 5.0 * double(sigma_angle);
2468 DOCTEST_CHECK(mean_lateral > 0.3 * expected_lateral);
2469 DOCTEST_CHECK(mean_lateral < 3.0 * expected_lateral);
2473 DOCTEST_CHECK(mean_abs_dz < 0.5 * mean_lateral);
2476 std::vector<vec3> repeat;
2480 context.seedRandomGenerator(2024u);
2481 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, rangeNoise, sigma_angle, columnFormat);
2485 for (
uint i = 0; i < hc; i++) {
2487 if (fabs(p.
z) < 1.0f) {
2488 repeat.push_back(p);
2492 DOCTEST_REQUIRE(repeat.size() == points.size());
2493 bool identical =
true;
2494 for (
size_t i = 0; i < points.size(); i++) {
2495 if ((points[i] - repeat[i]).magnitude() > 1e-6f) {
2500 DOCTEST_CHECK(identical);
2503DOCTEST_TEST_CASE(
"LiDAR Multi-Return Gaussian Weighting Test") {
2508 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
2511 float thetaMin = 0.0f;
2512 float thetaMax =
M_PI;
2513 float phiMin = 0.0f;
2514 float phiMax = 2.0f *
M_PI;
2515 float exitDiameter = 0.0f;
2516 float beamDivergence = 0.0f;
2517 std::vector<std::string> columnFormat;
2519 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2520 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
2523 vec3 grid_center(0.0f, 0.0f, 0.5f);
2524 vec3 grid_size(1.0f, 1.0f, 1.0f);
2526 DOCTEST_CHECK_NOTHROW(lidar.
addGrid(grid_center, grid_size, grid_divisions, 0));
2531 std::vector<uint> UUIDs =
context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
2533 float LAD_exact = 0.f;
2534 for (
uint UUID: UUIDs) {
2535 LAD_exact +=
context.getPrimitiveArea(UUID) / (gsize.
x * gsize.
y * gsize.
z);
2539 float Gtheta_exact_numerator = 0.f;
2540 float Gtheta_exact_denominator = 0.f;
2541 for (
uint UUID: UUIDs) {
2542 float area =
context.getPrimitiveArea(UUID);
2544 std::vector<vec3> vertices =
context.getPrimitiveVertices(UUID);
2545 vec3 raydir = vertices.front() - scan_origin;
2549 float normal_dot_ray = fabs(normal * raydir);
2550 Gtheta_exact_numerator += normal_dot_ray * area;
2551 Gtheta_exact_denominator += area;
2554 float Gtheta_exact = 0.f;
2555 if (Gtheta_exact_denominator > 0) {
2556 Gtheta_exact = Gtheta_exact_numerator / Gtheta_exact_denominator;
2576 DOCTEST_CHECK_NOTHROW(lidar2.
addScan(scan));
2577 DOCTEST_CHECK_NOTHROW(lidar2.
addGrid(grid_center, grid_size, grid_divisions, 0));
2588 float LAD_multi = LAD_grid_true;
2589 float Gtheta_multi = G_grid_true;
2592 DOCTEST_CHECK(fabs(LAD_grid_true - LAD_grid_false) < 0.01f);
2593 DOCTEST_CHECK(fabs(G_grid_true - G_grid_false) < 0.01f);
2598 DOCTEST_CHECK_NOTHROW(lidar_single.
addScan(scan));
2599 DOCTEST_CHECK_NOTHROW(lidar_single.
addGrid(grid_center, grid_size, grid_divisions, 0));
2611 DOCTEST_CHECK(LAD_multi > LAD_exact * 0.98f);
2612 DOCTEST_CHECK(LAD_multi < LAD_exact * 1.02f);
2615 DOCTEST_CHECK(Gtheta_multi == Gtheta_multi);
2616 DOCTEST_CHECK(fabs(Gtheta_multi - Gtheta_exact) / Gtheta_exact == doctest::Approx(0.0f).epsilon(0.05f));
2619DOCTEST_TEST_CASE(
"LiDAR Eight Voxel Multi-Return Gaussian Weighting Test") {
2630 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
2631 uint Ntheta = 10000;
2633 float thetaMin = 0.0f;
2634 float thetaMax =
M_PI;
2635 float phiMin = 0.0f;
2636 float phiMax = 2.0f *
M_PI;
2637 float exitDiameter = 0.0f;
2638 float beamDivergence = 0.0004f;
2639 std::vector<std::string> columnFormat;
2641 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2642 DOCTEST_CHECK_NOTHROW(synthetic_mr8.
addScan(scan));
2646 vec3 grid_center(0.0f, 0.0f, 0.5f);
2647 vec3 grid_size(1.0f, 1.0f, 1.0f);
2649 DOCTEST_CHECK_NOTHROW(synthetic_mr8.
addGrid(grid_center, grid_size, grid_divisions, 0));
2655 std::vector<uint> UUIDs = context_mr8.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
2658 std::vector<float> LAD_ex(8, 0);
2659 std::vector<float> Gtheta_ex(8, 0);
2660 std::vector<float> Gtheta_ex_numerator(8, 0);
2661 std::vector<float> Gtheta_ex_denominator(8, 0);
2663 for (
uint UUID: UUIDs) {
2676 int ID = k * 4 + j * 2 + i;
2679 LAD_ex.at(ID) += area / (gsize.
x * gsize.
y * gsize.
z);
2684 vec3 raydir = vertices.front() - scan_origin;
2688 float normal_dot_ray = fabs(normal * raydir);
2689 Gtheta_ex_numerator.at(ID) += normal_dot_ray * area;
2690 Gtheta_ex_denominator.at(ID) += area;
2695 for (
int i = 0; i < 8; i++) {
2696 if (Gtheta_ex_denominator[i] > 0) {
2697 Gtheta_ex[i] = Gtheta_ex_numerator[i] / Gtheta_ex_denominator[i];
2702 DOCTEST_CHECK_NOTHROW(synthetic_mr8.
syntheticScan(&context_mr8, 100, 0.1f,
true,
true));
2706 uint multi_return_count = 0;
2707 for (
uint i = 0; i < hits_grid_true; i++) {
2709 multi_return_count++;
2718 std::vector<float> LAD_grid_true(8);
2719 std::vector<float> G_grid_true(8);
2720 for (
int i = 0; i < 8; i++) {
2728 bool has_target_index =
true;
2729 bool has_target_count =
true;
2730 bool has_timestamp =
true;
2732 for (
uint i = 0; i < hits_grid_true; i++) {
2734 has_target_index =
false;
2736 has_target_count =
false;
2738 has_timestamp =
false;
2741 DOCTEST_CHECK(has_target_index);
2742 DOCTEST_CHECK(has_target_count);
2743 DOCTEST_CHECK(has_timestamp);
2746 std::map<int, int> timestamp_first_return_count;
2747 for (
uint i = 0; i < hits_grid_true; i++) {
2749 int tidx =
static_cast<int>(synthetic_mr8.
getHitData(i,
"target_index"));
2750 int tstamp =
static_cast<int>(synthetic_mr8.
getHitData(i,
"timestamp"));
2752 timestamp_first_return_count[tstamp]++;
2757 for (
const auto &pair: timestamp_first_return_count) {
2758 DOCTEST_CHECK(pair.second == 1);
2763 for (
int i = 0; i < 8; i++) {
2764 float LAD = LAD_grid_true[i];
2765 RMSE += powf(LAD - LAD_ex.at(i), 2) / LAD_ex.at(i) / 8.0f;
2776 DOCTEST_CHECK(RMSE == doctest::Approx(0.0f).epsilon(0.15f));
2779 for (
int i = 0; i < 8; i++) {
2780 float G = G_grid_true[i];
2781 float G_exact = Gtheta_ex[i];
2784 if (LAD_grid_true[i] > 0.001f && G_exact > 0) {
2785 DOCTEST_CHECK(G == G);
2786 DOCTEST_CHECK(G_exact == G_exact);
2787 DOCTEST_CHECK(fabs(G - G_exact) / G_exact == doctest::Approx(0.0f).epsilon(0.1f));
2792DOCTEST_TEST_CASE(
"LiDAR LAD DDA vs Brute-Force Equivalence") {
2800 auto run_pair = [](
int3 divisions,
const vec3 &grid_center,
const vec3 &grid_size,
bool multi_return) {
2802 auto build_and_invert = [&](
bool force_bruteforce, std::vector<float> &leaf_area, std::vector<float> >heta, std::vector<int> &beam_count, std::vector<float> &lad_var) {
2806 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
2807 ScanMetadata scan(scan_origin, 4000, 0.0f, M_PI, 6000, 0.0f, 2.0f * M_PI, 0.0f, multi_return ? 0.0004f : 0.0f, 0.0f, 0.0f, std::vector<std::string>{});
2809 lidar.
addGrid(grid_center, grid_size, divisions, 0);
2812 context.seedRandomGenerator(0);
2813 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
2826 leaf_area.resize(Ncells);
2827 gtheta.resize(Ncells);
2828 beam_count.resize(Ncells);
2829 lad_var.resize(Ncells);
2830 for (
uint c = 0; c < Ncells; c++) {
2838 std::vector<float> la_dda, g_dda, var_dda;
2839 std::vector<int> bc_dda;
2840 std::vector<float> la_bf, g_bf, var_bf;
2841 std::vector<int> bc_bf;
2842 build_and_invert(
false, la_dda, g_dda, bc_dda, var_dda);
2843 build_and_invert(
true, la_bf, g_bf, bc_bf, var_bf);
2845 DOCTEST_REQUIRE(la_dda.size() == la_bf.size());
2846 for (
size_t c = 0; c < la_dda.size(); c++) {
2847 DOCTEST_CHECK_MESSAGE(la_dda[c] == doctest::Approx(la_bf[c]).epsilon(1e-4f),
"leaf_area mismatch in cell " << c);
2848 DOCTEST_CHECK_MESSAGE(g_dda[c] == doctest::Approx(g_bf[c]).epsilon(1e-4f),
"Gtheta mismatch in cell " << c);
2849 DOCTEST_CHECK_MESSAGE(bc_dda[c] == bc_bf[c],
"beam_count mismatch in cell " << c <<
" (" << bc_dda[c] <<
" vs " << bc_bf[c] <<
")");
2851 if (var_dda[c] < 0.f || var_bf[c] < 0.f) {
2852 DOCTEST_CHECK_MESSAGE(var_dda[c] == var_bf[c],
"LAD_variance sign mismatch in cell " << c);
2854 DOCTEST_CHECK_MESSAGE(var_dda[c] == doctest::Approx(var_bf[c]).epsilon(1e-3f),
"LAD_variance mismatch in cell " << c);
2860 run_pair(
make_int3(2, 2, 2),
vec3(0.0f, 0.0f, 0.5f),
vec3(1.0f, 1.0f, 1.0f),
false);
2862 run_pair(
make_int3(2, 2, 2),
vec3(0.0f, 0.0f, 0.5f),
vec3(1.0f, 1.0f, 1.0f),
true);
2864 run_pair(
make_int3(3, 2, 4),
vec3(0.0f, 0.0f, 0.5f),
vec3(1.5f, 1.0f, 1.2f),
false);
2867DOCTEST_TEST_CASE(
"LiDAR LAD Non-Lattice Fallback") {
2872 auto build = [](
bool force_bruteforce, std::vector<float> &leaf_area) {
2876 ScanMetadata scan(
vec3(-5.0f, 0.0f, 0.5f), 3000, 0.0f, M_PI, 5000, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>{});
2885 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
2895 DOCTEST_CHECK(leaf_area[c] == leaf_area[c]);
2899 std::vector<float> la_auto, la_forced;
2900 build(
false, la_auto);
2901 build(
true, la_forced);
2902 DOCTEST_REQUIRE(la_auto.size() == la_forced.size());
2903 for (
size_t c = 0; c < la_auto.size(); c++) {
2904 DOCTEST_CHECK(la_auto[c] == doctest::Approx(la_forced[c]).epsilon(1e-4f));
2908DOCTEST_TEST_CASE(
"LiDAR Beam Perturbation - Single Return with Sphere") {
2914 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
2917 float thetaMin = 1.45f;
2918 float thetaMax = 1.69f;
2919 float phiMin = 0.0f;
2920 float phiMax = 6.28f;
2921 float exitDiameter = 0.0f;
2922 float beamDivergence = 0.0f;
2923 std::vector<std::string> columnFormat;
2925 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2926 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
2929 vec3 grid_center(0.0f, 0.0f, 0.5f);
2930 vec3 grid_size(1.0f, 1.0f, 1.0f);
2932 DOCTEST_CHECK_NOTHROW(lidar.
addGrid(grid_center, grid_size, grid_divisions, 0));
2934 uint expected_rays = Ntheta * Nphi;
2938 vec3 sphere_center(0, 0, 0.5);
2939 float sphere_radius = 50.0f;
2940 context.addSphereObject(6, sphere_center, sphere_radius);
2947 DOCTEST_CHECK(hit_count == expected_rays);
2950DOCTEST_TEST_CASE(
"LiDAR Beam Perturbation - Multi Return Zero Beam Width") {
2956 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
2959 float thetaMin = 0.0f;
2960 float thetaMax =
M_PI;
2961 float phiMin = 0.0f;
2962 float phiMax = 2.0f *
M_PI;
2963 float exitDiameter = 0.0f;
2964 float beamDivergence = 0.0f;
2965 std::vector<std::string> columnFormat;
2967 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
2968 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
2971 vec3 grid_center(0.0f, 0.0f, 0.5f);
2972 vec3 grid_size(1.0f, 1.0f, 1.0f);
2974 DOCTEST_CHECK_NOTHROW(lidar.
addGrid(grid_center, grid_size, grid_divisions, 0));
2976 uint expected_rays = Ntheta * Nphi;
2979 vec3 sphere_center(0, 0, 0.5);
2980 float sphere_radius = 50.0f;
2981 context.addSphereObject(6, sphere_center, sphere_radius);
2989 DOCTEST_CHECK(hit_count == expected_rays);
2992DOCTEST_TEST_CASE(
"LiDAR Beam Perturbation - Multi Return Miss Recording") {
2998 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
3001 float thetaMin = 0.0f;
3002 float thetaMax =
M_PI;
3003 float phiMin = 0.0f;
3004 float phiMax = 2.0f *
M_PI;
3005 float exitDiameter = 0.0f;
3006 float beamDivergence = 0.0f;
3007 std::vector<std::string> columnFormat;
3009 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
3010 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
3013 vec3 grid_center(0.0f, 0.0f, 0.5f);
3014 vec3 grid_size(1.0f, 1.0f, 1.0f);
3016 DOCTEST_CHECK_NOTHROW(lidar.
addGrid(grid_center, grid_size, grid_divisions, 0));
3018 uint expected_rays = Ntheta * Nphi;
3030 DOCTEST_CHECK(hit_count == expected_rays);
3033 bool all_misses =
true;
3034 for (
uint i = 0; i < hit_count; i++) {
3036 float dist = lidar.
getHitData(i,
"distance");
3037 if (dist < 1000.0f) {
3043 DOCTEST_CHECK(all_misses);
3046DOCTEST_TEST_CASE(
"LiDAR Multi-Return with Beam Spreading") {
3052 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
3053 uint Ntheta = 10000;
3055 float thetaMin = 0.0f;
3056 float thetaMax =
M_PI;
3057 float phiMin = 0.0f;
3058 float phiMax = 2.0f *
M_PI;
3059 float exitDiameter = 0.0f;
3060 float beamDivergence = 0.0004f;
3061 std::vector<std::string> columnFormat;
3063 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
3064 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
3068 vec3 grid_center(0.0f, 0.0f, 0.5f);
3069 vec3 grid_size(1.0f, 1.0f, 1.0f);
3071 DOCTEST_CHECK_NOTHROW(lidar.
addGrid(grid_center, grid_size, grid_divisions, 0));
3076 std::vector<uint> UUIDs =
context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
3078 float LAD_exact = 0.f;
3079 for (
uint UUID: UUIDs) {
3080 LAD_exact +=
context.getPrimitiveArea(UUID) / (gsize.
x * gsize.
y * gsize.
z);
3089 std::map<int, int> timestamp_first_return_count;
3090 for (
uint i = 0; i < hit_count; i++) {
3092 int tidx =
static_cast<int>(lidar.
getHitData(i,
"target_index"));
3093 int tstamp =
static_cast<int>(lidar.
getHitData(i,
"timestamp"));
3095 timestamp_first_return_count[tstamp]++;
3099 int timestamps_with_multi_first = 0;
3100 for (
auto &pair: timestamp_first_return_count) {
3101 if (pair.second > 1)
3102 timestamps_with_multi_first++;
3116 DOCTEST_CHECK(LAD > LAD_exact * 0.9f);
3117 DOCTEST_CHECK(LAD < LAD_exact * 1.1f);
3122 DOCTEST_CHECK(Gtheta > 0.42f);
3123 DOCTEST_CHECK(Gtheta < 0.58f);
3126DOCTEST_TEST_CASE(
"LiDAR Exit Diameter - Comparative Spread Test") {
3129 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
3131 vec3 scan_origin(0, 0, 5.0f);
3137 const uint scan_seed = 2024u;
3142 ScanMetadata scan_point(scan_origin, 100, 0, M_PI, 100, 0, 2 * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, {});
3143 lidar_point.
addScan(scan_point);
3144 context.seedRandomGenerator(scan_seed);
3150 ScanMetadata scan_exit(scan_origin, 100, 0, M_PI, 100, 0, 2 * M_PI, 0.1f, 0.0f, 0.0f, 0.0f, {});
3151 lidar_exit.
addScan(scan_exit);
3152 context.seedRandomGenerator(scan_seed);
3159 float x_min_pt = 1e6f, x_max_pt = -1e6f, y_min_pt = 1e6f, y_max_pt = -1e6f;
3162 x_min_pt = fmin(x_min_pt, pos.
x);
3163 x_max_pt = fmax(x_max_pt, pos.
x);
3164 y_min_pt = fmin(y_min_pt, pos.
y);
3165 y_max_pt = fmax(y_max_pt, pos.
y);
3169 float x_min_ex = 1e6f, x_max_ex = -1e6f, y_min_ex = 1e6f, y_max_ex = -1e6f;
3172 x_min_ex = fmin(x_min_ex, pos.
x);
3173 x_max_ex = fmax(x_max_ex, pos.
x);
3174 y_min_ex = fmin(y_min_ex, pos.
y);
3175 y_max_ex = fmax(y_max_ex, pos.
y);
3178 float extent_point = fmax(x_max_pt - x_min_pt, y_max_pt - y_min_pt);
3179 float extent_exit = fmax(x_max_ex - x_min_ex, y_max_ex - y_min_ex);
3183 DOCTEST_CHECK(extent_exit > extent_point * 1.01f);
3186DOCTEST_TEST_CASE(
"LiDAR Exit Diameter - Zero Backward Compatibility") {
3189 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
3194 ScanMetadata scan_zero(
vec3(0, 0, 5), 100, 0, M_PI, 100, 0, 2 * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, {});
3195 lidar_zero.
addScan(scan_zero);
3202 ScanMetadata scan_exit(
vec3(0, 0, 5), 100, 0, M_PI, 100, 0, 2 * M_PI, 0.01f, 0.0f, 0.0f, 0.0f, {});
3203 lidar_exit.
addScan(scan_exit);
3208DOCTEST_TEST_CASE(
"LiDAR Exit Diameter - Combined with Beam Divergence") {
3211 context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
3213 vec3 scan_origin(0, 0, 5.0f);
3220 const int spread_rays = 200;
3225 ScanMetadata scan_both(scan_origin, 100, 0, M_PI, 100, 0, 2 * M_PI, 0.1f, 0.01f, 0.0f, 0.0f, {});
3226 lidar_both.
addScan(scan_both);
3233 ScanMetadata scan_div(scan_origin, 100, 0, M_PI, 100, 0, 2 * M_PI, 0.0f, 0.01f, 0.0f, 0.0f, {});
3242 float x_min_b = 1e6f, x_max_b = -1e6f, y_min_b = 1e6f, y_max_b = -1e6f;
3245 x_min_b = fmin(x_min_b, pos.
x);
3246 x_max_b = fmax(x_max_b, pos.
x);
3247 y_min_b = fmin(y_min_b, pos.
y);
3248 y_max_b = fmax(y_max_b, pos.
y);
3252 float x_min_d = 1e6f, x_max_d = -1e6f, y_min_d = 1e6f, y_max_d = -1e6f;
3255 x_min_d = fmin(x_min_d, pos.
x);
3256 x_max_d = fmax(x_max_d, pos.
x);
3257 y_min_d = fmin(y_min_d, pos.
y);
3258 y_max_d = fmax(y_max_d, pos.
y);
3261 float spread_both = fmax(x_max_b - x_min_b, y_max_b - y_min_b);
3262 float spread_div = fmax(x_max_d - x_min_d, y_max_d - y_min_d);
3265 DOCTEST_CHECK(spread_both > spread_div * 1.01f);
3268DOCTEST_TEST_CASE(
"LiDAR Idealized Single-Ray Exact Intersection") {
3275 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3281 DOCTEST_CHECK(fabs(p.
x) < 1e-3f);
3282 DOCTEST_CHECK(fabs(p.
y) < 1e-3f);
3283 DOCTEST_CHECK(p.
z == doctest::Approx(2.0f).epsilon(1e-3f));
3284 DOCTEST_CHECK(lidar.
getHitData(0,
"target_count") == 1);
3287DOCTEST_TEST_CASE(
"LiDAR Single-Return Waveform - Ghost Point at Edge") {
3297 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3306 DOCTEST_CHECK(p.
z > 1.71f);
3307 DOCTEST_CHECK(p.
z < 1.99f);
3308 DOCTEST_CHECK(lidar.
getHitData(0,
"target_count") == 1);
3310 DOCTEST_CHECK(lidar.
getHitData(0,
"echo_width") > 0.5f);
3313DOCTEST_TEST_CASE(
"LiDAR Multi-Return Waveform - Resolved Returns") {
3322 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3331 DOCTEST_CHECK(znear == doctest::Approx(2.0f).epsilon(0.03f));
3332 DOCTEST_CHECK(zfar == doctest::Approx(0.5f).epsilon(0.03f));
3333 DOCTEST_CHECK(lidar.
getHitData(0,
"target_count") == 2);
3336DOCTEST_TEST_CASE(
"LiDAR Single-Return Waveform - Selection Policy") {
3346 ScanMetadata scan_m(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3352 int ifar = 1 - inear;
3353 float znear = lidar_multi.
getHitXYZ(inear).
z;
3355 DOCTEST_REQUIRE(fabs(lidar_multi.
getHitData(ifar,
"intensity")) > fabs(lidar_multi.
getHitData(inear,
"intensity")));
3360 ScanMetadata scan_s(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3366 DOCTEST_CHECK(lidar_str.
getHitXYZ(0).
z == doctest::Approx(zfar).epsilon(0.05f));
3371 ScanMetadata scan_f(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3377 DOCTEST_CHECK(lidar_first.
getHitXYZ(0).
z == doctest::Approx(znear).epsilon(0.05f));
3380DOCTEST_TEST_CASE(
"LiDAR Waveform - Detection Threshold") {
3390 ScanMetadata scan_b(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3395 float I0 = fabs(base.
getHitData(0,
"intensity"));
3396 float I1 = fabs(base.
getHitData(1,
"intensity"));
3397 float Iweak = fmin(I0, I1);
3398 float Istrong = fmax(I0, I1);
3399 DOCTEST_REQUIRE(Istrong > Iweak);
3404 ScanMetadata scan_f(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3410 DOCTEST_CHECK(fabs(filt.
getHitData(0,
"intensity")) == doctest::Approx(Istrong).epsilon(0.15f));
3413DOCTEST_TEST_CASE(
"LiDAR Return-Mode XML Load and Round-Trip") {
3415 std::string xml_path =
"plugins/lidar/xml/.tmp_returnmode_test.xml";
3417 std::ofstream f(xml_path);
3418 f <<
"<helios>\n <scan>\n";
3419 f <<
" <origin> 0 0 5 </origin>\n";
3420 f <<
" <size> 10 10 </size>\n";
3421 f <<
" <returnMode> single </returnMode>\n";
3422 f <<
" <singleReturnSelection> first </singleReturnSelection>\n";
3423 f <<
" <maxReturns> 3 </maxReturns>\n";
3424 f <<
" <pulseWidth> 0.4 </pulseWidth>\n";
3425 f <<
" <detectionThreshold> 0.15 </detectionThreshold>\n";
3426 f <<
" </scan>\n</helios>\n";
3431 DOCTEST_CHECK_NOTHROW(cloud.
loadXML(xml_path.c_str()));
3444 const std::string out_dir =
"lidar_returnmode_export_tmp";
3445 std::filesystem::remove_all(out_dir);
3446 const std::string xml_out = out_dir +
"/scans.xml";
3447 DOCTEST_CHECK_NOTHROW(cloud.
exportScans(xml_out.c_str()));
3451 DOCTEST_CHECK_NOTHROW(reloaded.
loadXML(xml_out.c_str()));
3459 std::filesystem::remove_all(out_dir);
3460 std::remove(xml_path.c_str());
3463DOCTEST_TEST_CASE(
"LiDAR Strongest-Plus-Last XML Round-Trip") {
3465 auto load_selection = [](
const std::string &spelling) {
3466 std::string xml_path =
"plugins/lidar/xml/.tmp_spl_test.xml";
3468 std::ofstream f(xml_path);
3469 f <<
"<helios>\n <scan>\n";
3470 f <<
" <origin> 0 0 5 </origin>\n";
3471 f <<
" <size> 10 10 </size>\n";
3472 f <<
" <returnMode> single </returnMode>\n";
3473 f <<
" <singleReturnSelection> " << spelling <<
" </singleReturnSelection>\n";
3474 f <<
" </scan>\n</helios>\n";
3478 cloud.
loadXML(xml_path.c_str());
3480 std::remove(xml_path.c_str());
3491 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3499 const std::string out_dir =
"lidar_spl_export_tmp";
3500 std::filesystem::remove_all(out_dir);
3501 const std::string xml_out = out_dir +
"/scans.xml";
3502 DOCTEST_CHECK_NOTHROW(cloud.
exportScans(xml_out.c_str()));
3506 DOCTEST_CHECK_NOTHROW(reloaded.
loadXML(xml_out.c_str()));
3510 std::filesystem::remove_all(out_dir);
3513DOCTEST_TEST_CASE(
"LiDAR N-Return - maxReturns Default and Validation") {
3520 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3533DOCTEST_TEST_CASE(
"LiDAR N-Return Waveform - Keeps Exactly N (stacked surfaces)") {
3545 buildScene(ctx_multi);
3548 ScanMetadata scan_m(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3557 buildScene(ctx_two);
3560 ScanMetadata scan_t(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3569 DOCTEST_CHECK(lidar_two.
getHitData(0,
"target_count") == 2);
3570 DOCTEST_CHECK(lidar_two.
getHitData(1,
"target_count") == 2);
3572 int idx0 = (lidar_two.
getHitData(0,
"target_index") == 0) ? 0 : 1;
3573 int idx1 = 1 - idx0;
3575 DOCTEST_CHECK(lidar_two.
getHitXYZ(idx0).
z == doctest::Approx(2.5f).epsilon(0.05f));
3576 DOCTEST_CHECK(lidar_two.
getHitXYZ(idx1).
z == doctest::Approx(1.5f).epsilon(0.05f));
3579DOCTEST_TEST_CASE(
"LiDAR N-Return Waveform - Selection Policy with N=2") {
3595 ScanMetadata scan_b(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3607 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3620 int i0 = (lidar.
getHitData(0,
"target_index") == 0) ? 0 : 1;
3628 auto z = keptZ(lidar);
3629 DOCTEST_CHECK(z.first > z.second);
3630 DOCTEST_CHECK(z.first == doctest::Approx(2.5f).epsilon(0.05f));
3631 DOCTEST_CHECK(z.second == doctest::Approx(1.5f).epsilon(0.05f));
3636 auto z = keptZ(lidar);
3637 DOCTEST_CHECK(z.first > z.second);
3638 DOCTEST_CHECK(z.first == doctest::Approx(1.5f).epsilon(0.05f));
3639 DOCTEST_CHECK(z.second == doctest::Approx(0.4f).epsilon(0.05f));
3645 auto z = keptZ(lidar);
3646 DOCTEST_CHECK(z.first > z.second);
3647 DOCTEST_CHECK(z.first == doctest::Approx(1.5f).epsilon(0.05f));
3648 DOCTEST_CHECK(z.second == doctest::Approx(0.4f).epsilon(0.05f));
3652DOCTEST_TEST_CASE(
"LiDAR Strongest-Plus-Last Dual Return - Three Surfaces") {
3661 const float znear = 2.5f, zmid = 1.5f, zfar = 0.4f;
3662 const std::vector<std::string> columnFormat = {
"reflectivity_lidar"};
3681 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, columnFormat);
3691 auto classify = [&](
LiDARcloud &c,
bool &hit_near,
bool &hit_mid,
bool &hit_far) {
3692 hit_near = hit_mid = hit_far =
false;
3695 if (fabs(z - znear) < 0.2f)
3697 else if (fabs(z - zmid) < 0.2f)
3699 else if (fabs(z - zfar) < 0.2f)
3707 double I_near = 0.0, I_mid = 0.0, I_far = 0.0;
3711 double I = fabs((
double)base.
getHitData(i,
"intensity"));
3712 if (fabs(z - znear) < 0.2f)
3714 else if (fabs(z - zmid) < 0.2f)
3716 else if (fabs(z - zfar) < 0.2f)
3719 DOCTEST_REQUIRE(I_near > 0.0);
3720 DOCTEST_REQUIRE(I_mid > 0.0);
3721 DOCTEST_REQUIRE(I_far > 0.0);
3722 DOCTEST_REQUIRE(I_near > I_mid);
3723 DOCTEST_REQUIRE(I_mid > I_far);
3729 classify(spl, sn, sm, sf);
3732 DOCTEST_CHECK_FALSE(sm);
3734 DOCTEST_CHECK(spl.
getHitData(0,
"target_count") == 2);
3735 DOCTEST_CHECK(spl.
getHitData(1,
"target_count") == 2);
3736 int i0 = (spl.
getHitData(0,
"target_index") == 0) ? 0 : 1;
3739 DOCTEST_CHECK(spl.
getHitXYZ(i0).
z == doctest::Approx(znear).epsilon(0.05f));
3740 DOCTEST_CHECK(spl.
getHitXYZ(i1).
z == doctest::Approx(zfar).epsilon(0.05f));
3746 classify(strong2, tn, tm, tf);
3749 DOCTEST_CHECK_FALSE(tf);
3754 classify(last2, ln, lm, lf);
3755 DOCTEST_CHECK_FALSE(ln);
3760DOCTEST_TEST_CASE(
"LiDAR Strongest-Plus-Last Dual Return - Dedup To Single") {
3764 const float znear = 2.0f, zfar = 0.5f;
3765 const std::vector<std::string> columnFormat = {
"reflectivity_lidar"};
3777 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, columnFormat);
3788 double I_near = 0.0, I_far = 0.0;
3792 double I = fabs((
double)base.
getHitData(i,
"intensity"));
3793 if (fabs(z - znear) < 0.3f)
3795 else if (fabs(z - zfar) < 0.3f)
3798 DOCTEST_REQUIRE(I_near > 0.0);
3799 DOCTEST_REQUIRE(I_far > 0.0);
3800 DOCTEST_REQUIRE(I_far > I_near);
3805 DOCTEST_CHECK(spl.
getHitXYZ(0).
z == doctest::Approx(zfar).epsilon(0.05f));
3806 DOCTEST_CHECK(spl.
getHitData(0,
"target_count") == 1);
3809DOCTEST_TEST_CASE(
"LiDAR Dual-Return Is N=2") {
3821 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3830 DOCTEST_CHECK(znear == doctest::Approx(2.0f).epsilon(0.03f));
3831 DOCTEST_CHECK(zfar == doctest::Approx(0.5f).epsilon(0.03f));
3834DOCTEST_TEST_CASE(
"LiDAR N-Return - Full Miss Preserved") {
3841 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3849 DOCTEST_CHECK(lidar.
getHitData(0,
"is_miss") == 1.0);
3850 DOCTEST_CHECK(lidar.
getHitData(0,
"target_index") == 99);
3853DOCTEST_TEST_CASE(
"LiDAR N-Return - Partial Miss Dropped") {
3863 buildScene(ctx_multi);
3866 ScanMetadata scan_m(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3870 lidar_multi.
syntheticScan(&ctx_multi, 400, 0.5f,
false,
true);
3872 int multi_real = 0, multi_miss = 0;
3874 if (lidar_multi.
getHitData(i,
"is_miss") == 1.0) {
3880 DOCTEST_REQUIRE(multi_real >= 1);
3881 DOCTEST_REQUIRE(multi_miss == 1);
3885 buildScene(ctx_lim);
3888 ScanMetadata scan_l(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3896 if (lidar_lim.
getHitData(i,
"is_miss") == 1.0) {
3901 DOCTEST_CHECK(lim_miss == 0);
3904DOCTEST_TEST_CASE(
"LiDAR N-Return - Backward Compatibility (single/multi unchanged)") {
3913 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3927 ScanMetadata scan(
make_vec3(0, 0, 5), 1, M_PI, M_PI, 1, 0, 0, 0.0f, 0.06f, 0.0f, 0.0f, {});
3935DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Grid Position Verification") {
3941 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
3948 std::vector<uint> sphere_uuids =
context.addSphere(10,
make_vec3(0, 0, 1.0), 0.3);
3955 DOCTEST_CHECK(hits_before_gapfill > 0);
3958 std::vector<vec3> filled_points = lidar.
gapfillMisses(0,
false,
true);
3962 DOCTEST_CHECK(hits_after_gapfill > hits_before_gapfill);
3963 DOCTEST_CHECK(filled_points.size() > 0);
3967 std::map<std::pair<int, int>,
bool> filled_grid_positions;
3973 float theta = raydir.
zenith;
3978 int row = round((theta - theta_range.
x) / (theta_range.
y - theta_range.
x) * (Ntheta - 1));
3979 int col = round((phi - phi_range.
x) / (phi_range.
y - phi_range.
x) * (Nphi - 1));
3981 filled_grid_positions[std::make_pair(row, col)] =
true;
3985 uint filled_cells = filled_grid_positions.size();
3988 uint flag_0_count = 0;
3989 uint flag_1_count = 0;
3990 uint flag_2_count = 0;
3991 uint flag_3_count = 0;
3995 int code = (int) lidar.
getHitData(r,
"gapfillMisses_code");
4007 DOCTEST_CHECK(flag_0_count == hits_before_gapfill);
4008 DOCTEST_CHECK((flag_1_count + flag_2_count + flag_3_count) == filled_points.size());
4012 DOCTEST_CHECK((flag_1_count + flag_2_count + flag_3_count) > 0);
4015DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Comparison with Record Misses") {
4023 DOCTEST_CHECK_NOTHROW(lidar1.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
4024 DOCTEST_CHECK_NOTHROW(lidar2.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
4027 std::vector<uint> sphere_uuids =
context.addSphere(10,
make_vec3(0, 0, 1.0), 0.3);
4037 std::vector<vec3> filled = lidar2.
gapfillMisses(0,
false,
false);
4043 DOCTEST_CHECK(hits_after_gapfill > hits_before_gapfill);
4044 DOCTEST_CHECK(filled.size() > 0);
4048 float hit_ratio = float(hits_after_gapfill) / float(hits_with_misses);
4049 DOCTEST_CHECK(hit_ratio > 0.7f);
4050 DOCTEST_CHECK(hit_ratio < 1.3f);
4054 uint real_hits_method1 = 0;
4055 uint real_hits_method2 = 0;
4060 real_hits_method1++;
4066 real_hits_method2++;
4070 DOCTEST_CHECK(real_hits_method1 == real_hits_method2);
4073DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Edge Cases") {
4078 bool caught_error =
false;
4081 }
catch (
const std::runtime_error &e) {
4082 caught_error =
true;
4083 std::string msg(e.what());
4084 DOCTEST_CHECK(msg.find(
"Invalid scanID") != std::string::npos);
4086 DOCTEST_CHECK(caught_error);
4089 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
4095 std::vector<vec3> filled;
4096 DOCTEST_CHECK_NOTHROW(filled = lidar.
gapfillMisses(0,
false,
false));
4099 if (hits_before == 0) {
4100 DOCTEST_CHECK(filled.empty());
4107 DOCTEST_CHECK_NOTHROW(lidar2.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
4108 std::vector<uint> sphere_uuids =
context.addSphere(10,
make_vec3(0, 0, 1.0), 0.3);
4112 std::vector<vec3> filled_all;
4116 DOCTEST_CHECK(hits_after_all >= hits_before_all);
4119DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Grid Only Mode") {
4125 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
4128 std::vector<uint> sphere_uuids =
context.addSphere(10,
make_vec3(0, 0, 1.0), 0.3);
4135 std::vector<vec3> filled_grid_only = lidar.
gapfillMisses(0,
true,
false);
4141 DOCTEST_CHECK_NOTHROW(lidar2.
loadXML(
"plugins/lidar/xml/synthetic_test_8.xml"));
4144 std::vector<vec3> filled_full = lidar2.
gapfillMisses(0,
false,
false);
4148 DOCTEST_CHECK(filled_grid_only.size() <= filled_full.size());
4151DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Multi-Return Data") {
4164 vec3 scan_origin(-3, 0, 1);
4167 float thetaMin =
M_PI / 3;
4168 float thetaMax = 2 *
M_PI / 3;
4171 float exitDiameter = 0.015;
4172 float beamDivergence = 0.002;
4174 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, std::vector<std::string>{
"x",
"y",
"z",
"timestamp"});
4182 bool has_multi_return =
false;
4183 for (
size_t r = 0; r < lidar.
getHitCount(); r++) {
4185 has_multi_return =
true;
4193 std::vector<vec3> filled = lidar.
gapfillMisses(scanID,
false,
true);
4197 DOCTEST_CHECK(hits_after > hits_before);
4198 DOCTEST_CHECK(filled.size() > 0);
4201 uint flag_0_count = 0;
4202 uint flag_other_count = 0;
4206 int code = (int) lidar.
getHitData(r,
"gapfillMisses_code");
4216 if (has_multi_return) {
4217 DOCTEST_CHECK(flag_0_count > 0);
4218 DOCTEST_CHECK(flag_other_count == filled.size());
4228DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Strict Accuracy Verification") {
4234 vec3 scan_origin(-4, 0, 1.5);
4237 float thetaMin =
M_PI / 3;
4238 float thetaMax = 2 *
M_PI / 3;
4251 ScanMetadata scan1(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0, 0, 0.0f, 0.0f, std::vector<std::string>{
"x",
"y",
"z",
"timestamp"});
4258 std::map<std::pair<int, int>,
vec3> ground_truth_positions;
4259 std::map<std::pair<int, int>,
bool> ground_truth_is_miss;
4265 const float dphi_grid = (phiMax - phiMin) /
float(Nphi - 1);
4266 const float dphi_per_row = dphi_grid / float(Ntheta);
4273 float theta = raydir.
zenith;
4275 int row = round((theta - thetaMin) / (thetaMax - thetaMin) * (Ntheta - 1));
4276 int col = round((phi -
float(row) * dphi_per_row - phiMin) / (phiMax - phiMin) * (Nphi - 1));
4278 ground_truth_positions[std::make_pair(row, col)] = pos;
4281 float dist = sqrt(pow(pos.
x - scan_origin.
x, 2) + pow(pos.
y - scan_origin.
y, 2) + pow(pos.
z - scan_origin.
z, 2));
4282 ground_truth_is_miss[std::make_pair(row, col)] = (dist > 1000);
4286 uint expected_grid_size = Ntheta * Nphi;
4288 DOCTEST_CHECK_MESSAGE(hits_ground_truth == expected_grid_size,
"record_misses should produce Ntheta×Nphi hits but got " << hits_ground_truth <<
" vs " << expected_grid_size);
4290 DOCTEST_CHECK(ground_truth_positions.size() == hits_ground_truth);
4296 ScanMetadata scan2(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0, 0, 0.0f, 0.0f, std::vector<std::string>{
"x",
"y",
"z",
"timestamp"});
4302 std::vector<vec3> filled = lidar2.
gapfillMisses(0,
false,
false);
4306 std::map<std::pair<int, int>,
vec3> gapfilled_positions;
4307 std::map<std::pair<int, int>,
uint> gapfilled_hit_count;
4313 float theta = raydir.
zenith;
4315 int row = round((theta - thetaMin) / (thetaMax - thetaMin) * (Ntheta - 1));
4316 int col = round((phi -
float(row) * dphi_per_row - phiMin) / (phiMax - phiMin) * (Nphi - 1));
4318 auto key = std::make_pair(row, col);
4319 gapfilled_positions[key] = pos;
4320 gapfilled_hit_count[key]++;
4324 uint gapfilled_duplicates = hits_after_gapfill - gapfilled_positions.size();
4325 DOCTEST_CHECK_MESSAGE(gapfilled_duplicates == 0,
"CRITICAL BUG: gapfillMisses created " << gapfilled_duplicates <<
" duplicate hits at positions that already had hits!");
4332 uint ground_truth_positions_found = 0;
4333 for (
const auto &kv: ground_truth_positions) {
4334 if (gapfilled_positions.find(kv.first) != gapfilled_positions.end()) {
4335 ground_truth_positions_found++;
4339 float ground_truth_recovery = float(ground_truth_positions_found) / float(ground_truth_positions.size());
4340 DOCTEST_CHECK_MESSAGE(ground_truth_recovery > 0.95f,
4341 "Gapfilling missed positions that record_misses found: only recovered " << ground_truth_positions_found <<
" of " << ground_truth_positions.size() <<
" (" << (ground_truth_recovery * 100) <<
"%)");
4344 uint invalid_positions = 0;
4345 for (
const auto &kv: gapfilled_positions) {
4346 int row = kv.first.first;
4347 int col = kv.first.second;
4349 if (row < 0 || row >= (
int) Ntheta || col < 0 || col >= (
int) Nphi) {
4350 invalid_positions++;
4353 DOCTEST_CHECK_MESSAGE(invalid_positions == 0,
"Gapfilling created " << invalid_positions <<
" points at invalid grid positions");
4356 uint position_mismatches = 0;
4357 float max_position_error = 0;
4359 for (
const auto &kv: ground_truth_positions) {
4360 auto key = kv.first;
4361 if (gapfilled_positions.find(key) != gapfilled_positions.end()) {
4362 vec3 pos_gt = kv.second;
4363 vec3 pos_gf = gapfilled_positions[key];
4365 bool is_miss = ground_truth_is_miss[key];
4367 float dist = sqrt(pow(pos_gt.
x - pos_gf.
x, 2) + pow(pos_gt.
y - pos_gf.
y, 2) + pow(pos_gt.
z - pos_gf.
z, 2));
4369 if (dist > max_position_error)
4370 max_position_error = dist;
4374 if (!is_miss && dist > 0.01f) {
4375 position_mismatches++;
4376 }
else if (is_miss) {
4378 vec3 dir_gt = pos_gt - scan_origin;
4379 vec3 dir_gf = pos_gf - scan_origin;
4380 float mag_gt = sqrt(dir_gt.
x * dir_gt.
x + dir_gt.
y * dir_gt.
y + dir_gt.
z * dir_gt.
z);
4381 float mag_gf = sqrt(dir_gf.
x * dir_gf.
x + dir_gf.
y * dir_gf.
y + dir_gf.
z * dir_gf.
z);
4383 float dot = (dir_gt.
x * dir_gf.
x + dir_gt.
y * dir_gf.
y + dir_gt.
z * dir_gf.
z) / (mag_gt * mag_gf);
4384 if (dot < 0.9998f) {
4385 position_mismatches++;
4391 float position_match_rate = float(ground_truth_positions_found - position_mismatches) / float(ground_truth_positions_found);
4392 DOCTEST_CHECK_MESSAGE(position_match_rate > 0.95f,
"Position accuracy too low: " << position_mismatches <<
" mismatches out of " << ground_truth_positions_found <<
" (" << (position_match_rate * 100) <<
"% correct)");
4395 DOCTEST_CHECK_MESSAGE(hits_after_gapfill == hits_ground_truth,
4396 "Gapfilling should exactly match record_misses for ideal scan: " << hits_after_gapfill <<
" vs " << hits_ground_truth <<
" (difference: " << (
int) hits_after_gapfill - (
int) hits_ground_truth <<
")");
4399DOCTEST_TEST_CASE(
"LiDAR exportScans round-trip") {
4401 const std::string out_dir =
"lidar_export_scans_test_tmp";
4402 std::filesystem::remove_all(out_dir);
4408 std::vector<std::string> columnFormat0 = {
"x",
"y",
"z",
"zenith",
"azimuth"};
4409 ScanMetadata scan0(
vec3(-3.0f, 0.0f, 0.5f), 80, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 160, 0.0f, 2.0f *
float(M_PI), 0.01f, 0.001f, 0.004f, 0.0005f, columnFormat0);
4410 DOCTEST_CHECK_NOTHROW(original.
addScan(scan0));
4413 std::vector<std::string> columnFormat1;
4414 ScanMetadata scan1(
vec3(0.0f, -3.0f, 0.5f), 80, 0.0f,
float(M_PI), 160, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat1);
4415 DOCTEST_CHECK_NOTHROW(original.
addScan(scan1));
4420 std::vector<uint> UUIDs = scene_ctx.
loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
4421 DOCTEST_CHECK(!UUIDs.empty());
4425 DOCTEST_CHECK(original_hits > 0);
4427 const std::string xml_out = out_dir +
"/scans.xml";
4428 DOCTEST_CHECK_NOTHROW(original.
exportScans(xml_out.c_str()));
4430 DOCTEST_CHECK(std::filesystem::exists(xml_out));
4431 DOCTEST_CHECK(std::filesystem::exists(out_dir +
"/scans_0.xyz"));
4432 DOCTEST_CHECK(std::filesystem::exists(out_dir +
"/scans_1.xyz"));
4436 std::ifstream xyz0(out_dir +
"/scans_0.xyz");
4437 std::string header_line;
4438 std::getline(xyz0, header_line);
4439 DOCTEST_CHECK(!header_line.empty());
4440 DOCTEST_CHECK(header_line.front() ==
'#');
4441 for (
const std::string &col: columnFormat0) {
4442 DOCTEST_CHECK(header_line.find(col) != std::string::npos);
4449 DOCTEST_CHECK_NOTHROW(reloaded.
loadXML(xml_out.c_str()));
4452 DOCTEST_CHECK(reloaded.
getHitCount() == original_hits);
4457 DOCTEST_CHECK(o_new.
x == doctest::Approx(o_orig.
x));
4458 DOCTEST_CHECK(o_new.
y == doctest::Approx(o_orig.
y));
4459 DOCTEST_CHECK(o_new.
z == doctest::Approx(o_orig.
z));
4466 DOCTEST_CHECK(t_new.
x == doctest::Approx(t_orig.
x).epsilon(1e-4));
4467 DOCTEST_CHECK(t_new.
y == doctest::Approx(t_orig.
y).epsilon(1e-4));
4471 DOCTEST_CHECK(p_new.
x == doctest::Approx(p_orig.
x).epsilon(1e-4));
4472 DOCTEST_CHECK(p_new.
y == doctest::Approx(p_orig.
y).epsilon(1e-4));
4480 if (f_orig.empty()) {
4481 f_orig = {
"x",
"y",
"z"};
4489 DOCTEST_CHECK_THROWS(empty_cloud.
exportScans((out_dir +
"/empty.xml").c_str()));
4491 std::filesystem::remove_all(out_dir);
4494DOCTEST_TEST_CASE(
"LiDAR exportPointCloud header") {
4496 const std::string out_dir =
"lidar_export_header_test_tmp";
4497 std::filesystem::remove_all(out_dir);
4501 const std::vector<std::string> columnFormat = {
"x",
"y",
"z",
"intensity",
"my_field"};
4505 ScanMetadata scan(
vec3(0.0f, 0.0f, 0.0f), 4, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 4, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4506 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
4508 const uint Nhits = 5;
4509 for (
uint i = 0; i < Nhits; i++) {
4510 std::map<std::string, double> data;
4511 data[
"intensity"] = 0.5 + 0.1 * i;
4512 data[
"my_field"] = 100.0 + i;
4513 SphericalCoord dir(1.f, 0.5f *
float(M_PI) - 0.5f *
float(M_PI), 0.1f * i);
4514 DOCTEST_CHECK_NOTHROW(cloud.
addHitPoint(0,
vec3(
float(i), 0.2f * i, 1.0f), dir, data));
4519 const std::string with_header = out_dir +
"/cloud_header.xyz";
4521 DOCTEST_CHECK(std::filesystem::exists(with_header));
4523 std::ifstream f(with_header);
4524 std::string first_line;
4525 std::getline(f, first_line);
4526 DOCTEST_CHECK(first_line ==
"# x y z intensity my_field");
4528 std::string data_line;
4529 std::getline(f, data_line);
4530 std::istringstream iss(data_line);
4533 while (iss >> tok) {
4536 DOCTEST_CHECK(ntok == columnFormat.size());
4540 const std::string no_header = out_dir +
"/cloud_nohdr.xyz";
4541 DOCTEST_CHECK_NOTHROW(cloud.
exportPointCloud(no_header.c_str(), 0u,
false));
4542 DOCTEST_CHECK(std::filesystem::exists(no_header));
4544 std::ifstream f(no_header);
4545 std::string first_line;
4546 std::getline(f, first_line);
4547 DOCTEST_CHECK(!first_line.empty());
4548 DOCTEST_CHECK(first_line.front() !=
'#');
4555 ScanMetadata reload_scan(
vec3(0.0f, 0.0f, 0.0f), 4, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 4, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4556 DOCTEST_CHECK_NOTHROW(reloaded.
addScan(reload_scan));
4557 DOCTEST_CHECK_NOTHROW(reloaded.
loadASCIIFile(0, with_header));
4561 DOCTEST_CHECK_NOTHROW(mf = reloaded.
getHitData(0,
"my_field"));
4562 DOCTEST_CHECK(mf == doctest::Approx(100.0));
4564 std::filesystem::remove_all(out_dir);
4567DOCTEST_TEST_CASE(
"LiDAR Columnar Hit Data - Bulk Getter Matches Per-Hit") {
4571 const std::vector<std::string> columnFormat = {
"x",
"y",
"z",
"intensity",
"sparse_field"};
4575 ScanMetadata scan(
vec3(0.0f, 0.0f, 0.0f), 4, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 4, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4576 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
4578 const uint Nhits = 7;
4579 for (
uint i = 0; i < Nhits; i++) {
4580 std::map<std::string, double> data;
4581 data[
"intensity"] = 0.5 + 0.1 * i;
4583 data[
"sparse_field"] = 1000.0 + i;
4586 DOCTEST_CHECK_NOTHROW(cloud.
addHitPoint(0,
vec3(
float(i), 0.2f * i, 1.0f), dir, data));
4590 const double absent = -9999.0;
4593 std::vector<double> intensity_bulk;
4594 DOCTEST_CHECK_NOTHROW(cloud.
getHitDataColumn(
"intensity", intensity_bulk, absent));
4595 DOCTEST_CHECK(intensity_bulk.size() == Nhits);
4596 for (
uint i = 0; i < Nhits; i++) {
4598 DOCTEST_CHECK(intensity_bulk[i] == doctest::Approx(expected));
4602 std::vector<double> sparse_bulk;
4603 DOCTEST_CHECK_NOTHROW(cloud.
getHitDataColumn(
"sparse_field", sparse_bulk, absent));
4604 DOCTEST_CHECK(sparse_bulk.size() == Nhits);
4605 for (
uint i = 0; i < Nhits; i++) {
4607 DOCTEST_CHECK(sparse_bulk[i] == doctest::Approx(expected));
4609 DOCTEST_CHECK(sparse_bulk[i] == doctest::Approx(1000.0 + i));
4611 DOCTEST_CHECK(sparse_bulk[i] == doctest::Approx(absent));
4617 std::vector<double> missing_bulk;
4618 DOCTEST_CHECK_NOTHROW(cloud.
getHitDataColumn(
"never_set", missing_bulk, absent));
4619 DOCTEST_CHECK(missing_bulk.size() == Nhits);
4620 for (
uint i = 0; i < Nhits; i++) {
4621 DOCTEST_CHECK(missing_bulk[i] == doctest::Approx(absent));
4626DOCTEST_TEST_CASE(
"LiDAR Columnar Hit Data - Delete Lockstep") {
4631 const std::vector<std::string> columnFormat = {
"x",
"y",
"z",
"id"};
4635 ScanMetadata scan(
vec3(0.0f, 0.0f, 0.0f), 4, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 4, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4636 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
4638 const uint Nhits = 10;
4639 for (
uint i = 0; i < Nhits; i++) {
4640 std::map<std::string, double> data;
4641 data[
"id"] = double(i);
4644 DOCTEST_CHECK_NOTHROW(cloud.
addHitPoint(0,
vec3(
double(i), 0.f, 1.f), dir, data));
4657 DOCTEST_CHECK(
id == doctest::Approx(x));
4659 DOCTEST_CHECK(
id != doctest::Approx(2.0));
4660 DOCTEST_CHECK(
id != doctest::Approx(5.0));
4661 DOCTEST_CHECK(
id != doctest::Approx(8.0));
4665DOCTEST_TEST_CASE(
"LiDAR Columnar Hit Data - Mid-Cloud New Label Back-Fill") {
4671 const std::vector<std::string> columnFormat = {
"x",
"y",
"z"};
4672 ScanMetadata scan(
vec3(0.0f, 0.0f, 0.0f), 4, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 4, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4673 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
4675 const uint Nhits = 6;
4676 for (
uint i = 0; i < Nhits; i++) {
4677 std::map<std::string, double> data;
4679 DOCTEST_CHECK_NOTHROW(cloud.
addHitPoint(0,
vec3(
double(i), 0.f, 1.f), dir, data));
4684 for (
uint i = 0; i < Nhits; i++) {
4689 DOCTEST_CHECK_NOTHROW(cloud.
setHitData(4,
"late_label", 42.0));
4692 for (
uint i = 0; i < Nhits; i++) {
4695 DOCTEST_CHECK(cloud.
getHitData(i,
"late_label") == doctest::Approx(42.0));
4704 DOCTEST_CHECK_THROWS(cloud.
getHitData(0,
"late_label"));
4708DOCTEST_TEST_CASE(
"LiDAR Columnar Hit Data - Origin Survives coordinateShift") {
4716 const std::vector<std::string> columnFormat = {
"x",
"y",
"z"};
4717 ScanMetadata scan(
vec3(0.0f, 0.0f, 0.0f), 4, 0.25f *
float(M_PI), 0.75f *
float(M_PI), 4, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4718 DOCTEST_CHECK_NOTHROW(cloud.
addScan(scan));
4721 std::map<std::string, double> moving;
4722 moving[
"origin_x"] = 1.0;
4723 moving[
"origin_y"] = 2.0;
4724 moving[
"origin_z"] = 3.0;
4728 std::map<std::string, double> stat;
4735 DOCTEST_CHECK(cloud.
getHitData(0,
"origin_x") == doctest::Approx(1.0 + 100.0));
4736 DOCTEST_CHECK(cloud.
getHitData(0,
"origin_y") == doctest::Approx(2.0 + 200.0));
4737 DOCTEST_CHECK(cloud.
getHitData(0,
"origin_z") == doctest::Approx(3.0 + 300.0));
4738 DOCTEST_CHECK(cloud.
getHitXYZ(0).
x == doctest::Approx(5.f + 100.f));
4742 DOCTEST_CHECK(cloud.
getHitXYZ(1).
x == doctest::Approx(10.f + 100.f));
4745DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Texture Color Sampling") {
4750 const char *texture =
"plugins/visualizer/textures/GrapeLeaf.png";
4755 DOCTEST_CHECK(
context.primitiveTextureHasTransparencyChannel(patch));
4760 std::vector<std::string> columnFormat = {
"x",
"y",
"z"};
4761 ScanMetadata scan(
make_vec3(0, 0, 5), 60, 0.0f,
float(M_PI), 60, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
4762 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
4768 DOCTEST_CHECK(hit_count > 0);
4770 uint black_hits = 0;
4771 for (
uint i = 0; i < hit_count; i++) {
4774 DOCTEST_CHECK(c.
r >= 0.f);
4775 DOCTEST_CHECK(c.
r <= 1.f);
4776 DOCTEST_CHECK(c.
g >= 0.f);
4777 DOCTEST_CHECK(c.
g <= 1.f);
4778 DOCTEST_CHECK(c.
b >= 0.f);
4779 DOCTEST_CHECK(c.
b <= 1.f);
4780 if (c.
r == 0.f && c.
g == 0.f && c.
b == 0.f) {
4786 DOCTEST_CHECK(black_hits == 0);
4789DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - ASCII Multi-Return Cloud") {
4808 namespace fs = std::filesystem;
4815 fs::path data_src =
"plugins/lidar/data/leafcube_multi.xyz";
4816 DOCTEST_REQUIRE(fs::exists(data_src));
4821 fs::path long_dir = fs::absolute(
"lidar_longpath_overflow_regression_dir/"
4822 "subdirectory_padding_to_exceed_one_hundred_byte_buffer/"
4823 "additional_nesting_for_safety_margin");
4824 fs::create_directories(long_dir);
4825 fs::path data_abs = long_dir /
"leafcube_multi.xyz";
4826 fs::copy_file(data_src, data_abs, fs::copy_options::overwrite_existing);
4828 std::string data_path_str = data_abs.string();
4829 DOCTEST_REQUIRE_MESSAGE(data_path_str.size() > 100,
"Fixture path must exceed the old 100-byte buffer to exercise the loadXML "
4830 "overflow; got length "
4831 << data_path_str.size() <<
" (" << data_path_str <<
")");
4833 const char *test_xml =
"lidar_leafcube_multi_longpath_test.xml";
4835 std::ofstream xml(test_xml);
4836 xml <<
"<?xml version=\"1.0\"?>\n<helios>\n<scan>\n"
4837 <<
" <filename> " << data_path_str <<
" </filename>\n"
4838 <<
" <ASCII_format> x y z timestamp target_index target_count </ASCII_format>\n"
4839 <<
" <origin> -5.000000 0.000000 0.500000 </origin>\n"
4840 <<
" <size> 800 1600 </size>\n"
4842 <<
"<grid>\n <center> 0 0 0.5 </center>\n <size> 1 1 1 </size>\n"
4843 <<
" <Nx> 1 </Nx>\n <Ny> 1 </Ny>\n <Nz> 1 </Nz>\n</grid>\n"
4847 const int N_repeats = 25;
4849 for (
int rep = 0; rep < N_repeats; rep++) {
4855 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(test_xml));
4863 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(test_xml));
4872 DOCTEST_CHECK(LAD == LAD);
4877 DOCTEST_CHECK(LAD > 1.0f);
4878 DOCTEST_CHECK(LAD < 3.0f);
4880 std::remove(test_xml);
4882 fs::remove_all(
"lidar_longpath_overflow_regression_dir", ec);
4885DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Scanner Tilt Test") {
4903 const vec3 scan_origin(0.0f, 0.0f, 0.0f);
4904 const uint Ntheta = 30;
4905 const uint Nphi = 30;
4906 const float phiMin = 0.0f;
4907 const float phiMax = 2.0f * float(
M_PI);
4908 const float exitDiameter = 0.0f;
4909 const float beamDivergence = 0.0f;
4910 std::vector<std::string> columnFormat;
4916 const float cone_thetaMin = 0.80f * float(
M_PI);
4917 const float cone_thetaMax = float(
M_PI);
4918 auto run_scan = [&](
float roll,
float pitch, std::vector<vec3> &dirs) {
4921 ScanMetadata scan(scan_origin, Ntheta, cone_thetaMin, cone_thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat, roll, pitch);
4928 for (
uint i = 0; i < hc; i++) {
4938 std::vector<vec3> dirs_default, dirs_zero;
4939 run_scan(0.0f, 0.0f, dirs_zero);
4943 ScanMetadata scan(scan_origin, Ntheta, cone_thetaMin, cone_thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
4948 for (
uint i = 0; i < hc; i++) {
4951 dirs_default.push_back(d);
4953 DOCTEST_REQUIRE(dirs_zero.size() == dirs_default.size());
4954 DOCTEST_CHECK(dirs_zero.size() > 0);
4955 for (
size_t i = 0; i < dirs_zero.size(); i++) {
4956 DOCTEST_CHECK(dirs_zero[i].x == doctest::Approx(dirs_default[i].x).epsilon(1e-5));
4957 DOCTEST_CHECK(dirs_zero[i].y == doctest::Approx(dirs_default[i].y).epsilon(1e-5));
4958 DOCTEST_CHECK(dirs_zero[i].z == doctest::Approx(dirs_default[i].z).epsilon(1e-5));
4967 const float roll = 7.0f * float(
M_PI) / 180.0f;
4968 const float pitch = 11.0f * float(
M_PI) / 180.0f;
4970 const vec3 lateral_axis =
make_vec3(cosf(phiMin), -sinf(phiMin), 0.f);
4971 const vec3 forward_axis =
make_vec3(sinf(phiMin), cosf(phiMin), 0.f);
4972 std::vector<vec3> level_dirs, tilted_dirs;
4973 run_scan(0.0f, 0.0f, level_dirs);
4974 run_scan(roll, pitch, tilted_dirs);
4975 DOCTEST_REQUIRE(level_dirs.size() == tilted_dirs.size());
4976 DOCTEST_CHECK(level_dirs.size() > 0);
4978 for (
size_t i = 0; i < level_dirs.size(); i++) {
4981 DOCTEST_CHECK(tilted_dirs[i].x == doctest::Approx(expected.
x).epsilon(1e-3));
4982 DOCTEST_CHECK(tilted_dirs[i].y == doctest::Approx(expected.
y).epsilon(1e-3));
4983 DOCTEST_CHECK(tilted_dirs[i].z == doctest::Approx(expected.
z).epsilon(1e-3));
4998 const uint Nt = 8, Np = 8;
4999 const float tmin = 0.97f * float(
M_PI);
5000 const float tmax = float(
M_PI);
5001 const float tilt = 10.0f * float(
M_PI) / 180.0f;
5003 auto mean_floor_hit = [&](
float roll,
float pitch) ->
vec3 {
5006 ScanMetadata scan(scan_origin, Nt, tmin, tmax, Np, 0.0f, 2.0f *
float(
M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat, roll, pitch);
5012 for (
uint i = 0; i < hc; i++) {
5019 DOCTEST_REQUIRE(n > 0);
5020 return mean / float(n);
5023 vec3 pitch_hit = mean_floor_hit(0.0f, tilt);
5024 DOCTEST_CHECK(pitch_hit.
x < -0.5f);
5025 DOCTEST_CHECK(fabs(pitch_hit.
y) < 0.2f);
5027 vec3 roll_hit = mean_floor_hit(tilt, 0.0f);
5028 DOCTEST_CHECK(roll_hit.
y > 0.5f);
5029 DOCTEST_CHECK(fabs(roll_hit.
x) < 0.2f);
5034 const char *tilt_xml =
"lidar_scantilt_xml_test.xml";
5035 std::ofstream ofs(tilt_xml);
5038 <<
" <origin> 0 0 0 </origin>\n"
5039 <<
" <size> 10 10 </size>\n"
5040 <<
" <scanTilt> 5 3 </scanTilt>\n"
5043 <<
" <origin> 0 0 0 </origin>\n"
5044 <<
" <size> 10 10 </size>\n"
5051 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(tilt_xml));
5053 DOCTEST_CHECK(lidar.
getScanTiltRoll(0) == doctest::Approx(5.0f *
float(
M_PI) / 180.0f).epsilon(1e-5));
5054 DOCTEST_CHECK(lidar.
getScanTiltPitch(0) == doctest::Approx(3.0f *
float(
M_PI) / 180.0f).epsilon(1e-5));
5059 std::remove(tilt_xml);
5069 const uint Nt = 8, Np = 8;
5070 const float tmin = 0.97f * float(
M_PI);
5071 const float tmax = float(
M_PI);
5072 const float tilt = 12.0f * float(
M_PI) / 180.0f;
5073 const float phi0 = 0.5f * float(
M_PI);
5076 const vec3 forward_axis =
make_vec3(sinf(phi0), cosf(phi0), 0.f);
5081 ScanMetadata scan(scan_origin, Nt, tmin, tmax, Np, phi0, phi0 + 2.0f *
float(
M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat, 0.0f, tilt);
5087 for (
uint i = 0; i < hc; i++) {
5094 DOCTEST_REQUIRE(n > 0);
5103 DOCTEST_CHECK(mean_dir.
x == doctest::Approx(expected_dir.
x).epsilon(2e-2));
5104 DOCTEST_CHECK(mean_dir.
y == doctest::Approx(expected_dir.
y).epsilon(2e-2));
5105 DOCTEST_CHECK(mean_dir.
z == doctest::Approx(expected_dir.
z).epsilon(2e-2));
5108 DOCTEST_CHECK(
mean.y > 0.5f);
5109 DOCTEST_CHECK(fabs(
mean.x) < 0.3f);
5113DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Scanner Azimuth Offset Test") {
5131 const vec3 scan_origin(0.0f, 0.0f, 0.0f);
5132 const uint Ntheta = 30;
5133 const uint Nphi = 30;
5134 const float phiMin = 0.0f;
5135 const float phiMax = 2.0f * float(
M_PI);
5136 const float exitDiameter = 0.0f;
5137 const float beamDivergence = 0.0f;
5138 std::vector<std::string> columnFormat;
5142 const float cone_thetaMin = 0.80f * float(
M_PI);
5143 const float cone_thetaMax = float(
M_PI);
5144 auto run_scan = [&](
float azimuth_offset, std::vector<vec3> &dirs) {
5147 ScanMetadata scan(scan_origin, Ntheta, cone_thetaMin, cone_thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat, 0.0f, 0.0f, azimuth_offset);
5154 for (
uint i = 0; i < hc; i++) {
5163 std::vector<vec3> dirs_default, dirs_zero;
5164 run_scan(0.0f, dirs_zero);
5167 ScanMetadata scan(scan_origin, Ntheta, cone_thetaMin, cone_thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
5172 for (
uint i = 0; i < hc; i++) {
5175 dirs_default.push_back(d);
5177 DOCTEST_REQUIRE(dirs_zero.size() == dirs_default.size());
5178 DOCTEST_CHECK(dirs_zero.size() > 0);
5179 for (
size_t i = 0; i < dirs_zero.size(); i++) {
5180 DOCTEST_CHECK(dirs_zero[i].x == doctest::Approx(dirs_default[i].x).epsilon(1e-5));
5181 DOCTEST_CHECK(dirs_zero[i].y == doctest::Approx(dirs_default[i].y).epsilon(1e-5));
5182 DOCTEST_CHECK(dirs_zero[i].z == doctest::Approx(dirs_default[i].z).epsilon(1e-5));
5190 const float azimuth_offset = 35.0f * float(
M_PI) / 180.0f;
5192 std::vector<vec3> base_dirs, offset_dirs;
5193 run_scan(0.0f, base_dirs);
5194 run_scan(azimuth_offset, offset_dirs);
5195 DOCTEST_REQUIRE(base_dirs.size() == offset_dirs.size());
5196 DOCTEST_CHECK(base_dirs.size() > 0);
5198 for (
size_t i = 0; i < base_dirs.size(); i++) {
5200 DOCTEST_CHECK(offset_dirs[i].x == doctest::Approx(expected.
x).epsilon(1e-3));
5201 DOCTEST_CHECK(offset_dirs[i].y == doctest::Approx(expected.
y).epsilon(1e-3));
5202 DOCTEST_CHECK(offset_dirs[i].z == doctest::Approx(expected.
z).epsilon(1e-3));
5214 const uint Nt = 8, Np = 8;
5215 const float tmin = 0.97f * float(
M_PI);
5216 const float tmax = float(
M_PI);
5217 const float pitch = 12.0f * float(
M_PI) / 180.0f;
5218 const float scan_phiMin = 0.0f;
5219 const float azimuth_offset = 0.5f * float(
M_PI);
5223 ScanMetadata scan(scan_origin, Nt, tmin, tmax, Np, scan_phiMin, scan_phiMin + 2.0f *
float(
M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat, 0.0f, pitch, azimuth_offset);
5229 for (
uint i = 0; i < hc; i++) {
5236 DOCTEST_REQUIRE(n > 0);
5241 const float heading = scan_phiMin - azimuth_offset;
5242 const vec3 forward_axis =
make_vec3(sinf(heading), cosf(heading), 0.f);
5247 DOCTEST_CHECK(mean_dir.
x == doctest::Approx(expected_dir.
x).epsilon(2e-2));
5248 DOCTEST_CHECK(mean_dir.
y == doctest::Approx(expected_dir.
y).epsilon(2e-2));
5249 DOCTEST_CHECK(mean_dir.
z == doctest::Approx(expected_dir.
z).epsilon(2e-2));
5250 DOCTEST_CHECK(
mean.y < -0.5f);
5251 DOCTEST_CHECK(fabs(
mean.x) < 0.3f);
5256 const char *az_xml =
"lidar_scanazimuth_xml_test.xml";
5257 std::ofstream ofs(az_xml);
5260 <<
" <origin> 0 0 0 </origin>\n"
5261 <<
" <size> 10 10 </size>\n"
5262 <<
" <scanAzimuthOffset> 45 </scanAzimuthOffset>\n"
5265 <<
" <origin> 0 0 0 </origin>\n"
5266 <<
" <size> 10 10 </size>\n"
5273 DOCTEST_CHECK_NOTHROW(lidar.
loadXML(az_xml));
5279 std::remove(az_xml);
5283DOCTEST_TEST_CASE(
"LiDAR Synthetic Scan Continuous Azimuth Skew Test") {
5302 const vec3 scan_origin(0.0f, 0.0f, 0.0f);
5303 const float exitDiameter = 0.0f;
5304 const float beamDivergence = 0.0f;
5305 std::vector<std::string> columnFormat;
5310 auto run_azimuths = [&](
uint Ntheta,
uint Nphi,
float thetaMin,
float thetaMax,
float phiMin,
float phiMax, std::vector<float> &azim) {
5313 ScanMetadata scan(scan_origin, Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, exitDiameter, beamDivergence, 0.0f, 0.0f, columnFormat);
5320 for (
uint i = 0; i < hc; i++) {
5329 const float thetaMin = 144.0f * float(
M_PI) / 180.0f;
5330 const float thetaMax = 171.0f * float(
M_PI) / 180.0f;
5331 const float phiMin = 0.0f;
5332 const float phiMax = 30.0f * float(
M_PI) / 180.0f;
5336 const uint Ntheta = 12;
5337 const uint Nphi = 6;
5338 std::vector<float> azim;
5339 run_azimuths(Ntheta, Nphi, thetaMin, thetaMax, phiMin, phiMax, azim);
5340 DOCTEST_REQUIRE(azim.size() ==
size_t(Ntheta) * Nphi);
5342 const float dphi = (phiMax - phiMin) /
float(Nphi - 1);
5343 const float dphi_per_row = dphi / float(Ntheta);
5346 for (
uint j = 0; j < Nphi; j++) {
5347 for (
uint i = 0; i < Ntheta; i++) {
5348 float expected = phiMin + float(j) * dphi + float(i) * dphi_per_row;
5349 DOCTEST_CHECK(azim[
size_t(Ntheta) * j + i] == doctest::Approx(expected).epsilon(1e-4));
5354 for (
uint j = 0; j < Nphi; j++) {
5355 float bottom = azim[size_t(Ntheta) * j + 0];
5356 float top = azim[size_t(Ntheta) * j + (Ntheta - 1)];
5357 DOCTEST_CHECK((top - bottom) == doctest::Approx(dphi *
float(Ntheta - 1) /
float(Ntheta)).epsilon(1e-3));
5361 for (
uint j = 0; j + 1 < Nphi; j++) {
5362 float top_j = azim[size_t(Ntheta) * j + (Ntheta - 1)];
5363 float bottom_next = azim[size_t(Ntheta) * (j + 1) + 0];
5364 DOCTEST_CHECK((bottom_next - top_j) == doctest::Approx(dphi_per_row).epsilon(1e-3));
5370 const uint Ntheta = 1;
5371 const uint Nphi = 6;
5372 std::vector<float> azim;
5373 run_azimuths(Ntheta, Nphi, thetaMin, thetaMax, phiMin, phiMax, azim);
5374 DOCTEST_REQUIRE(azim.size() ==
size_t(Nphi));
5375 const float dphi = (phiMax - phiMin) /
float(Nphi - 1);
5376 for (
uint j = 0; j < Nphi; j++) {
5377 DOCTEST_CHECK(azim[j] == doctest::Approx(phiMin +
float(j) * dphi).epsilon(1e-4));
5383 const uint Ntheta = 12;
5384 const uint Nphi = 1;
5385 std::vector<float> azim;
5386 run_azimuths(Ntheta, Nphi, thetaMin, thetaMax, phiMin, phiMax, azim);
5387 DOCTEST_REQUIRE(azim.size() ==
size_t(Ntheta));
5388 for (
uint i = 0; i < Ntheta; i++) {
5389 DOCTEST_CHECK(azim[i] == doctest::Approx(phiMin).epsilon(1e-4));
5410 struct GenerativeGrid {
5413 double theta_min, theta_max;
5414 double phi_min, phi_max;
5418 double zenith(
int row)
const {
5419 const double frac = double(row) / double(Ntheta - 1);
5421 return theta_min + (theta_max - theta_min) * frac + curve * (frac - 0.5) * (frac - 0.5);
5423 double azimuth(
int row,
int col)
const {
5424 const double col_frac = double(col) / double(Nphi - 1);
5425 const double base = phi_min + (phi_max - phi_min) * col_frac;
5426 return base + shear * double(row) / double(Ntheta - 1);
5437 double d = va.
x * vb.
x + va.
y * vb.
y + va.
z * vb.
z;
5438 d = std::max(-1.0, std::min(1.0, d));
5439 return std::acos(d);
5444DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Row/Column Idealized Grid") {
5451 GenerativeGrid g{20, 36, 0.05, 0.95 *
M_PI, 0.0, 2.0 *
M_PI, 0.0, 0.0};
5452 ScanMetadata scan(
make_vec3(0, 0, 0), g.Ntheta, g.theta_min, g.theta_max, g.Nphi, g.phi_min, g.phi_max, 0.0f, 0.0f, 0.0f, 0.0f, {});
5456 const int blank_r0 = 5, blank_r1 = 9, blank_c0 = 10, blank_c1 = 15;
5457 std::set<std::pair<int, int>> blanks;
5458 for (
int row = 0; row < g.Ntheta; row++) {
5459 for (
int col = 0; col < g.Nphi; col++) {
5460 if (row >= blank_r0 && row <= blank_r1 && col >= blank_c0 && col <= blank_c1) {
5461 blanks.insert({row, col});
5466 std::map<std::string, double> data;
5468 data[
"column"] = col;
5474 std::vector<vec3> filled = lidar.
gapfillMisses(0,
false,
true);
5477 DOCTEST_CHECK(filled.size() == blanks.size());
5478 DOCTEST_CHECK(lidar.
getHitCount() == hits_before + blanks.size());
5481 double max_err = 0.0;
5489 int row = (int) std::lround(lidar.
getHitData(r,
"row"));
5490 int col = (int) std::lround(lidar.
getHitData(r,
"column"));
5491 double err = angularError(lidar.
getHitRaydir(r), g.direction(row, col));
5492 max_err = std::max(max_err, err);
5495 DOCTEST_CHECK(max_err < 1e-3);
5498DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Row/Column Tilted and Sheared Grid") {
5504 GenerativeGrid g{24, 48, 0.05, 0.95 *
M_PI, 0.0, 1.5 *
M_PI, 0.30, 0.20};
5505 ScanMetadata scan(
make_vec3(0, 0, 0), g.Ntheta, g.theta_min, g.theta_max, g.Nphi, g.phi_min, g.phi_max, 0.0f, 0.0f, 0.0f, 0.0f, {});
5508 const int blank_r0 = 8, blank_r1 = 14, blank_c0 = 18, blank_c1 = 30;
5509 std::set<std::pair<int, int>> blanks;
5510 for (
int row = 0; row < g.Ntheta; row++) {
5511 for (
int col = 0; col < g.Nphi; col++) {
5512 if (row >= blank_r0 && row <= blank_r1 && col >= blank_c0 && col <= blank_c1) {
5513 blanks.insert({row, col});
5518 std::map<std::string, double> data;
5520 data[
"column"] = col;
5525 std::vector<vec3> filled = lidar.
gapfillMisses(0,
false,
true);
5526 DOCTEST_CHECK(filled.size() == blanks.size());
5528 double max_err_fit = 0.0;
5529 double max_err_affine = 0.0;
5534 int row = (int) std::lround(lidar.
getHitData(r,
"row"));
5535 int col = (int) std::lround(lidar.
getHitData(r,
"column"));
5537 max_err_fit = std::max(max_err_fit, angularError(lidar.
getHitRaydir(r), truth));
5540 max_err_affine = std::max(max_err_affine, angularError(affine, truth));
5544 DOCTEST_CHECK(max_err_fit < 0.02);
5545 DOCTEST_CHECK(max_err_fit < 0.25 * max_err_affine);
5548DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Row/Column Noise Robustness") {
5553 GenerativeGrid g{24, 48, 0.05, 0.95 *
M_PI, 0.0, 1.5 *
M_PI, 0.30, 0.15};
5554 ScanMetadata scan(
make_vec3(0, 0, 0), g.Ntheta, g.theta_min, g.theta_max, g.Nphi, g.phi_min, g.phi_max, 0.0f, 0.0f, 0.0f, 0.0f, {});
5558 auto noise = [](
int row,
int col) {
5559 double s = std::sin(12.9898 * row + 78.233 * col) * 43758.5453;
5560 return (s - std::floor(s)) - 0.5;
5563 const double grid_dphi = (g.phi_max - g.phi_min) /
double(g.Nphi - 1);
5564 const double grid_dtheta = (g.theta_max - g.theta_min) /
double(g.Ntheta - 1);
5566 const int blank_r0 = 8, blank_r1 = 14, blank_c0 = 18, blank_c1 = 30;
5567 int outlier_counter = 0;
5568 for (
int row = 0; row < g.Ntheta; row++) {
5569 for (
int col = 0; col < g.Nphi; col++) {
5570 if (row >= blank_r0 && row <= blank_r1 && col >= blank_c0 && col <= blank_c1) {
5573 double zen = g.zenith(row) + 0.05 * grid_dtheta * noise(row, col);
5574 double az = g.azimuth(row, col) + 0.05 * grid_dphi * noise(col, row);
5576 if ((outlier_counter++ % 33) == 0) {
5582 std::map<std::string, double> data;
5584 data[
"column"] = col;
5589 std::vector<vec3> filled = lidar.
gapfillMisses(0,
false,
false);
5590 DOCTEST_CHECK(filled.size() > 0);
5592 double max_err = 0.0;
5597 int row = (int) std::lround(lidar.
getHitData(r,
"row"));
5598 int col = (int) std::lround(lidar.
getHitData(r,
"column"));
5599 max_err = std::max(max_err, angularError(lidar.
getHitRaydir(r), g.direction(row, col)));
5602 DOCTEST_CHECK(max_err < 0.05);
5605DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Row/Column Near-Zenith Extrapolation") {
5611 GenerativeGrid g{30, 48, 0.02, 0.95 *
M_PI, 0.0, 1.5 *
M_PI, 0.25, 0.10};
5612 ScanMetadata scan(
make_vec3(0, 0, 0), g.Ntheta, g.theta_min, g.theta_max, g.Nphi, g.phi_min, g.phi_max, 0.0f, 0.0f, 0.0f, 0.0f, {});
5616 const int empty_rows_below = 6;
5617 for (
int row = empty_rows_below; row < g.Ntheta; row++) {
5618 for (
int col = 0; col < g.Nphi; col++) {
5621 std::map<std::string, double> data;
5623 data[
"column"] = col;
5628 std::vector<vec3> filled = lidar.
gapfillMisses(0,
false,
true);
5631 DOCTEST_CHECK(filled.size() == (
size_t) (empty_rows_below * g.Nphi));
5634 double max_err = 0.0;
5640 int row = (int) std::lround(lidar.
getHitData(r,
"row"));
5641 int col = (int) std::lround(lidar.
getHitData(r,
"column"));
5642 if (row < empty_rows_below) {
5643 DOCTEST_CHECK(lidar.
getHitData(r,
"gapfillMisses_code") == 4.0);
5645 max_err = std::max(max_err, angularError(lidar.
getHitRaydir(r), g.direction(row, col)));
5648 DOCTEST_CHECK(n_extrap == empty_rows_below * g.Nphi);
5650 DOCTEST_CHECK(max_err < 0.1);
5653DOCTEST_TEST_CASE(
"LiDAR Miss Gapfilling - Dispatcher Selection") {
5661 ScanMetadata scan(
make_vec3(0, 0, 0), 10, 0.05, 0.95 * M_PI, 18, 0.0, 2.0 * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, {});
5671 }
catch (
const std::runtime_error &e) {
5675 DOCTEST_CHECK(threw);
5676 DOCTEST_CHECK(msg.find(
"neither 'timestamp' nor 'row'/'column'") != std::string::npos);
5683 GenerativeGrid g{12, 24, 0.05, 0.95 *
M_PI, 0.0, 2.0 *
M_PI, 0.1, 0.0};
5684 ScanMetadata scan(
make_vec3(0, 0, 0), g.Ntheta, g.theta_min, g.theta_max, g.Nphi, g.phi_min, g.phi_max, 0.0f, 0.0f, 0.0f, 0.0f, {});
5686 for (
int row = 0; row < g.Ntheta; row++) {
5687 for (
int col = 0; col < g.Nphi; col++) {
5688 if (row == 5 && col >= 8 && col <= 12) {
5692 std::map<std::string, double> data;
5694 data[
"column"] = col;
5698 std::vector<vec3> filled = lidar.
gapfillMisses(0,
false,
true);
5699 DOCTEST_CHECK(filled.size() == 5);
5701 bool found_interior_flag =
false;
5704 found_interior_flag =
true;
5708 DOCTEST_CHECK(found_interior_flag);
5712DOCTEST_TEST_CASE(
"LiDAR LAD Inversion Uncertainty") {
5724 vec3 scan_origin(-5.0f, 0.0f, 0.5f);
5727 std::vector<std::string> columnFormat;
5728 ScanMetadata scan(scan_origin, Ntheta, 0.0f, M_PI, Nphi, 0.0f, 2.0f * M_PI, 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
5729 DOCTEST_CHECK_NOTHROW(lidar.
addScan(scan));
5731 vec3 grid_center(0.0f, 0.0f, 0.5f);
5732 vec3 grid_size(1.0f, 1.0f, 1.0f);
5733 DOCTEST_CHECK_NOTHROW(lidar.
addGrid(grid_center, grid_size,
make_int3(1, 1, 1), 0));
5735 float volume = gsize.
x * gsize.
y * gsize.
z;
5738 std::vector<uint> UUIDs =
context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
5739 DOCTEST_CHECK(!UUIDs.empty());
5750 DOCTEST_CHECK(N > 0);
5752 DOCTEST_CHECK(leaf_area > 0.f);
5754 DOCTEST_CHECK(Gtheta > 0.f);
5756 DOCTEST_CHECK(I > 0.f);
5757 DOCTEST_CHECK(I < 1.f);
5759 DOCTEST_CHECK(zbar > 0.f);
5760 DOCTEST_CHECK(zbar < 2.f);
5763 DOCTEST_CHECK(var_sampling_only >= 0.f);
5764 DOCTEST_CHECK(var_sampling_only == var_sampling_only);
5771 float I_b = std::min(I, 1.f - 1.f / (2.f *
float(N) + 2.f));
5772 float expected_var = I_b / (float(N) * (1.f - I_b) * zbar * zbar * Gtheta * Gtheta);
5773 DOCTEST_CHECK(var_sampling_only == doctest::Approx(expected_var).epsilon(0.02f));
5775 float a_est = leaf_area / volume;
5776 DOCTEST_CHECK(a_est > 0.f);
5781 DOCTEST_CHECK(var_with_element >= 0.f);
5783 DOCTEST_CHECK(var_with_element >= var_sampling_only - 1e-9f);
5786 float lo = 0.f, hi = 0.f;
5789 DOCTEST_CHECK(lo >= 0.f);
5790 DOCTEST_CHECK(lo < leaf_area);
5791 DOCTEST_CHECK(hi > leaf_area);
5794 float mean_lad = 0.f, glo = 0.f, ghi = 0.f;
5797 DOCTEST_CHECK(mean_lad == doctest::Approx(a_est).epsilon(1e-3f));
5798 DOCTEST_CHECK(glo >= 0.f);
5799 DOCTEST_CHECK(glo <= mean_lad);
5800 DOCTEST_CHECK(ghi >= mean_lad);
5803 DOCTEST_CHECK(have_group);
5806 const char *uncertainty_file =
"lidar_lad_uncertainty_selftest.txt";
5809 std::ifstream in(uncertainty_file);
5810 DOCTEST_CHECK(in.is_open());
5812 std::getline(in, header);
5813 DOCTEST_CHECK(header.find(
"cell_index") != std::string::npos);
5814 DOCTEST_CHECK(header.find(
"LAD_std_error") != std::string::npos);
5816 bool have_row = (bool) std::getline(in, row);
5817 DOCTEST_CHECK(have_row);
5818 DOCTEST_CHECK(!row.empty());
5820 std::remove(uncertainty_file);
5832static vec3 test_quat_rotate(
const vec4 &q,
const vec3 &v) {
5835 return v + t * q.
w +
cross(qv, t);
5839static vec4 test_quat_from_rpy(
float roll,
float pitch,
float yaw) {
5840 const float cr = std::cos(roll * 0.5f), sr = std::sin(roll * 0.5f);
5841 const float cp = std::cos(pitch * 0.5f), sp = std::sin(pitch * 0.5f);
5842 const float cy = std::cos(yaw * 0.5f), sy = std::sin(yaw * 0.5f);
5844 q.
w = cr * cp * cy + sr * sp * sy;
5845 q.
x = sr * cp * cy - cr * sp * sy;
5846 q.
y = cr * sp * cy + sr * cp * sy;
5847 q.
z = cr * cp * sy - sr * sp * cy;
5851DOCTEST_TEST_CASE(
"LiDAR Moving Platform Per-Beam Origin Reconstruction") {
5861 const uint Ntheta = 8;
5862 const uint Nphi = 12;
5863 const float thetaMin = 0.97f * float(M_PI);
5864 const float thetaMax = float(M_PI);
5865 const float phiMin = 0.0f;
5866 const float phiMax = 2.0f * float(M_PI);
5867 const float H = 10.0f;
5868 const float v = 2.0f;
5872 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
5875 const float pulse_rate = 1000.0f;
5876 const double pulse_period = 1.0 / double(pulse_rate);
5877 const double t_total = double(Ntheta * Nphi) * pulse_period;
5878 std::vector<double> traj_t;
5879 std::vector<vec3> traj_pos;
5880 std::vector<vec4> traj_quat;
5882 for (
int k = 0; k < M; k++) {
5883 double tk = t_total * double(k) / double(M - 1);
5884 traj_t.push_back(tk);
5885 traj_pos.push_back(
make_vec3(0.f,
float(v * tk), H));
5886 traj_quat.push_back(
make_vec4(0, 0, 0, 1));
5889 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), pulse_rate, 0.0);
5893 DOCTEST_REQUIRE(hit_count > 0);
5896 for (
uint i = 0; i < hit_count; i++) {
5899 DOCTEST_CHECK(origin.
x == doctest::Approx(0.0f).epsilon(0.001));
5900 DOCTEST_CHECK(origin.
z == doctest::Approx(H).epsilon(0.001));
5901 DOCTEST_CHECK(origin.
y == doctest::Approx(
float(v * t)).epsilon(0.001));
5905 std::map<double, double> pulse_time;
5906 for (
uint i = 0; i < hit_count; i++) {
5908 double pid = lidar.
getHitData(i,
"pulse_id");
5910 if (pulse_time.count(pid) == 0) {
5911 pulse_time[pid] = t;
5913 DOCTEST_CHECK(pulse_time[pid] == doctest::Approx(t));
5916 DOCTEST_CHECK(t == doctest::Approx(pid * pulse_period));
5920 double prev_t = -1.0;
5921 double prev_pid = -1.0;
5922 for (
auto &kv: pulse_time) {
5923 if (prev_pid >= 0) {
5924 DOCTEST_CHECK(kv.second > prev_t);
5926 prev_pid = kv.first;
5931DOCTEST_TEST_CASE(
"LiDAR Moving Platform Static Equivalence") {
5935 const uint Ntheta = 10;
5936 const uint Nphi = 16;
5937 const float thetaMin = 0.95f * float(M_PI);
5938 const float thetaMax = float(M_PI);
5939 const float phiMin = 0.0f;
5940 const float phiMax = 2.0f * float(M_PI);
5941 const float H = 8.0f;
5950 build_scene(context_static);
5953 ScanMetadata scan_static(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
5954 lidar_static.
addScan(scan_static);
5959 build_scene(context_moving);
5962 ScanMetadata scan_moving(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
5963 std::vector<double> traj_t = {0.0, 1.0};
5966 lidar_moving.
addScanMoving(scan_moving, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 1.0e6f, 0.0);
5971 DOCTEST_REQUIRE(count_static > 0);
5972 DOCTEST_CHECK(count_moving == count_static);
5977 for (
uint i = 0; i < n; i++) {
5984 return c / float(n);
5987 vec3 c_static = centroid(lidar_static);
5988 vec3 c_moving = centroid(lidar_moving);
5989 DOCTEST_CHECK(c_moving.
x == doctest::Approx(c_static.
x).epsilon(0.01));
5990 DOCTEST_CHECK(c_moving.
y == doctest::Approx(c_static.
y).epsilon(0.01));
5991 DOCTEST_CHECK(c_moving.
z == doctest::Approx(c_static.
z).epsilon(0.01));
5994DOCTEST_TEST_CASE(
"LiDAR Moving Platform Non-Trivial Attitude") {
6003 const uint Ntheta = 6;
6004 const uint Nphi = 10;
6005 const float thetaMin = 0.97f * float(M_PI);
6006 const float thetaMax = float(M_PI);
6007 const float phiMin = 0.0f;
6008 const float phiMax = 2.0f * float(M_PI);
6009 const float H = 15.0f;
6012 const float roll = 0.10f;
6013 const float pitch = -0.07f;
6018 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6020 const float pulse_rate = 2000.0f;
6021 const double pulse_period = 1.0 / double(pulse_rate);
6022 const double t_total = double(Ntheta * Nphi) * pulse_period;
6023 std::vector<double> traj_t;
6024 std::vector<vec3> traj_pos;
6025 std::vector<vec4> traj_quat;
6027 for (
int k = 0; k < M; k++) {
6028 double tk = t_total * double(k) / double(M - 1);
6029 float yaw = 0.5f * float(tk / t_total);
6030 traj_t.push_back(tk);
6031 traj_pos.push_back(
make_vec3(
float(1.5 * tk),
float(-0.8 * tk), H));
6032 traj_quat.push_back(test_quat_from_rpy(roll, pitch, yaw));
6039 DOCTEST_REQUIRE(hit_count > 0);
6045 auto pose_at = [&](
double t,
vec3 &pos,
vec4 &quat) {
6046 if (t <= traj_t.front()) {
6047 pos = traj_pos.front();
6048 quat = traj_quat.front();
6051 if (t >= traj_t.back()) {
6052 pos = traj_pos.back();
6053 quat = traj_quat.back();
6057 while (i1 < traj_t.size() && traj_t[i1] < t) {
6061 double u = (t - traj_t[i0]) / (traj_t[i1] - traj_t[i0]);
6062 pos = traj_pos[i0] + (traj_pos[i1] - traj_pos[i0]) *
float(u);
6064 vec4 q0 = traj_quat[i0], q1 = traj_quat[i1];
6067 double dot = double(q0.
x) * q1.x + double(q0.
y) * q1.y + double(q0.
z) * q1.z + double(q0.
w) * q1.w;
6069 q1 =
make_vec4(-q1.x, -q1.y, -q1.z, -q1.w);
6074 q =
make_vec4(q0.
x +
float(u) * (q1.x - q0.
x), q0.
y +
float(u) * (q1.y - q0.
y), q0.
z +
float(u) * (q1.z - q0.
z), q0.
w +
float(u) * (q1.w - q0.
w));
6076 double th0 = std::acos(dot);
6077 double th = th0 * u;
6078 double s0 = std::sin(th0 - th) / std::sin(th0);
6079 double s1 = std::sin(th) / std::sin(th0);
6080 q =
make_vec4(
float(s0 * q0.
x + s1 * q1.x),
float(s0 * q0.
y + s1 * q1.y),
float(s0 * q0.
z + s1 * q1.z),
float(s0 * q0.
w + s1 * q1.w));
6087 for (
uint i = 0; i < hit_count; i++) {
6091 pose_at(t, pos, quat);
6092 vec3 expected_origin = pos + test_quat_rotate(quat, lever_arm);
6094 DOCTEST_CHECK(origin.
x == doctest::Approx(expected_origin.
x).epsilon(0.005));
6095 DOCTEST_CHECK(origin.
y == doctest::Approx(expected_origin.
y).epsilon(0.005));
6096 DOCTEST_CHECK(origin.
z == doctest::Approx(expected_origin.
z).epsilon(0.005));
6099 DOCTEST_REQUIRE(checked > 0);
6107 pose_at(lidar.
getHitData(0,
"timestamp"), pos0, q0);
6108 vec3 rotated_lever = test_quat_rotate(q0, lever_arm);
6109 DOCTEST_CHECK(rotated_lever.
magnitude() == doctest::Approx(lever_arm.
magnitude()).epsilon(1e-4));
6113DOCTEST_TEST_CASE(
"LiDAR Moving Platform Euler-Angle Overload Equivalence") {
6118 const uint Ntheta = 6;
6119 const uint Nphi = 12;
6120 const float thetaMin = 0.96f * float(M_PI);
6121 const float thetaMax = float(M_PI);
6122 const float phiMin = 0.0f;
6123 const float phiMax = 2.0f * float(M_PI);
6124 const float H = 12.0f;
6133 std::vector<double> traj_t;
6134 std::vector<vec3> traj_pos;
6135 std::vector<vec3> traj_rpy;
6136 for (
int k = 0; k < M; k++) {
6137 double tk = double(k) / double(M - 1);
6138 float roll = 0.08f * float(tk);
6139 float pitch = -0.05f * float(tk);
6140 float yaw = 0.3f * float(tk);
6141 traj_t.push_back(tk);
6142 traj_pos.push_back(
make_vec3(
float(2.0 * tk),
float(-1.0 * tk), H));
6143 traj_rpy.push_back(
make_vec3(roll, pitch, yaw));
6147 std::vector<vec4> traj_quat;
6148 for (
const vec3 &rpy: traj_rpy) {
6149 traj_quat.push_back(test_quat_from_rpy(rpy.x, rpy.y, rpy.z));
6154 const float pulseRate = 2000.0f;
6158 build_scene(context_q);
6161 ScanMetadata scan_q(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6162 lidar_q.
addScanMoving(scan_q, traj_t, traj_pos, traj_quat, lever, boresight, pulseRate, 0.0);
6167 build_scene(context_e);
6170 ScanMetadata scan_e(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, phiMin, phiMax, 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6171 lidar_e.
addScanMoving(scan_e, traj_t, traj_pos, traj_rpy, lever, boresight, pulseRate, 0.0);
6176 DOCTEST_REQUIRE(nq > 0);
6177 DOCTEST_CHECK(ne == nq);
6181 for (
uint i = 0; i < nq && i < ne; i++) {
6184 DOCTEST_CHECK(pe.
x == doctest::Approx(pq.
x).epsilon(1e-5));
6185 DOCTEST_CHECK(pe.
y == doctest::Approx(pq.
y).epsilon(1e-5));
6186 DOCTEST_CHECK(pe.
z == doctest::Approx(pq.
z).epsilon(1e-5));
6189 DOCTEST_CHECK(oe.
x == doctest::Approx(oq.
x).epsilon(1e-5));
6190 DOCTEST_CHECK(oe.
y == doctest::Approx(oq.
y).epsilon(1e-5));
6191 DOCTEST_CHECK(oe.
z == doctest::Approx(oq.
z).epsilon(1e-5));
6194 DOCTEST_REQUIRE(compared > 0);
6197DOCTEST_TEST_CASE(
"LiDAR Moving Platform Leaf Area Inversion") {
6205 std::vector<uint> UUIDs =
context.loadXML(
"plugins/lidar/xml/leaf_cube_LAI2_lw0_01_spherical.xml",
true);
6206 DOCTEST_REQUIRE(!UUIDs.empty());
6212 const vec3 grid_center(0.0f, 0.0f, 0.5f);
6213 const vec3 grid_size(1.0f, 1.0f, 1.0f);
6217 float LAD_exact = 0.f;
6218 for (
uint UUID: UUIDs) {
6219 LAD_exact +=
context.getPrimitiveArea(UUID) / (gsize.
x * gsize.
y * gsize.
z);
6221 DOCTEST_REQUIRE(LAD_exact > 0.f);
6226 std::vector<float> beamZenithAngles;
6227 const int Nchannels = 30;
6228 for (
int c = 0; c < Nchannels; c++) {
6229 float dev = (float(c) / float(Nchannels - 1) - 0.5f) *
deg2rad(40.0f);
6230 beamZenithAngles.push_back(
float(M_PI) - fabsf(dev));
6232 const uint Nphi = 400;
6233 ScanMetadata scan(
make_vec3(0, 0, 5), beamZenithAngles, Nphi, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6236 std::vector<double> traj_t;
6237 std::vector<vec3> traj_pos;
6238 std::vector<vec3> traj_rpy;
6240 for (
int k = 0; k < M; k++) {
6241 double tk = double(k) / double(M - 1);
6242 traj_t.push_back(tk);
6243 traj_pos.push_back(
make_vec3(
float(-1.0 + 2.0 * tk), 0.f, 5.f));
6246 const float pulseRate = float(Nchannels * Nphi);
6248 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_rpy,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), pulseRate, 0.0);
6257 DOCTEST_CHECK(LAD == LAD);
6259 DOCTEST_CHECK(fabs(LAD - LAD_exact) / LAD_exact == doctest::Approx(0.0f).epsilon(0.15f));
6262DOCTEST_TEST_CASE(
"LiDAR calculateLeafArea Supplied-Gtheta Validation") {
6272 bool threw_zero =
false, threw_high =
false;
6286 DOCTEST_CHECK(threw_zero);
6287 DOCTEST_CHECK(threw_high);
6290DOCTEST_TEST_CASE(
"LiDAR Moving Platform exportScans Origin Handling") {
6299 const uint Ntheta = 6;
6300 const uint Nphi = 10;
6301 const float thetaMin = 0.97f * float(M_PI);
6302 const float thetaMax = float(M_PI);
6303 const float H = 10.0f;
6306 std::vector<std::string> columnFormat = {
"x",
"y",
"z",
"timestamp",
"origin_x",
"origin_y",
"origin_z"};
6310 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, thetaMin, thetaMax, Nphi, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, columnFormat);
6311 std::vector<double> traj_t = {0.0, 1.0};
6314 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 1000.0f, 0.0);
6318 DOCTEST_REQUIRE(export_hits > 0);
6320 const char *xml_file =
"moving_export_test.xml";
6321 const char *xyz_file =
"moving_export_test_0.xyz";
6325 std::string xml_contents;
6327 std::ifstream in(xml_file);
6328 DOCTEST_REQUIRE(in.good());
6329 std::stringstream ss;
6331 xml_contents = ss.str();
6333 DOCTEST_CHECK(xml_contents.find(
"<origin>") == std::string::npos);
6334 DOCTEST_CHECK(xml_contents.find(
"origin_x") != std::string::npos);
6341 DOCTEST_CHECK_NOTHROW(reloaded.
loadXML(xml_file));
6342 DOCTEST_CHECK(reloaded.
getHitCount() == export_hits);
6347 DOCTEST_CHECK(o.
z == doctest::Approx(H).epsilon(0.001));
6350 std::remove(xml_file);
6351 std::remove(xyz_file);
6354DOCTEST_TEST_CASE(
"LiDAR loadXML Requires An Origin Source") {
6356 const char *bad_xml =
"no_origin_test.xml";
6358 std::ofstream out(bad_xml);
6359 out <<
"<helios>\n";
6361 out <<
" <size>10 10</size>\n";
6362 out <<
" <ASCII_format>x y z</ASCII_format>\n";
6363 out <<
" </scan>\n";
6364 out <<
"</helios>\n";
6378 DOCTEST_CHECK(threw);
6380 std::remove(bad_xml);
6383DOCTEST_TEST_CASE(
"LiDAR Moving Platform getHitRaydir Uses Per-Beam Origin") {
6392 const uint Ntheta = 8;
6393 const uint Nphi = 16;
6394 const float H = 10.0f;
6397 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, 0.96f *
float(M_PI),
float(M_PI), Nphi, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6398 std::vector<double> traj_t = {0.0, 1.0};
6401 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_rpy,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0),
float(Ntheta * Nphi), 0.0);
6405 DOCTEST_REQUIRE(n > 0);
6408 uint mismatched_with_static = 0;
6409 for (
uint i = 0; i < n; i++) {
6414 DOCTEST_CHECK(rd.
zenith == doctest::Approx(expected.
zenith).epsilon(1e-4));
6415 DOCTEST_CHECK(rd.
azimuth == doctest::Approx(expected.
azimuth).epsilon(1e-4));
6418 if ((lidar.
getHitOrigin(i) - static_origin).magnitude() > 1.0f) {
6421 mismatched_with_static++;
6426 DOCTEST_CHECK(mismatched_with_static > 0);
6429DOCTEST_TEST_CASE(
"LiDAR Moving Platform coordinateShift Preserves Beam Geometry") {
6436 const uint Ntheta = 6;
6437 const uint Nphi = 12;
6438 const float H = 9.0f;
6441 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, 0.96f *
float(M_PI),
float(M_PI), Nphi, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6442 std::vector<double> traj_t = {0.0, 1.0};
6445 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_rpy,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0),
float(Ntheta * Nphi), 0.0);
6449 DOCTEST_REQUIRE(n > 0);
6452 std::vector<vec3> beam_before(n);
6453 for (
uint i = 0; i < n; i++) {
6462 for (
uint i = 0; i < n; i++) {
6464 DOCTEST_CHECK(beam_after.
x == doctest::Approx(beam_before[i].x).epsilon(1e-4));
6465 DOCTEST_CHECK(beam_after.
y == doctest::Approx(beam_before[i].y).epsilon(1e-4));
6466 DOCTEST_CHECK(beam_after.
z == doctest::Approx(beam_before[i].z).epsilon(1e-4));
6470DOCTEST_TEST_CASE(
"LiDAR Moving Platform Static-Only Functions Fail Fast") {
6477 const uint Ntheta = 6;
6478 const uint Nphi = 10;
6479 const float H = 8.0f;
6482 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, 0.96f *
float(M_PI),
float(M_PI), Nphi, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6483 std::vector<double> traj_t = {0.0, 1.0};
6486 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_rpy,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0),
float(Ntheta * Nphi), 0.0);
6489 bool tri_threw =
false, validate_threw =
false;
6498 lidar.validateRayDirections();
6500 validate_threw =
true;
6503 DOCTEST_CHECK(tri_threw);
6504 DOCTEST_CHECK(validate_threw);
6507DOCTEST_TEST_CASE(
"LiDAR Moving Platform addScanMoving Rejects Non-Finite Trajectory") {
6511 ScanMetadata scan(
make_vec3(0, 0, 5), 4, 0.97f *
float(M_PI),
float(M_PI), 8, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6513 const float nan_val = std::numeric_limits<float>::quiet_NaN();
6514 std::vector<double> traj_t = {0.0, 1.0};
6522 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_rpy,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 100.0f, 0.0);
6527 DOCTEST_CHECK(threw);
6530DOCTEST_TEST_CASE(
"LiDAR Moving Platform Gapfill Writes Per-Pulse Origins") {
6538 const uint Ntheta = 8;
6539 const uint Nphi = 60;
6540 const float H = 6.0f;
6541 const float v = 3.0f;
6545 ScanMetadata scan(
make_vec3(0, 0, H), Ntheta, 0.93f *
float(M_PI),
float(M_PI), Nphi, 0.0f, 2.0f *
float(M_PI), 0.0f, 0.0f, 0.0f, 0.0f, std::vector<std::string>());
6547 const float pulseRate = float(Ntheta * Nphi);
6548 const double pulse_period = 1.0 / double(pulseRate);
6549 const double t_total = double(Ntheta * Nphi) * pulse_period;
6550 std::vector<double> traj_t;
6551 std::vector<vec3> traj_pos;
6552 std::vector<vec3> traj_rpy;
6554 for (
int k = 0; k < M; k++) {
6555 double tk = t_total * double(k) / double(M - 1);
6556 traj_t.push_back(tk);
6557 traj_pos.push_back(
make_vec3(
float(v * tk), 0.f, H));
6560 lidar.
addScanMoving(scan, traj_t, traj_pos, traj_rpy,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), pulseRate, 0.0);
6565 DOCTEST_REQUIRE(before > 0);
6568 DOCTEST_REQUIRE(!filled.empty());
6582 DOCTEST_CHECK(o.
z == doctest::Approx(H).epsilon(0.01));
6583 DOCTEST_CHECK(o.
y == doctest::Approx(0.0f).epsilon(0.01));
6584 DOCTEST_CHECK(o.
x == doctest::Approx(
float(v * t)).epsilon(0.05));
6587 DOCTEST_REQUIRE(checked > 0);
6596DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam Multi-Revolution Derivation") {
6606 std::vector<float> beam_elev_rad;
6607 const std::vector<float> elev_deg = {-15.f, -11.f, -7.f, -3.f, 3.f, 7.f, 11.f, 15.f};
6608 for (
float d: elev_deg) {
6609 beam_elev_rad.push_back(d *
float(M_PI) / 180.f);
6611 const uint channels =
uint(beam_elev_rad.size());
6612 const float azimuthStep_rad = 10.f * float(M_PI) / 180.f;
6613 const uint expected_steps_per_rev = 36;
6617 const float H = 30.0f;
6618 const float v = 2.0f;
6619 const double duration = 0.6;
6620 const double target_revs = 3.0;
6621 const float PRF = float(
double(channels) *
double(expected_steps_per_rev) * target_revs / duration);
6623 std::vector<double> traj_t = {0.0, duration};
6624 std::vector<vec3> traj_pos = {
make_vec3(0, 0, H),
make_vec3(0,
float(v * duration), H)};
6629 uint scanID = lidar.
addScanSpinning(beam_elev_rad, azimuthStep_rad, PRF, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0);
6636 DOCTEST_CHECK(lidar.
getScanRevolutions(scanID) == doctest::Approx(target_revs).epsilon(0.001));
6637 const double expected_rotation_rate = double(PRF) / (double(channels) * double(expected_steps_per_rev));
6638 DOCTEST_CHECK(lidar.
getScanRotationRate(scanID) == doctest::Approx(expected_rotation_rate).epsilon(0.001));
6639 DOCTEST_CHECK(lidar.
getScanSizePhi(scanID) ==
uint(std::lround(
double(expected_steps_per_rev) * target_revs)));
6643 DOCTEST_REQUIRE(hit_count > 0);
6647 std::vector<float> channel_zenith;
6648 for (
float er: beam_elev_rad) {
6649 channel_zenith.push_back(0.5f *
float(M_PI) - er);
6652 for (
uint i = 0; i < hit_count; i++) {
6659 float zenith = 0.5f * float(M_PI) - sc.
elevation;
6661 for (
float cz: channel_zenith) {
6662 best = std::min(best, std::fabs(zenith - cz));
6664 DOCTEST_CHECK(best < 1e-3f);
6667 DOCTEST_REQUIRE(checked > 0);
6670 const double pulse_period = 1.0 / double(PRF);
6671 for (
uint i = 0; i < hit_count; i++) {
6673 double pid = lidar.
getHitData(i,
"pulse_id");
6675 DOCTEST_CHECK(t == doctest::Approx(pid * pulse_period));
6679DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam Stationary Seam") {
6684 std::vector<float> beam_elev_rad = {-5.f * float(M_PI) / 180.f, 5.f * float(M_PI) / 180.f};
6685 const float azimuthStep_rad = 10.f * float(M_PI) / 180.f;
6686 const uint expected_steps_per_rev = 36;
6687 const float PRF = 1000.f;
6689 const double one_rev_duration = 2.0 * double(expected_steps_per_rev) / double(PRF);
6690 const std::vector<double> traj_t = {0.0, one_rev_duration};
6696 uint scanID = lidar.
addScanSpinning(beam_elev_rad, azimuthStep_rad, PRF, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0);
6699 DOCTEST_CHECK(lidar.
getScanRevolutions(scanID) == doctest::Approx(1.0).epsilon(0.001));
6700 DOCTEST_CHECK(lidar.
getScanSizePhi(scanID) == expected_steps_per_rev);
6705 std::vector<float> beam_zenith = {0.5f * float(M_PI) - beam_elev_rad[0], 0.5f * float(M_PI) - beam_elev_rad[1]};
6706 ScanMetadata grid(
make_vec3(0, 0, 1), beam_zenith, lidar.
getScanSizePhi(scanID), 0.f, 1.f * 2.f *
float(M_PI), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>());
6707 std::vector<float> phis;
6713 bool seam_duplicate =
false;
6714 for (
size_t a = 0; a < phis.size(); a++) {
6715 for (
size_t b = a + 1; b < phis.size(); b++) {
6716 float diff = std::fabs(std::fmod(phis[a] - phis[b], 2.f *
float(M_PI)));
6717 diff = std::min(diff, 2.f *
float(M_PI) - diff);
6719 seam_duplicate =
true;
6723 DOCTEST_CHECK(!seam_duplicate);
6726DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam Fail-Fast Validation") {
6730 std::vector<float> elev = {-5.f * float(M_PI) / 180.f, 5.f * float(M_PI) / 180.f};
6731 std::vector<double> traj_t = {0.0, 0.5};
6736 DOCTEST_CHECK_THROWS(lidar.
addScanSpinning(std::vector<float>(), 10.f *
float(M_PI) / 180.f, 1000.f, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0));
6738 DOCTEST_CHECK_THROWS(lidar.
addScanSpinning(elev, 0.f, 1000.f, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0));
6740 DOCTEST_CHECK_THROWS(lidar.
addScanSpinning(elev, 10.f *
float(M_PI) / 180.f, 0.f, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0));
6743 std::vector<vec3> traj_pos_short = {
make_vec3(0, 0, 10)};
6744 DOCTEST_CHECK_THROWS(lidar.
addScanSpinning(elev, 10.f *
float(M_PI) / 180.f, 1000.f, traj_t, traj_pos_short, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0));
6745 std::vector<vec4> traj_quat_short = {
make_vec4(0, 0, 0, 1)};
6746 DOCTEST_CHECK_THROWS(lidar.
addScanSpinning(elev, 10.f *
float(M_PI) / 180.f, 1000.f, traj_t, traj_pos, traj_quat_short,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0));
6748 DOCTEST_CHECK_THROWS(lidar.
addScanSpinning(elev, 10.f *
float(M_PI) / 180.f, 1000.f, traj_t, std::vector<vec3>(), traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0));
6751DOCTEST_TEST_CASE(
"LiDAR Azimuth Warning Gated To Static Raster") {
6756 std::string spinning_err;
6760 std::vector<float> elev = {-5.f * float(M_PI) / 180.f, 5.f * float(M_PI) / 180.f};
6761 std::vector<double> traj_t = {0.0, 0.5};
6765 lidar.
addScanSpinning(elev, 1.f *
float(M_PI) / 180.f, 100000.f, traj_t, traj_pos, traj_quat,
make_vec3(0, 0, 0),
make_vec3(0, 0, 0), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>(), 0.0);
6768 DOCTEST_CHECK(spinning_err.find(
"greater than 2pi") == std::string::npos);
6771 std::string raster_err;
6774 ScanMetadata scan(
make_vec3(0, 0, 1), 4, 0.f,
float(M_PI), 8, 0.f, 6.f *
float(M_PI), 0.f, 0.f, 0.f, 0.f, std::vector<std::string>());
6779 DOCTEST_CHECK(raster_err.find(
"greater than 2pi") != std::string::npos);
6782DOCTEST_TEST_CASE(
"LiDAR Spinning Multibeam XML Load And Export Round-Trip") {
6786 const std::string dir =
"lidar_spin_xml_test";
6787 std::filesystem::create_directories(dir);
6788 const std::string xml_path = dir +
"/spin.xml";
6791 std::ofstream xml(xml_path);
6792 xml <<
"<helios>\n";
6794 xml <<
" <origin> 0 0 30 </origin>\n";
6795 xml <<
" <scanPattern> spinning_multibeam </scanPattern>\n";
6796 xml <<
" <beamElevationAngles> -85 -80 -75 -70 </beamElevationAngles>\n";
6797 xml <<
" <azimuthStep> 10 </azimuthStep>\n";
6798 xml <<
" <PRF> 576 </PRF>\n";
6799 xml <<
" <trajectory>\n";
6800 xml <<
" <pose> 0.0 0 0 30 0 0 0 1 </pose>\n";
6801 xml <<
" <pose> 0.5 0 1 30 0 0 0 1 </pose>\n";
6802 xml <<
" </trajectory>\n";
6803 xml <<
" <ASCII_format> x y z origin_x origin_y origin_z timestamp </ASCII_format>\n";
6804 xml <<
" </scan>\n";
6805 xml <<
"</helios>\n";
6814 lidar.
loadXML(xml_path.c_str());
6826 const std::string export_path = dir +
"/spin_export.xml";
6831 lidar2.
loadXML(export_path.c_str());
6838 std::filesystem::remove_all(dir);