test_ndt_model.cpp
Go to the documentation of this file.
1 // Copyright 2024 Ekumen, Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include <gtest/gtest.h>
16 
17 #include <stdexcept>
18 #include <unordered_map>
19 #include <utility>
20 #include <vector>
21 
22 #include <Eigen/Core>
23 #include <sophus/se2.hpp>
24 
28 
29 namespace beluga {
30 
31 namespace {
32 
33 Eigen::Matrix2Xd get_diagonal_covariance_2d(double x_var = 0.5, double y_var = 0.5) {
34  return Eigen::Vector2d{x_var, y_var}.asDiagonal();
35 }
36 
37 using sparse_grid_2d_t = SparseValueGrid2<std::unordered_map<Eigen::Vector2i, NDTCell2d, detail::CellHasher<2>>>;
38 using sparse_grid_3d_t = SparseValueGrid3<std::unordered_map<Eigen::Vector3i, NDTCell3d, detail::CellHasher<3>>>;
39 } // namespace
40 
41 TEST(NDTSensorModel2DTests, CanConstruct) {
42  NDTSensorModel{NDTModelParam2d{}, sparse_grid_2d_t{}};
43 }
44 
45 TEST(NDTSensorModel2DTests, MinLikelihood) {
46  constexpr double kMinimumLikelihood = 1e-6;
47 
48  const NDTModelParam2d param{kMinimumLikelihood};
49  const NDTSensorModel model{param, sparse_grid_2d_t{}};
50 
51  for (const auto& val : {
52  Eigen::Vector2d{0.1, 0.1},
53  Eigen::Vector2d{0.15, 0.15},
54  Eigen::Vector2d{0.25, 0.25},
55  Eigen::Vector2d{0.35, 0.35},
56  Eigen::Vector2d{0.45, 0.45},
57  Eigen::Vector2d{0.50, 0.50},
58  Eigen::Vector2d{1.65, 0.65},
59  Eigen::Vector2d{0.75, 0.75},
60 
61  }) {
62  ASSERT_DOUBLE_EQ(model.likelihood_at({val, get_diagonal_covariance_2d()}), kMinimumLikelihood);
63  }
64 }
65 
66 TEST(NDTSensorModel2DTests, Likelihoood) {
67  typename sparse_grid_2d_t::map_type map;
68  {
69  Eigen::Array<double, 2, 2> cov;
70  // clang-format off
71  cov << 0.5, 0.0,
72  0.0, 0.3;
73  // clang-format on
74  const Eigen::Vector2d mean(0.5, 0.5);
75  map[Eigen::Vector2i(0, 0)] = NDTCell2d{mean, cov};
76  }
77  {
78  Eigen::Array<double, 2, 2> cov;
79  // clang-format off
80  cov << 0.5, 0.0,
81  0.0, 0.5;
82  // clang-format on
83  const Eigen::Vector2d mean(1.5, 1.5);
84  map[Eigen::Vector2i(1, 1)] = NDTCell2d{mean, cov};
85  }
86  sparse_grid_2d_t grid{std::move(map), 1.0};
87 
88  constexpr double kMinimumLikelihood = 1e-6;
89  const NDTModelParam2d param{kMinimumLikelihood};
90  const NDTSensorModel model{param, std::move(grid)};
91 
92  EXPECT_DOUBLE_EQ(model.likelihood_at({{0.5, 0.5}, get_diagonal_covariance_2d()}), 1.3678794411714423);
93  EXPECT_DOUBLE_EQ(model.likelihood_at({{0.8, 0.5}, get_diagonal_covariance_2d()}), 1.4307317817730123);
94  EXPECT_DOUBLE_EQ(model.likelihood_at({{0.5, 0.8}, get_diagonal_covariance_2d()}), 1.4200370805919718);
95 
96  EXPECT_DOUBLE_EQ(model.likelihood_at({{1.5, 1.5}, get_diagonal_covariance_2d()}), 1.3246524673583497);
97  EXPECT_DOUBLE_EQ(model.likelihood_at({{1.8, 1.5}, get_diagonal_covariance_2d()}), 1.1859229670198237);
98  EXPECT_DOUBLE_EQ(model.likelihood_at({{1.5, 1.8}, get_diagonal_covariance_2d()}), 1.1669230426687498);
99 }
100 
101 TEST(NDTSensorModel2DTests, FitPoints) {
102  {
103  const std::vector meas{
104  Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.1, 0.2},
105  Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.1, 0.2},
106  };
107  auto cell = detail::fit_points(meas);
108  ASSERT_TRUE(cell.mean.isApprox(Eigen::Vector2d{0.1, 0.2}));
109  // We introduce a minimum variance to avoid numeric errors down the line.
110  ASSERT_FALSE(cell.covariance.isZero());
111  }
112  {
113  const std::vector meas{
114  Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.1, 0.9}, Eigen::Vector2d{0.1, 0.2},
115  Eigen::Vector2d{0.1, 0.9}, Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.1, 0.2},
116  };
117  auto cell = detail::fit_points(meas);
118  ASSERT_TRUE(cell.mean.isApprox(Eigen::Vector2d{0.1, 0.433333}, 1e-6));
119  ASSERT_FALSE(cell.covariance.isZero());
120  ASSERT_GT(cell.covariance(1, 1), cell.covariance(0, 0));
121  }
122 }
123 
124 TEST(NDTSensorModel2DTests, ToCellsNotEnoughPointsInCell) {
125  constexpr double kMapResolution = 0.5;
126  const std::vector map_data{
127  Eigen::Vector2d{0.1, 0.2},
128  Eigen::Vector2d{0.112, 0.22},
129  Eigen::Vector2d{0.15, 0.23},
130  };
131  const auto cells = detail::to_cells(map_data, kMapResolution);
132  ASSERT_EQ(cells.size(), 0UL);
133 }
134 
135 TEST(NDTSensorModel3DTests, ToCellsNotEnoughPointsInCell) {
136  constexpr double kMapResolution = 0.5;
137  const std::vector map_data{
138  Eigen::Vector3d{0.1, 0.2, 0.0},
139  Eigen::Vector3d{0.112, 0.22, 0.0},
140  Eigen::Vector3d{0.15, 0.23, 0.0},
141  };
142  const auto cells = detail::to_cells(map_data, kMapResolution);
143  ASSERT_EQ(cells.size(), 0UL);
144 }
145 
146 TEST(NDTSensorModel3DTests, FitPoints) {
147  {
148  const std::vector meas{
149  Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.2, 0.3},
150  Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.2, 0.3},
151  };
152  auto cell = detail::fit_points(meas);
153  ASSERT_TRUE(cell.mean.isApprox(Eigen::Vector3d{0.1, 0.2, 0.3}));
154  // We introduce a minimum variance to avoid numeric errors down the line.
155  ASSERT_FALSE(cell.covariance.isZero());
156  }
157  {
158  const std::vector meas{
159  Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.2, 0.5},
160  Eigen::Vector3d{0.1, 0.2, 0.3}, Eigen::Vector3d{0.1, 0.9, 0.4}};
161  auto cell = detail::fit_points(meas);
162  ASSERT_TRUE(cell.mean.isApprox(Eigen::Vector3d{0.1, 0.34, 0.36}, 1e-6));
163  ASSERT_FALSE(cell.covariance.isZero());
164  ASSERT_GT(cell.covariance(1, 1), cell.covariance(0, 0));
165  ASSERT_GT(cell.covariance(1, 1), cell.covariance(2, 2));
166  ASSERT_GT(cell.covariance(2, 2), cell.covariance(0, 0));
167  }
168 }
169 
170 TEST(NDTSensorModel3DTests, SensorModel) {
171  constexpr double kMapResolution = 0.5;
172  const std::vector map_data{
173  Eigen::Vector3d{0.1, 0.2, 0.0}, Eigen::Vector3d{0.112, 0.22, 0.1}, Eigen::Vector3d{0.15, 0.23, 0.1},
174  Eigen::Vector3d{0.1, 0.24, 0.1}, Eigen::Vector3d{0.16, 0.25, 0.1}, Eigen::Vector3d{0.1, 0.26, 0.0},
175  };
176  auto cells = detail::to_cells(map_data, kMapResolution);
177 
178  typename sparse_grid_3d_t::map_type map_cells_data;
179 
180  for (const auto& cell : cells) {
181  map_cells_data[(cell.mean.array() / kMapResolution).floor().cast<int>()] = cell;
182  }
183 
184  std::vector perfect_measurement = map_data;
185  const NDTSensorModel model{{}, sparse_grid_3d_t{map_cells_data, kMapResolution}};
186  auto state_weighing_fn = model(std::move(perfect_measurement));
187 
188  {
189  // This section of the test makes sure we're matching 2D sensor model in the planar movement case.
190  // This is a perfect hit, so we should expect weight to be 1 + num_cells == 2.
191  ASSERT_DOUBLE_EQ(state_weighing_fn(Sophus::SE3d{}), 2);
192 
193  // Subtle miss, this value should be close to 1 but not quite.
194  ASSERT_GT(state_weighing_fn(Sophus::SE3d{Sophus::SO3d{}, Eigen::Vector3d{0.1, 0.1, 0.0}}), 1.0);
195 
196  // This is a perfect miss, so we should expect weight to be 1.
197  ASSERT_DOUBLE_EQ(state_weighing_fn(Sophus::SE3d{Sophus::SO3d{}, Eigen::Vector3d{-10, -10, 0.0}}), 1.0);
198  }
199 
200  // This is a perfect miss, so we should expect weight to be 1.
201  ASSERT_DOUBLE_EQ(state_weighing_fn(Sophus::SE3d::rotZ(3.14)), 1);
202 
203  // Subtle miss, this value should be close to 1 but not quite.
204  ASSERT_GT(state_weighing_fn(Sophus::SE3d{Sophus::SO3d{}, Eigen::Vector3d{0.0, 0.0, 0.1}}), 1.0);
205 
206  // Subtle miss, this value should be close to 1 but not quite.
207  ASSERT_GT(state_weighing_fn(Sophus::SE3d{Sophus::SO3d{}, Eigen::Vector3d{0.0, 0.0, -0.1}}), 1.0);
208 
209  // This is a perfect miss, so we should expect weight to be 1.
210  ASSERT_DOUBLE_EQ(state_weighing_fn(Sophus::SE3d{Sophus::SO3d{}, Eigen::Vector3d{0, 0, 1.0}}), 1.0);
211 }
212 
213 TEST(NDTSensorModel2DTests, SensorModel) {
214  constexpr double kMapResolution = 0.5;
215  const std::vector map_data{
216  Eigen::Vector2d{0.1, 0.2}, Eigen::Vector2d{0.112, 0.22}, Eigen::Vector2d{0.15, 0.23},
217  Eigen::Vector2d{0.1, 0.24}, Eigen::Vector2d{0.16, 0.25}, Eigen::Vector2d{0.1, 0.26},
218  };
219  auto cells = detail::to_cells(map_data, kMapResolution);
220 
221  typename sparse_grid_2d_t::map_type map_cells_data;
222  for (const auto& cell : cells) {
223  map_cells_data[(cell.mean.array() / kMapResolution).cast<int>()] = cell;
224  }
225  std::vector perfect_measurement = map_data;
226  const NDTSensorModel model{{}, sparse_grid_2d_t{map_cells_data, kMapResolution}};
227  auto state_weighing_fn = model(std::move(perfect_measurement));
228  // This is a perfect hit, so we should expect weight to be 1 + num_cells == 2.
229  ASSERT_DOUBLE_EQ(state_weighing_fn(Sophus::SE2d{}), 2);
230 
231  // Subtle miss, this value should be close to 1 but not quite.
232  ASSERT_GT(state_weighing_fn(Sophus::SE2d{Sophus::SO2d{}, Eigen::Vector2d{0.1, 0.1}}), 1.0);
233 
234  // This is a perfect miss, so we should expect weight to be 1.
235  ASSERT_DOUBLE_EQ(state_weighing_fn(Sophus::SE2d{Sophus::SO2d{}, Eigen::Vector2d{-10, -10}}), 1.0);
236 }
237 
238 TEST(NDTSensorModel2DTests, LoadFromHDF5HappyPath) {
239  const auto ndt_map_representation = io::load_from_hdf5<sparse_grid_2d_t>("./test_data/turtlebot3_world.hdf5");
240  ASSERT_EQ(ndt_map_representation.size(), 30UL);
241 }
242 
243 TEST(NDTSensorModel2DTests, LoadFromHDF5NonExistingFile) {
244  ASSERT_THROW(io::load_from_hdf5<sparse_grid_2d_t>("bad_file.hdf5"), std::invalid_argument);
245 }
246 
247 TEST(NDTSensorModel3DTests, LoadFromHDF5NonExistingFile) {
248  ASSERT_THROW(io::load_from_hdf5<sparse_grid_3d_t>("bad_file.hdf5"), std::invalid_argument);
249 }
250 
251 TEST(NDTSensorModel3DTests, LoadFromHDF5HappyPath) {
252  const auto ndt_map_representation = io::load_from_hdf5<sparse_grid_3d_t>("./test_data/sample_3d_ndt_map.hdf5");
253  ASSERT_EQ(ndt_map_representation.size(), 398);
254 }
255 
256 } // namespace beluga
Sophus::SO2
Sophus::SO3
ndt_sensor_model.hpp
beluga::NDTCell
Representation for a cell of a N dimensional NDT cell.
Definition: ndt_cell.hpp:36
beluga::TEST
TEST(NDTSensorModel3DTests, LoadFromHDF5HappyPath)
Definition: test_ndt_model.cpp:251
Sophus::SE3::rotZ
static SOPHUS_FUNC SE3 rotZ(Scalar const &z)
se2.hpp
beluga::detail::fit_points
NDTCell< NDim, Scalar > fit_points(const std::vector< Eigen::Vector< Scalar, NDim >> &points)
Fit a vector of points to an NDT cell, by computing its mean and covariance.
Definition: ndt_sensor_model.hpp:60
Sophus::SE2
Sophus::SE3
beluga::NDTSensorModel
NDT sensor model for range finders.
Definition: ndt_sensor_model.hpp:172
beluga::detail::to_cells
std::vector< NDTCell< NDim, Scalar > > to_cells(const std::vector< Eigen::Vector< Scalar, NDim >> &points, const double resolution)
Definition: ndt_sensor_model.hpp:81
beluga::NDTModelParam
Parameters used to construct a NDTSensorModel instance.
Definition: ndt_sensor_model.hpp:147
sparse_value_grid.hpp
Implementation of generic sparse value regular grid.
ndt_cell.hpp
beluga
The main Beluga namespace.
Definition: 3d_embedding.hpp:21
beluga::TEST
TEST(Bresenham, MultiPassGuarantee)
Definition: test_bresenham.cpp:27


beluga
Author(s):
autogenerated on Tue Jul 16 2024 02:59:53