test_estimation.cpp
Go to the documentation of this file.
1 // Copyright 2022-2023 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 <gmock/gmock.h>
16 #include <gtest/gtest.h>
17 
18 #include <algorithm>
19 #include <limits>
20 #include <numeric>
21 #include <vector>
22 
23 #include <Eigen/Core>
24 #include <sophus/common.hpp>
25 #include <sophus/se2.hpp>
26 #include <sophus/so2.hpp>
27 
30 
31 namespace {
32 
35 
36 using Constants = Sophus::Constants<double>;
37 using Eigen::Vector2d;
38 using Sophus::SE2d;
39 using Sophus::SO2d;
40 
41 struct CovarianceCalculation : public testing::Test {};
42 
43 TEST_F(CovarianceCalculation, UniformWeightOverload) {
44  // Covariance matrix calculated for items with uniform weights.
45  // The following Octave code was used to validate the results:
46  /*
47  translations = [ 0 2 0 2 0 2 0 2 0 2; 0 2 0 2 0 0 2 2 2 0 ]';
48  cov_matrix = cov(translations)
49  */
50  const auto translation_vector = std::vector{
51  Vector2d{0, 0}, Vector2d{2, 2}, Vector2d{0, 0}, Vector2d{2, 2}, Vector2d{0, 0},
52  Vector2d{2, 0}, Vector2d{0, 2}, Vector2d{2, 2}, Vector2d{0, 2}, Vector2d{2, 0},
53  };
54  const auto translation_mean = Vector2d{1, 1};
55  const auto covariance = beluga::calculate_covariance(translation_vector, translation_mean);
56  ASSERT_NEAR(covariance(0, 0), 1.1111, 0.001);
57  ASSERT_NEAR(covariance(0, 1), 0.2222, 0.001);
58  ASSERT_NEAR(covariance(1, 0), 0.2222, 0.001);
59  ASSERT_NEAR(covariance(1, 1), 1.1111, 0.001);
60 }
61 
62 TEST_F(CovarianceCalculation, NonUniformWeightOverload) {
63  // Covariance matrix calculated with non-uniform weights.
64  // The following Octave code was used to validate the results:
65  /*
66  translations = [ 0 2 0 2 0 2 0 2 0 2; 0 2 0 2 0 0 2 2 2 0 ]';
67  weights = [0 1 2 1 0 1 2 1 0 1]';
68  normalized_weight = weights ./ sum(weights);
69  weighted_mean = sum(normalized_weight .* translations)
70  deviations = translations - weighted_mean;
71  weighted_cov_matrix = (normalized_weight .* deviations)' * deviations ./ (1 - sum(normalized_weight.^2))
72  */
73  const auto translation_vector = std::vector{
74  Vector2d{0, 0}, Vector2d{2, 2}, Vector2d{0, 0}, Vector2d{2, 2}, Vector2d{0, 0},
75  Vector2d{2, 0}, Vector2d{0, 2}, Vector2d{2, 2}, Vector2d{0, 2}, Vector2d{2, 0},
76  };
77  auto weights = std::vector{0.0, 1.0, 2.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 1.0};
78  const auto total_weight = std::accumulate(weights.begin(), weights.end(), 0.0);
79  std::for_each(weights.begin(), weights.end(), [total_weight](auto& weight) { weight /= total_weight; });
80  const auto translation_mean = Vector2d{1.1111, 1.1111};
81  const auto covariance = beluga::calculate_covariance(translation_vector, weights, translation_mean);
82  ASSERT_NEAR(covariance(0, 0), 1.1765, 0.001);
83  ASSERT_NEAR(covariance(0, 1), 0.1176, 0.001);
84  ASSERT_NEAR(covariance(1, 0), 0.1176, 0.001);
85  ASSERT_NEAR(covariance(1, 1), 1.1765, 0.001);
86 }
87 
88 struct PoseCovarianceEstimation : public testing::Test {
89  // The following Octave code can be used to validate the results in the tests that derive from this fixture:
90  /*
91  # inputs
92  translations = [ x1 y1 rot1; x2 y2 rot2; x3 y3 yaw3; ... xn yn rotn ] ;
93  weights = [w1 w2 w3 ... wn]; ]';
94 
95  # auxiliar variables
96  xy_translation = translations(:, 1:2);
97  complex_rotation = exp(i* translations(:, 3));
98  normalized_weight = weights ./ sum(weights);
99  complex_rotation_mean = sum(normalized_weight .* complex_rotation);
100  R = abs(complex_rotation_mean);
101  # mean estimations
102  xy_mean = sum(normalized_weight .* xy_translation);
103  rot_mean = imag(log(complex_rotation_mean / abs(complex_rotation_mean)));
104  # covariance estimations
105  xy_deviations = xy_translation - xy_mean;
106  xy_cov_matrix = (normalized_weight .* xy_deviations)' * xy_deviations ./ (1 - sum(normalized_weight.^2));
107  rot_cov = -2 * log(R);
108  # results
109  means = [ xy_mean, rot_mean ]
110  covariance_matrix = [xy_cov_matrix, [0; 0]; [0 0], rot_cov]
111  */
112 };
113 
114 TEST_F(PoseCovarianceEstimation, PureTranslation) {
115  // test the mean and covariance estimations for states that have different translations but the same rotation
116  const auto states = std::vector{SE2d{SO2d{0.0}, Vector2d{1.0, 2.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}}};
117  const auto weights = std::vector(states.size(), 1.0);
118  constexpr double kTolerance = 0.001;
119  const auto [pose, covariance] = beluga::estimate(states, weights);
120  ASSERT_THAT(pose, SE2Near(SO2d{0.0}, Vector2d{0.5, 1.0}, kTolerance));
121  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({0.5, 1.0, 0.0}, kTolerance));
122  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({1.0, 2.0, 0.0}, kTolerance));
123  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.0, 0.0, 0.0}, kTolerance));
124 }
125 
126 TEST_F(PoseCovarianceEstimation, PureRotation) {
127  // test the mean and covariance estimations for states that have different rotations but the same translation
128  const auto states =
129  std::vector{SE2d{SO2d{-Constants::pi() / 2}, Vector2d{0.0, 0.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}}};
130  const auto weights = std::vector(states.size(), 1.0);
131  constexpr double kTolerance = 0.001;
132  const auto [pose, covariance] = beluga::estimate(states, weights);
133  ASSERT_THAT(pose, SE2Near(SO2d{-Constants::pi() / 4}, Vector2d{0.0, 0.0}, kTolerance));
134  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({0.000, 0.000, 0.000}, kTolerance));
135  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({0.000, 0.000, 0.000}, kTolerance));
136  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.000, 0.000, 0.693}, kTolerance));
137 }
138 
139 TEST_F(PoseCovarianceEstimation, JointTranslationAndRotation) {
140  // test the mean and covariance estimations for states that have different translations and rotations
141  const auto states = std::vector{
142  SE2d{SO2d{Constants::pi() / 6}, Vector2d{0.0, -3.0}}, SE2d{SO2d{Constants::pi() / 2}, Vector2d{1.0, -2.0}},
143  SE2d{SO2d{Constants::pi() / 3}, Vector2d{2.0, -1.0}}, SE2d{SO2d{0.0}, Vector2d{3.0, 0.0}}};
144  const auto weights = std::vector(states.size(), 1.0);
145  constexpr double kTolerance = 0.001;
146  const auto [pose, covariance] = beluga::estimate(states, weights);
147  ASSERT_THAT(pose, SE2Near(SO2d{Constants::pi() / 4}, Vector2d{1.5, -1.5}, kTolerance));
148  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({1.666, 1.666, 0.000}, kTolerance));
149  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({1.666, 1.666, 0.000}, kTolerance));
150  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.000, 0.000, 0.357}, kTolerance));
151 }
152 
153 TEST_F(PoseCovarianceEstimation, CancellingOrientations) {
154  // test mean and covariance for two states with opposite angles that cause a singularity in angular covariance
155  // estimation
156  const auto states = std::vector{
157  SE2d{SO2d{Constants::pi() / 2}, Vector2d{0.0, 0.0}}, SE2d{SO2d{-Constants::pi() / 2}, Vector2d{0.0, 0.0}}};
158  const auto weights = std::vector(states.size(), 1.0);
159  constexpr double kTolerance = 0.001;
160  constexpr double kInf = std::numeric_limits<double>::infinity();
161  const auto [pose, covariance] = beluga::estimate(states, weights);
162  ASSERT_THAT(pose, SE2Near(SO2d{0.0}, Vector2d{0.0, 0.0}, kTolerance));
163  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({0.0, 0.0, 0.0}, kTolerance));
164  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({0.0, 0.0, 0.0}, kTolerance));
165  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.0, 0.0, kInf}, kTolerance));
166  ASSERT_EQ(covariance(2, 2), std::numeric_limits<double>::infinity());
167 }
168 
169 TEST_F(PoseCovarianceEstimation, RandomWalkWithSmoothRotationWithUniformWeights) {
170  // test the mean and covariance estimations for states with random variations of translation and rotation
171  const auto states = std::vector{
172  SE2d{SO2d{Constants::pi() * 0.1}, Vector2d{0.0, -2.0}}, //
173  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{1.0, -1.0}}, //
174  SE2d{SO2d{Constants::pi() * 0.3}, Vector2d{2.0, 1.0}}, //
175  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{3.0, 2.0}}, //
176  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{2.0, 1.0}}, //
177  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{1.0, -1.0}}, //
178  SE2d{SO2d{Constants::pi() * 0.3}, Vector2d{2.0, -2.0}}, //
179  SE2d{SO2d{Constants::pi() * 0.4}, Vector2d{3.0, -1.0}}, //
180  SE2d{SO2d{Constants::pi() * 0.5}, Vector2d{2.0, 1.0}}, //
181  SE2d{SO2d{Constants::pi() * 0.4}, Vector2d{1.0, 2.0}}, //
182  };
183  const auto weights = std::vector(states.size(), 1.0);
184  constexpr double kTolerance = 0.001;
185  const auto [pose, covariance] = beluga::estimate(states, weights);
186  ASSERT_THAT(pose, SE2Near(SO2d{0.8762}, Vector2d{1.700, 0.0}, kTolerance));
187  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({0.9000, 0.5556, 0.0000}, kTolerance));
188  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({0.5556, 2.4444, 0.0000}, kTolerance));
189  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.0000, 0.0000, 0.1355}, kTolerance));
190 }
191 
192 TEST_F(PoseCovarianceEstimation, WeightsCanSingleOutOneSample) {
193  // test the weights have effect by selecting a few states and ignoring others
194  const auto states = std::vector{
195  SE2d{SO2d{Constants::pi() / 6}, Vector2d{0.0, -3.0}}, //
196  SE2d{SO2d{Constants::pi() / 2}, Vector2d{1.0, -2.0}}, //
197  SE2d{SO2d{Constants::pi() / 3}, Vector2d{2.0, -1.0}}, //
198  SE2d{SO2d{Constants::pi() / 2}, Vector2d{1.0, -2.0}}, //
199  };
200  const auto weights = std::vector{0.0, 1.0, 0.0, 1.0};
201  constexpr double kTolerance = 0.001;
202  const auto [pose, covariance] = beluga::estimate(states, weights);
203  ASSERT_THAT(pose, SE2Near(SO2d{Constants::pi() / 2}, Vector2d{1.0, -2.0}, kTolerance));
204  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({0.0, 0.0, 0.0}, kTolerance));
205  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({0.0, 0.0, 0.0}, kTolerance));
206  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.0, 0.0, 0.0}, kTolerance));
207 }
208 
209 TEST_F(PoseCovarianceEstimation, RandomWalkWithSmoothRotationAndNonUniformWeights) {
210  // test the mean and covariance estimations for states with random variations of translation, rotation and weights
211  const auto states = std::vector{
212  SE2d{SO2d{Constants::pi() * 0.1}, Vector2d{0.0, -2.0}}, //
213  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{1.0, -1.0}}, //
214  SE2d{SO2d{Constants::pi() * 0.3}, Vector2d{2.0, 1.0}}, //
215  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{3.0, 2.0}}, //
216  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{2.0, 1.0}}, //
217  SE2d{SO2d{Constants::pi() * 0.2}, Vector2d{1.0, -1.0}}, //
218  SE2d{SO2d{Constants::pi() * 0.3}, Vector2d{2.0, -2.0}}, //
219  SE2d{SO2d{Constants::pi() * 0.4}, Vector2d{3.0, -1.0}}, //
220  SE2d{SO2d{Constants::pi() * 0.5}, Vector2d{2.0, 1.0}}, //
221  SE2d{SO2d{Constants::pi() * 0.4}, Vector2d{1.0, 2.0}}, //
222  };
223  const auto weights = std::vector{0.1, 0.4, 0.7, 0.1, 0.9, 0.2, 0.2, 0.4, 0.1, 0.4};
224  constexpr double kTolerance = 0.001;
225  const auto [pose, covariance] = beluga::estimate(states, weights);
226  ASSERT_THAT(pose, SE2Near(SO2d{0.8687}, Vector2d{1.800, 0.3143}, kTolerance));
227  ASSERT_THAT(covariance.col(0).eval(), Vector3Near({0.5946, 0.0743, 0.0000}, kTolerance));
228  ASSERT_THAT(covariance.col(1).eval(), Vector3Near({0.0743, 1.8764, 0.0000}, kTolerance));
229  ASSERT_THAT(covariance.col(2).eval(), Vector3Near({0.0000, 0.0000, 0.0855}, kTolerance));
230 }
231 
232 struct ScalarEstimation : public testing::Test {};
233 
234 TEST_F(ScalarEstimation, UniformWeightOverload) {
235  // Mean and variance estimation with uniform weights.
236  const auto states = std::vector{0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 7.0, 7.0, 8.0, 9.0};
237  const auto weights = std::vector(states.size(), 1.0);
238  const auto [mean, variance] = beluga::estimate(states, weights);
239  const auto standard_deviation = std::sqrt(variance);
240  constexpr double kTolerance = 0.001;
241  ASSERT_NEAR(mean, 4.266, kTolerance);
242  ASSERT_NEAR(standard_deviation, 2.763, kTolerance);
243 }
244 
245 TEST_F(ScalarEstimation, NonUniformWeightOverload) {
246  // Mean and variance estimation with non-uniform weights.
247  const auto states = std::vector{0.0, 1.0, 1.0, 2.0, 2.0, 3.0, 4.0, 4.0, 5.0, 5.0, 6.0, 7.0, 7.0, 8.0, 9.0};
248  const auto weights = std::vector{0.1, 0.15, 0.15, 0.3, 0.3, 0.4, 0.8, 0.8, 0.4, 0.4, 0.35, 0.3, 0.3, 0.15, 0.1};
249  const auto [mean, variance] = beluga::estimate(states, weights);
250  const auto standard_deviation = std::sqrt(variance);
251  constexpr double kTolerance = 0.001;
252  ASSERT_NEAR(mean, 4.300, kTolerance);
253  ASSERT_NEAR(standard_deviation, 2.026, kTolerance);
254 }
255 
256 } // namespace
kTolerance
double kTolerance
Definition: test_cluster_based_estimation.cpp:47
beluga::testing::SE2Near
auto SE2Near(const Sophus::SE2< Scalar > &t, Scalar e)
SE2 element matcher.
Definition: sophus_matchers.hpp:83
beluga::weight
constexpr weight_detail::weight_fn weight
Customization point object for accessing the weight of a particle.
Definition: primitives.hpp:264
estimation.hpp
Implementation of algorithms that allow calculating the estimated state of a particle filter.
beluga::testing::Vector3Near
auto Vector3Near(const Sophus::Vector3< Scalar > &t, Scalar e)
Vector3 element matcher.
Definition: sophus_matchers.hpp:58
se2.hpp
common.hpp
Sophus::SE2
Vector2d
Vector2< double > Vector2d
so2.hpp
sophus_matchers.hpp
Implements GTest matchers for Sophus/Eigen types.
beluga::estimate
std::pair< Sophus::SE2< Scalar >, Sophus::Matrix3< Scalar > > estimate(Poses &&poses, Weights &&weights)
Returns a pair consisting of the estimated mean pose and its covariance.
Definition: estimation.hpp:129
Sophus::Constants
beluga::views::weights
constexpr auto weights
Definition: particles.hpp:34
Sophus::SE2d
SE2< double > SE2d
Sophus::SO2d
SO2< double > SO2d
beluga::calculate_covariance
Sophus::Matrix2< Scalar > calculate_covariance(Range &&range, WeightsRange &&normalized_weights, const Sophus::Vector2< Scalar > &mean)
Calculates the covariance of a range given its mean and the weights of each element.
Definition: estimation.hpp:53
beluga::views::states
constexpr auto states
Definition: particles.hpp:30


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