test_differential_drive_model.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 <cmath>
19 #include <functional>
20 #include <random>
21 #include <sophus/se3.hpp>
22 #include <tuple>
23 #include <utility>
24 
25 #include <Eigen/Core>
26 #include <sophus/common.hpp>
27 #include <sophus/se2.hpp>
28 #include <sophus/so2.hpp>
29 
30 #include <range/v3/view/common.hpp>
31 #include <range/v3/view/generate.hpp>
32 #include <range/v3/view/take_exactly.hpp>
33 
34 #include "beluga/3d_embedding.hpp"
37 
38 namespace {
39 
40 using Constants = Sophus::Constants<double>;
41 using Eigen::Vector2d;
42 using Sophus::SE2d;
43 using Sophus::SO2d;
44 
46 
48 
49 class DifferentialDriveModelTest : public ::testing::Test {
50  protected:
51  const UUT motion_model_{beluga::DifferentialDriveModelParam{0.0, 0.0, 0.0, 0.0}}; // No variance
52  std::mt19937 generator_{std::random_device()()};
53 };
54 
55 TEST_F(DifferentialDriveModelTest, OneUpdate) {
56  constexpr double kTolerance = 0.001;
57  const auto control_action = std::make_tuple(
58  SE2d{SO2d{Constants::pi()}, Vector2d{1.0, -2.0}}, SE2d{SO2d{Constants::pi()}, Vector2d{1.0, -2.0}});
59  const auto state_sampling_function = motion_model_(control_action);
60  const auto pose = SE2d{SO2d{Constants::pi() / 3}, Vector2d{2.0, 5.0}};
61  ASSERT_THAT(state_sampling_function(pose, generator_), SE2Near(pose, kTolerance));
62 }
63 
64 TEST_F(DifferentialDriveModelTest, Translate) {
65  constexpr double kTolerance = 0.001;
66  const auto control_action = std::make_tuple(SE2d{SO2d{0.0}, Vector2d{1.0, 0.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
67  const auto state_sampling_function = motion_model_(control_action);
68 
69  const auto result1 = state_sampling_function(SE2d{SO2d{0.0}, Vector2d{2.0, 0.0}}, generator_);
70  ASSERT_THAT(result1, SE2Near(SO2d{0.0}, Vector2d{3.0, 0.0}, kTolerance));
71  const auto result2 = state_sampling_function(SE2d{SO2d{0.0}, Vector2d{0.0, 3.0}}, generator_);
72  ASSERT_THAT(result2, SE2Near(SO2d{0.0}, Vector2d{1.0, 3.0}, kTolerance));
73 }
74 
75 TEST_F(DifferentialDriveModelTest, RotateTranslate) {
76  constexpr double kTolerance = 0.001;
77  const auto control_action =
78  std::make_tuple(SE2d{SO2d{Constants::pi() / 2}, Vector2d{0.0, 1.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
79  const auto state_sampling_function = motion_model_(control_action);
80 
81  const auto result1 = state_sampling_function(SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}}, generator_);
82  ASSERT_THAT(result1, SE2Near(SO2d{Constants::pi() / 2}, Vector2d{0.0, 1.0}, kTolerance));
83  const auto result2 = state_sampling_function(SE2d{SO2d{-Constants::pi() / 2}, Vector2d{2.0, 3.0}}, generator_);
84  ASSERT_THAT(result2, SE2Near(SO2d{0.0}, Vector2d{3.0, 3.0}, kTolerance));
85 }
86 
87 TEST_F(DifferentialDriveModelTest, Rotate) {
88  constexpr double kTolerance = 0.001;
89  const auto control_action =
90  std::make_tuple(SE2d{SO2d{Constants::pi() / 4}, Vector2d{0.0, 0.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
91  const auto state_sampling_function = motion_model_(control_action);
92  const auto result1 = state_sampling_function(SE2d{SO2d{Constants::pi()}, Vector2d{0.0, 0.0}}, generator_);
93  ASSERT_THAT(result1, SE2Near(SO2d{Constants::pi() * 5 / 4}, Vector2d{0.0, 0.0}, kTolerance));
94  const auto result2 = state_sampling_function(SE2d{SO2d{-Constants::pi() / 2}, Vector2d{0.0, 0.0}}, generator_);
95  ASSERT_THAT(result2, SE2Near(SO2d{-Constants::pi() / 4}, Vector2d{0.0, 0.0}, kTolerance));
96 }
97 
98 TEST_F(DifferentialDriveModelTest, RotateTranslateRotate) {
99  constexpr double kTolerance = 0.001;
100  const auto control_action =
101  std::make_tuple(SE2d{SO2d{-Constants::pi() / 2}, Vector2d{1.0, 2.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
102  const auto state_sampling_function = motion_model_(control_action);
103  const auto result = state_sampling_function(SE2d{SO2d{Constants::pi()}, Vector2d{3.0, 4.0}}, generator_);
104  ASSERT_THAT(result, SE2Near(SO2d{Constants::pi() / 2}, Vector2d{2.0, 2.0}, kTolerance));
105 }
106 
107 template <class Range>
108 auto get_statistics(Range&& range) {
109  const auto size = static_cast<double>(std::distance(std::begin(range), std::end(range)));
110  const double sum = std::accumulate(std::begin(range), std::end(range), 0.0);
111  const double mean = sum / size;
112  const double squared_diff_sum =
113  std::transform_reduce(std::begin(range), std::end(range), 0.0, std::plus<>{}, [mean](double value) {
114  const double diff = value - mean;
115  return diff * diff;
116  });
117  const double stddev = std::sqrt(squared_diff_sum / size);
118  return std::pair{mean, stddev};
119 }
120 
121 TEST(DifferentialDriveModelSamples, Translate) {
122  const double tolerance = 0.015;
123  const double alpha = 0.2;
124  const double origin = 5.0;
125  const double distance = 3.0;
126  const auto motion_model = UUT{beluga::DifferentialDriveModelParam{0.0, 0.0, alpha, 0.0}}; // Translation variance
127  auto generator = std::mt19937{std::random_device()()};
128  const auto control_action =
129  std::make_tuple(SE2d{SO2d{0.0}, Vector2d{distance, 0.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
130  const auto state_sampling_function = motion_model(control_action);
131  auto view = ranges::views::generate([&]() {
132  const auto pose = SE2d{SO2d{0.0}, Vector2d{origin, 0.0}};
133  return state_sampling_function(pose, generator).translation().x();
134  }) |
135  ranges::views::take_exactly(100'000) | ranges::views::common;
136  const auto [mean, stddev] = get_statistics(view);
137  ASSERT_NEAR(mean, origin + distance, tolerance);
138  ASSERT_NEAR(stddev, std::sqrt(alpha * distance * distance), tolerance);
139 }
140 
141 TEST(DifferentialDriveModelSamples, RotateFirstQuadrant) {
142  const double tolerance = 0.01;
143  const double alpha = 0.2;
144  const double initial_angle = Constants::pi() / 6;
145  const double motion_angle = Constants::pi() / 4;
146  const auto motion_model = UUT{beluga::DifferentialDriveModelParam{alpha, 0.0, 0.0, 0.0}}; // Rotation variance
147  auto generator = std::mt19937{std::random_device()()};
148  const auto control_action =
149  std::make_tuple(SE2d{SO2d{motion_angle}, Vector2d{0.0, 0.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
150  const auto state_sampling_function = motion_model(control_action);
151  auto view = ranges::views::generate([&]() {
152  const auto pose = SE2d{SO2d{initial_angle}, Vector2d{0.0, 0.0}};
153  return state_sampling_function(pose, generator).so2().log();
154  }) |
155  ranges::views::take_exactly(100'000) | ranges::views::common;
156  const auto [mean, stddev] = get_statistics(view);
157  ASSERT_NEAR(mean, initial_angle + motion_angle, tolerance);
158  ASSERT_NEAR(stddev, std::sqrt(alpha * motion_angle * motion_angle), tolerance);
159 }
160 
161 TEST(DifferentialDriveModel3DSamples, RotateFirstQuadrant) {
162  // Demonstrate 3D planar diff drive model.
163  using beluga::To3d;
164  const double tolerance = 0.01;
165  const double alpha = 0.2;
166  const double initial_angle = Constants::pi() / 6;
167  const double motion_angle = Constants::pi() / 4;
168  const auto motion_model = beluga::DifferentialDriveModel<Sophus::SE3d>{
169  beluga::DifferentialDriveModelParam{alpha, 0.0, 0.0, 0.0}}; // Rotation variance
170  auto generator = std::mt19937{std::random_device()()};
171  const auto control_action =
172  std::make_tuple(To3d(SE2d{SO2d{motion_angle}, Vector2d{0.0, 0.0}}), To3d(SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}}));
173  const auto state_sampling_function = motion_model(control_action);
174  auto view = ranges::views::generate([&]() {
175  const auto pose = To3d(SE2d{SO2d{initial_angle}, Vector2d{0.0, 0.0}});
176  return state_sampling_function(pose, generator).angleZ();
177  }) |
178  ranges::views::take_exactly(100'000) | ranges::views::common;
179  const auto [mean, stddev] = get_statistics(view);
180  ASSERT_NEAR(mean, initial_angle + motion_angle, tolerance);
181  ASSERT_NEAR(stddev, std::sqrt(alpha * motion_angle * motion_angle), tolerance);
182 }
183 
184 TEST(DifferentialDriveModelSamples, RotateThirdQuadrant) {
185  const double tolerance = 0.01;
186  const double alpha = 0.2;
187  const double initial_angle = Constants::pi() / 6;
188  const double motion_angle = -Constants::pi() * 3 / 4;
189  const auto motion_model = UUT{beluga::DifferentialDriveModelParam{alpha, 0.0, 0.0, 0.0}}; // Rotation variance
190  auto generator = std::mt19937{std::random_device()()};
191  const auto control_action =
192  std::make_tuple(SE2d{SO2d{motion_angle}, Vector2d{0.0, 0.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
193  const auto state_sampling_function = motion_model(control_action);
194  auto view = ranges::views::generate([&]() {
195  const auto pose = SE2d{SO2d{initial_angle}, Vector2d{0.0, 0.0}};
196  return state_sampling_function(pose, generator).so2().log();
197  }) |
198  ranges::views::take_exactly(100'000) | ranges::views::common;
199  const auto [mean, stddev] = get_statistics(view);
200  ASSERT_NEAR(mean, initial_angle + motion_angle, tolerance);
201 
202  // Treat backward and forward motion symmetrically for the noise models.
203  const double flipped_angle = Constants::pi() + motion_angle;
204  ASSERT_NEAR(stddev, std::sqrt(alpha * flipped_angle * flipped_angle), tolerance);
205 }
206 
207 TEST(DifferentialDriveModelSamples, RotateTranslateRotateFirstQuadrant) {
208  const double tolerance = 0.01;
209  const double alpha = 0.2;
210  const auto motion_model =
211  UUT{beluga::DifferentialDriveModelParam{0.0, 0.0, 0.0, alpha}}; // Translation variance from rotation
212  auto generator = std::mt19937{std::random_device()()};
213  const auto control_action = std::make_tuple(SE2d{SO2d{0.0}, Vector2d{1.0, 1.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
214  const auto state_sampling_function = motion_model(control_action);
215  auto view = ranges::views::generate([&]() {
216  const auto pose = SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}};
217  return state_sampling_function(pose, generator).translation().norm();
218  }) |
219  ranges::views::take_exactly(100'000) | ranges::views::common;
220  const auto [mean, stddev] = get_statistics(view);
221  ASSERT_NEAR(mean, 1.41, tolerance);
222 
223  // Net rotation is zero comparing final and initial poses, but
224  // the model requires a 45 degree counter-clockwise rotation,
225  // a forward translation, and a 45 degree clockwise rotation.
226  const double first_rotation = Constants::pi() / 4;
227  const double second_rotation = first_rotation;
228  const double rotation_variance = (first_rotation * first_rotation) + (second_rotation * second_rotation);
229  ASSERT_NEAR(stddev, std::sqrt(alpha * rotation_variance), tolerance);
230 }
231 
232 TEST(DifferentialDriveModelSamples, RotateTranslateRotateThirdQuadrant) {
233  const double tolerance = 0.01;
234  const double alpha = 0.2;
235  const auto motion_model =
236  UUT{beluga::DifferentialDriveModelParam{0.0, 0.0, 0.0, alpha}}; // Translation variance from rotation
237  auto generator = std::mt19937{std::random_device()()};
238  const auto control_action =
239  std::make_tuple(SE2d{SO2d{0.0}, Vector2d{-1.0, -1.0}}, SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}});
240  const auto state_sampling_function = motion_model(control_action);
241  auto view = ranges::views::generate([&]() {
242  const auto pose = SE2d{SO2d{0.0}, Vector2d{0.0, 0.0}};
243  return state_sampling_function(pose, generator).translation().norm();
244  }) |
245  ranges::views::take_exactly(100'000) | ranges::views::common;
246  const auto [mean, stddev] = get_statistics(view);
247  ASSERT_NEAR(mean, 1.41, 0.01);
248 
249  // Net rotation is zero comparing final and initial poses, but
250  // the model requires a 45 degree counter-clockwise rotation,
251  // a backward translation and a 45 degree clockwise rotation.
252  const double first_rotation = Constants::pi() / 4;
253  const double second_rotation = first_rotation;
254  const double rotation_variance = (first_rotation * first_rotation) + (second_rotation * second_rotation);
255  ASSERT_NEAR(stddev, std::sqrt(alpha * rotation_variance), tolerance);
256 }
257 
258 } // namespace
result
result[0]
kTolerance
double kTolerance
Definition: test_cluster_based_estimation.cpp:47
beluga::To3d
Sophus::SE3d To3d(const Sophus::SE2d &tf)
Embed a SE2 transform into 3D space with zero Z translation and only rotation about the Z axis.
Definition: 3d_embedding.hpp:29
beluga::testing::SE2Near
auto SE2Near(const Sophus::SE2< Scalar > &t, Scalar e)
SE2 element matcher.
Definition: sophus_matchers.hpp:83
beluga::UUT
beluga::BeamSensorModel< StaticOccupancyGrid< 5, 5 > > UUT
Definition: test_beam_model.cpp:27
beluga::DifferentialDriveModelParam
Parameters to construct a DifferentialDriveModel instance.
Definition: differential_drive_model.hpp:40
se2.hpp
beluga::DifferentialDriveModel2d
DifferentialDriveModel< Sophus::SE2d > DifferentialDriveModel2d
Alias for a 2D differential drive model, for convinience.
Definition: differential_drive_model.hpp:177
common.hpp
3d_embedding.hpp
Sophus::SE2
Vector2d
Vector2< double > Vector2d
so2.hpp
beluga::DifferentialDriveModel
Sampled odometry model for a differential drive.
Definition: differential_drive_model.hpp:80
sophus_matchers.hpp
Implements GTest matchers for Sophus/Eigen types.
Sophus::Constants
Sophus::SE2::translation
SOPHUS_FUNC TranslationMember & translation()
Sophus::SE2d
SE2< double > SE2d
differential_drive_model.hpp
Implementation of a differential drive odometry motion model.
Sophus::SO2d
SO2< double > SO2d
beluga::TEST
TEST(Bresenham, MultiPassGuarantee)
Definition: test_bresenham.cpp:27
se3.hpp


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