blob: b811688a5cfe8f17aafc0b51acaf698c1d09f8f0 [file] [log] [blame]
James Kuszmaul1057ce82019-02-09 17:58:24 -08001#include "y2019/control_loops/drivetrain/localizer.h"
2
James Kuszmaul1057ce82019-02-09 17:58:24 -08003#include <queue>
James Kuszmaul651fc3f2019-05-15 21:14:25 -07004#include <random>
James Kuszmaul1057ce82019-02-09 17:58:24 -08005
6#include "aos/testing/random_seed.h"
7#include "aos/testing/test_shm.h"
James Kuszmaul1057ce82019-02-09 17:58:24 -08008#include "frc971/control_loops/drivetrain/splinedrivetrain.h"
James Kuszmaul651fc3f2019-05-15 21:14:25 -07009#include "frc971/control_loops/drivetrain/trajectory.h"
James Kuszmaul1057ce82019-02-09 17:58:24 -080010#include "gflags/gflags.h"
11#if defined(SUPPORT_PLOT)
12#include "third_party/matplotlib-cpp/matplotlibcpp.h"
13#endif
14#include "gtest/gtest.h"
James Kuszmaul1057ce82019-02-09 17:58:24 -080015#include "y2019/constants.h"
James Kuszmaul651fc3f2019-05-15 21:14:25 -070016#include "y2019/control_loops/drivetrain/drivetrain_base.h"
James Kuszmaul1057ce82019-02-09 17:58:24 -080017
18DEFINE_bool(plot, false, "If true, plot");
19
20namespace y2019 {
21namespace control_loops {
22namespace testing {
23
24using ::y2019::constants::Field;
25
26constexpr size_t kNumCameras = 4;
27constexpr size_t kNumTargetsPerFrame = 3;
28
29typedef TypedLocalizer<kNumCameras, Field::kNumTargets, Field::kNumObstacles,
James Kuszmaul651fc3f2019-05-15 21:14:25 -070030 kNumTargetsPerFrame, double>
31 TestLocalizer;
James Kuszmaul1057ce82019-02-09 17:58:24 -080032typedef typename TestLocalizer::Camera TestCamera;
James Kuszmaulf4ede202020-02-14 08:47:40 -080033typedef typename TestLocalizer::Target Target;
James Kuszmaul1057ce82019-02-09 17:58:24 -080034typedef typename TestCamera::Pose Pose;
35typedef typename TestCamera::LineSegment Obstacle;
36
37typedef TestLocalizer::StateIdx StateIdx;
38
39using ::frc971::control_loops::drivetrain::DrivetrainConfig;
40
41// When placing the cameras on the robot, set them all kCameraOffset out from
42// the center, to test that we really can handle cameras that aren't at the
43// center-of-mass.
44constexpr double kCameraOffset = 0.1;
45
46#if defined(SUPPORT_PLOT)
47// Plots a line from a vector of Pose's.
48void PlotPlotPts(const ::std::vector<Pose> &poses,
49 const ::std::map<::std::string, ::std::string> &kwargs) {
50 ::std::vector<double> x;
51 ::std::vector<double> y;
James Kuszmaul651fc3f2019-05-15 21:14:25 -070052 for (const Pose &p : poses) {
James Kuszmaul1057ce82019-02-09 17:58:24 -080053 x.push_back(p.abs_pos().x());
54 y.push_back(p.abs_pos().y());
55 }
56 matplotlibcpp::plot(x, y, kwargs);
57}
58#endif
59
60struct LocalizerTestParams {
61 // Control points for the spline to make the robot follow.
62 ::std::array<float, 6> control_pts_x;
63 ::std::array<float, 6> control_pts_y;
64 // The actual state to start the robot at. By setting voltage errors and the
65 // such you can introduce persistent disturbances.
66 TestLocalizer::State true_start_state;
67 // The initial state of the estimator.
68 TestLocalizer::State known_start_state;
69 // Whether or not to add Gaussian noise to the sensors and cameras.
70 bool noisify;
71 // Whether or not to add unmodelled disturbances.
72 bool disturb;
73 // The tolerances for the estimator and for the trajectory following at
74 // the end of the spline:
75 double estimate_tolerance;
76 double goal_tolerance;
77};
78
79class ParameterizedLocalizerTest
80 : public ::testing::TestWithParam<LocalizerTestParams> {
81 public:
82 ::aos::testing::TestSharedMemory shm_;
83
84 // Set up three targets in a row, at (-1, 0), (0, 0), and (1, 0).
85 // Make the right-most target (1, 0) be facing away from the camera, and give
86 // the middle target some skew.
87 // Place one camera facing forward, the other facing backward, and set the
88 // robot at (0, -5) with the cameras each 0.1m from the center.
89 // Place one obstacle in a place where it can block the left-most target (-1,
90 // 0).
91 ParameterizedLocalizerTest()
92 : field_(),
93 targets_(field_.targets()),
94 modeled_obstacles_(field_.obstacles()),
95 true_obstacles_(field_.obstacles()),
96 dt_config_(drivetrain::GetDrivetrainConfig()),
97 // Pull the noise for the encoders/gyros from the R matrix:
98 encoder_noise_(::std::sqrt(
99 dt_config_.make_kf_drivetrain_loop().observer().coefficients().R(
100 0, 0))),
101 gyro_noise_(::std::sqrt(
102 dt_config_.make_kf_drivetrain_loop().observer().coefficients().R(
103 2, 2))),
104 // As per the comments in localizer.h, we set the field of view and
105 // noise parameters on the robot_cameras_ so that they see a bit more
106 // than the true_cameras_. The robot_cameras_ are what is passed to the
107 // localizer and used to generate "expected" targets. The true_cameras_
108 // are what we actually use to generate targets to pass to the
109 // localizer.
110 robot_cameras_{
111 {TestCamera({&robot_pose_, {0.0, kCameraOffset, 0.0}, M_PI_2},
112 M_PI_2 * 1.1, robot_noise_parameters_, targets_,
113 modeled_obstacles_),
114 TestCamera({&robot_pose_, {kCameraOffset, 0.0, 0.0}, 0.0},
115 M_PI_2 * 1.1, robot_noise_parameters_, targets_,
116 modeled_obstacles_),
117 TestCamera({&robot_pose_, {-kCameraOffset, 0.0, 0.0}, M_PI},
118 M_PI_2 * 1.1, robot_noise_parameters_, targets_,
119 modeled_obstacles_),
120 TestCamera({&robot_pose_, {0.0, -kCameraOffset, 0.0}, -M_PI_2},
121 M_PI_2 * 1.1, robot_noise_parameters_, targets_,
122 modeled_obstacles_)}},
123 true_cameras_{
124 {TestCamera({&true_robot_pose_, {0.0, kCameraOffset, 0.0}, M_PI_2},
125 M_PI_2 * 0.9, true_noise_parameters_, targets_,
126 true_obstacles_),
127 TestCamera({&true_robot_pose_, {kCameraOffset, 0.0, 0.0}, 0.0},
128 M_PI_2 * 0.9, true_noise_parameters_, targets_,
129 true_obstacles_),
130 TestCamera({&true_robot_pose_, {-kCameraOffset, 0.0, 0.0}, M_PI},
131 M_PI_2 * 0.9, true_noise_parameters_, targets_,
132 true_obstacles_),
133 TestCamera(
134 {&true_robot_pose_, {-0.0, -kCameraOffset, 0.0}, -M_PI_2},
135 M_PI_2 * 0.9, true_noise_parameters_, targets_,
136 true_obstacles_)}},
137 localizer_(dt_config_, &robot_pose_),
138 spline_drivetrain_(dt_config_) {
139 // We use the default P() for initialization.
140 localizer_.ResetInitialState(t0_, GetParam().known_start_state,
141 localizer_.P());
142 }
143
144 void SetUp() {
Alex Perrycb7da4b2019-08-28 19:35:56 -0700145 flatbuffers::DetachedBuffer goal_buffer;
146 {
147 flatbuffers::FlatBufferBuilder fbb;
148
149 flatbuffers::Offset<flatbuffers::Vector<float>> spline_x_offset =
150 fbb.CreateVector<float>(GetParam().control_pts_x.begin(),
151 GetParam().control_pts_x.size());
152
153 flatbuffers::Offset<flatbuffers::Vector<float>> spline_y_offset =
154 fbb.CreateVector<float>(GetParam().control_pts_y.begin(),
155 GetParam().control_pts_y.size());
156
157 frc971::MultiSpline::Builder multispline_builder(fbb);
158
159 multispline_builder.add_spline_count(1);
160 multispline_builder.add_spline_x(spline_x_offset);
161 multispline_builder.add_spline_y(spline_y_offset);
162
163 flatbuffers::Offset<frc971::MultiSpline> multispline_offset =
164 multispline_builder.Finish();
165
166 frc971::control_loops::drivetrain::SplineGoal::Builder spline_builder(
167 fbb);
168
169 spline_builder.add_spline_idx(1);
170 spline_builder.add_spline(multispline_offset);
171
172 flatbuffers::Offset<frc971::control_loops::drivetrain::SplineGoal>
173 spline_offset = spline_builder.Finish();
174
175 frc971::control_loops::drivetrain::Goal::Builder goal_builder(fbb);
176
177 goal_builder.add_spline(spline_offset);
178 goal_builder.add_controller_type(
Austin Schuh872723c2019-12-25 14:38:09 -0800179 frc971::control_loops::drivetrain::ControllerType::SPLINE_FOLLOWER);
Alex Perrycb7da4b2019-08-28 19:35:56 -0700180 goal_builder.add_spline_handle(1);
181
182 fbb.Finish(goal_builder.Finish());
183
184 goal_buffer = fbb.Release();
185 }
186 aos::FlatbufferDetachedBuffer<frc971::control_loops::drivetrain::Goal> goal(
187 std::move(goal_buffer));
188
Alex Perrycc3ee4c2019-02-09 21:20:41 -0800189 // Let the spline drivetrain compute the spline.
Alex Perrycb7da4b2019-08-28 19:35:56 -0700190 while (true) {
Austin Schuhb574fe42019-12-06 23:51:47 -0800191 // We need to keep sending the goal. There are conditions when the
192 // trajectory lock isn't grabbed the first time, and we want to keep
193 // banging on it to keep trying. Otherwise we deadlock.
194 spline_drivetrain_.SetGoal(&goal.message());
195
Alex Perrycc3ee4c2019-02-09 21:20:41 -0800196 ::std::this_thread::sleep_for(::std::chrono::milliseconds(5));
Alex Perrycb7da4b2019-08-28 19:35:56 -0700197
198 flatbuffers::FlatBufferBuilder fbb;
199
200 flatbuffers::Offset<frc971::control_loops::drivetrain::TrajectoryLogging>
201 trajectory_logging_offset =
202 spline_drivetrain_.MakeTrajectoryLogging(&fbb);
203
204 ::frc971::control_loops::drivetrain::Status::Builder status_builder(fbb);
205 status_builder.add_trajectory_logging(trajectory_logging_offset);
206 spline_drivetrain_.PopulateStatus(&status_builder);
207 fbb.Finish(status_builder.Finish());
208 aos::FlatbufferDetachedBuffer<::frc971::control_loops::drivetrain::Status>
209 status(fbb.Release());
210
211 if (status.message().trajectory_logging()->planning_state() ==
Austin Schuh872723c2019-12-25 14:38:09 -0800212 ::frc971::control_loops::drivetrain::PlanningState::PLANNED) {
Alex Perrycb7da4b2019-08-28 19:35:56 -0700213 break;
214 }
215 }
216 spline_drivetrain_.SetGoal(&goal.message());
James Kuszmaul1057ce82019-02-09 17:58:24 -0800217 }
218
219 void TearDown() {
220 printf("Each iteration of the simulation took on average %f seconds.\n",
221 avg_time_.count());
222#if defined(SUPPORT_PLOT)
223 if (FLAGS_plot) {
224 matplotlibcpp::figure();
225 matplotlibcpp::plot(simulation_t_, simulation_vl_, {{"label", "Vl"}});
226 matplotlibcpp::plot(simulation_t_, simulation_vr_, {{"label", "Vr"}});
227 matplotlibcpp::legend();
228
229 matplotlibcpp::figure();
230 matplotlibcpp::plot(spline_x_, spline_y_, {{"label", "spline"}});
231 matplotlibcpp::plot(simulation_x_, simulation_y_, {{"label", "robot"}});
232 matplotlibcpp::plot(estimated_x_, estimated_y_,
233 {{"label", "estimation"}});
James Kuszmaul651fc3f2019-05-15 21:14:25 -0700234 for (const Target &target : targets_) {
James Kuszmaul1057ce82019-02-09 17:58:24 -0800235 PlotPlotPts(target.PlotPoints(), {{"c", "g"}});
236 }
237 for (const Obstacle &obstacle : true_obstacles_) {
238 PlotPlotPts(obstacle.PlotPoints(), {{"c", "k"}});
239 }
240 // Go through and plot true/expected camera targets for a few select
241 // time-steps.
242 ::std::vector<const char *> colors{"m", "y", "c"};
243 int jj = 0;
244 for (size_t ii = 0; ii < simulation_x_.size(); ii += 400) {
245 *true_robot_pose_.mutable_pos() << simulation_x_[ii], simulation_y_[ii],
246 0.0;
247 true_robot_pose_.set_theta(simulation_theta_[ii]);
248 for (const TestCamera &camera : true_cameras_) {
249 for (const auto &plot_pts : camera.PlotPoints()) {
250 PlotPlotPts(plot_pts, {{"c", colors[jj]}});
251 }
252 }
253 for (const TestCamera &camera : robot_cameras_) {
254 *robot_pose_.mutable_pos() << estimated_x_[ii], estimated_y_[ii], 0.0;
255 robot_pose_.set_theta(estimated_theta_[ii]);
256 const auto &all_plot_pts = camera.PlotPoints();
257 *robot_pose_.mutable_pos() = true_robot_pose_.rel_pos();
258 robot_pose_.set_theta(true_robot_pose_.rel_theta());
259 for (const auto &plot_pts : all_plot_pts) {
260 PlotPlotPts(plot_pts, {{"c", colors[jj]}, {"ls", "--"}});
261 }
262 }
263 jj = (jj + 1) % colors.size();
264 }
265 matplotlibcpp::legend();
266
267 matplotlibcpp::figure();
268 matplotlibcpp::plot(
269 simulation_t_, spline_x_,
270 {{"label", "spline x"}, {"c", "g"}, {"ls", ""}, {"marker", "o"}});
271 matplotlibcpp::plot(simulation_t_, simulation_x_,
272 {{"label", "simulated x"}, {"c", "g"}});
273 matplotlibcpp::plot(simulation_t_, estimated_x_,
274 {{"label", "estimated x"}, {"c", "g"}, {"ls", "--"}});
275
276 matplotlibcpp::plot(
277 simulation_t_, spline_y_,
278 {{"label", "spline y"}, {"c", "b"}, {"ls", ""}, {"marker", "o"}});
279 matplotlibcpp::plot(simulation_t_, simulation_y_,
280 {{"label", "simulated y"}, {"c", "b"}});
281 matplotlibcpp::plot(simulation_t_, estimated_y_,
282 {{"label", "estimated y"}, {"c", "b"}, {"ls", "--"}});
283
284 matplotlibcpp::plot(simulation_t_, simulation_theta_,
285 {{"label", "simulated theta"}, {"c", "r"}});
286 matplotlibcpp::plot(
287 simulation_t_, estimated_theta_,
288 {{"label", "estimated theta"}, {"c", "r"}, {"ls", "--"}});
289 matplotlibcpp::legend();
290
291 matplotlibcpp::show();
292 }
293#endif
294 }
295
296 protected:
297 // Returns a random number with a gaussian distribution with a mean of zero
298 // and a standard deviation of std, if noisify = true.
299 // If noisify is false, then returns 0.0.
300 double Normal(double std) {
301 if (GetParam().noisify) {
302 return normal_(gen_) * std;
303 }
304 return 0.0;
305 }
306 // Adds random noise to the given target view.
307 void Noisify(TestCamera::TargetView *view) {
308 view->reading.heading += Normal(view->noise.heading);
309 view->reading.distance += Normal(view->noise.distance);
310 view->reading.height += Normal(view->noise.height);
311 view->reading.skew += Normal(view->noise.skew);
312 }
313 // The differential equation for the dynamics of the system.
314 TestLocalizer::State DiffEq(const TestLocalizer::State &X,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800315 const Eigen::Vector2d &voltage) {
316 TestLocalizer::Input U;
317 U.setZero();
318 U(0) = voltage(0);
319 U(1) = voltage(1);
320 return localizer_.DiffEq(X, U, true);
James Kuszmaul1057ce82019-02-09 17:58:24 -0800321 }
322
323 Field field_;
324 ::std::array<Target, Field::kNumTargets> targets_;
325 // The obstacles that are passed to the camera objects for the localizer.
326 ::std::array<Obstacle, Field::kNumObstacles> modeled_obstacles_;
327 // The obstacles that are used for actually simulating the cameras.
328 ::std::array<Obstacle, Field::kNumObstacles> true_obstacles_;
329
330 DrivetrainConfig<double> dt_config_;
331
332 // Noise information for the actual simulated cameras (true_*) and the
333 // parameters that the localizer should use for modelling the cameras. Note
334 // how the max_viewable_distance is larger for the localizer, so that if
335 // there is any error in the estimation, it still thinks that it can see
336 // any targets that might actually be in range.
337 TestCamera::NoiseParameters true_noise_parameters_ = {
338 .max_viewable_distance = 10.0,
339 .heading_noise = 0.02,
340 .nominal_distance_noise = 0.06,
341 .nominal_skew_noise = 0.1,
342 .nominal_height_noise = 0.01};
343 TestCamera::NoiseParameters robot_noise_parameters_ = {
344 .max_viewable_distance = 11.0,
345 .heading_noise = 0.02,
346 .nominal_distance_noise = 0.06,
347 .nominal_skew_noise = 0.1,
348 .nominal_height_noise = 0.01};
349
350 // Standard deviations of the noise for the encoders/gyro.
351 double encoder_noise_, gyro_noise_;
352
353 Pose robot_pose_;
354 ::std::array<TestCamera, 4> robot_cameras_;
355 Pose true_robot_pose_;
356 ::std::array<TestCamera, 4> true_cameras_;
357
358 TestLocalizer localizer_;
359
360 ::frc971::control_loops::drivetrain::SplineDrivetrain spline_drivetrain_;
361
362 // All the data we want to end up plotting.
363 ::std::vector<double> simulation_t_;
364
365 ::std::vector<double> spline_x_;
366 ::std::vector<double> spline_y_;
367 ::std::vector<double> estimated_x_;
368 ::std::vector<double> estimated_y_;
369 ::std::vector<double> estimated_theta_;
370 ::std::vector<double> simulation_x_;
371 ::std::vector<double> simulation_y_;
372 ::std::vector<double> simulation_theta_;
373
374 ::std::vector<double> simulation_vl_;
375 ::std::vector<double> simulation_vr_;
376
377 // Simulation start time
378 ::aos::monotonic_clock::time_point t0_;
379
380 // Average duration of each iteration; used for debugging and getting a
381 // sanity-check on what performance looks like--uses a real system clock.
382 ::std::chrono::duration<double> avg_time_;
383
384 ::std::mt19937 gen_{static_cast<uint32_t>(::aos::testing::RandomSeed())};
385 ::std::normal_distribution<> normal_;
386};
387
James Kuszmaul6f941b72019-03-08 18:12:25 -0800388using ::std::chrono::milliseconds;
389
James Kuszmaul1057ce82019-02-09 17:58:24 -0800390// Tests that, when we attempt to follow a spline and use the localizer to
391// perform the state estimation, we end up roughly where we should and that
392// the localizer has a valid position estimate.
393TEST_P(ParameterizedLocalizerTest, SplineTest) {
394 // state stores the true state of the robot throughout the simulation.
395 TestLocalizer::State state = GetParam().true_start_state;
396
397 ::aos::monotonic_clock::time_point t = t0_;
398
399 // The period with which we should take frames from the cameras. Currently,
400 // we just trigger all the cameras at once, rather than offsetting them or
401 // anything.
James Kuszmaul651fc3f2019-05-15 21:14:25 -0700402 const int camera_period = 5; // cycles
James Kuszmaul6f941b72019-03-08 18:12:25 -0800403 // The amount of time to delay the camera images from when they are taken, for
404 // each camera.
405 const ::std::array<milliseconds, 4> camera_latencies{
406 {milliseconds(45), milliseconds(50), milliseconds(55),
407 milliseconds(100)}};
James Kuszmaul1057ce82019-02-09 17:58:24 -0800408
James Kuszmaul6f941b72019-03-08 18:12:25 -0800409 // A queue of camera frames for each camera so that we can add a time delay to
410 // the data coming from the cameras.
411 ::std::array<
412 ::std::queue<::std::tuple<
413 ::aos::monotonic_clock::time_point, const TestCamera *,
414 ::aos::SizedArray<TestCamera::TargetView, kNumTargetsPerFrame>>>,
415 4>
416 camera_queues;
James Kuszmaul1057ce82019-02-09 17:58:24 -0800417
418 // Start time, for debugging.
419 const auto begin = ::std::chrono::steady_clock::now();
420
421 size_t i;
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700422 // Run the feedback-controller slightly past the nominal end-time. This both
423 // exercises the code to see what happens when we are trying to stand still,
424 // and gives the control loops time to stabilize.
425 aos::monotonic_clock::duration extra_time = std::chrono::seconds(2);
426 for (i = 0;
427 !spline_drivetrain_.IsAtEnd() || extra_time > std::chrono::seconds(0);
428 ++i) {
429 if (spline_drivetrain_.IsAtEnd()) {
430 extra_time -= dt_config_.dt;
431 }
James Kuszmaul1057ce82019-02-09 17:58:24 -0800432 // Get the current state estimate into a matrix that works for the
433 // trajectory code.
434 ::Eigen::Matrix<double, 5, 1> known_state;
435 TestLocalizer::State X_hat = localizer_.X_hat();
436 known_state << X_hat(StateIdx::kX, 0), X_hat(StateIdx::kY, 0),
437 X_hat(StateIdx::kTheta, 0), X_hat(StateIdx::kLeftVelocity, 0),
438 X_hat(StateIdx::kRightVelocity, 0);
439
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700440 Eigen::Vector2d voltage_error(X_hat(StateIdx::kLeftVoltageError),
441 X_hat(StateIdx::kRightVoltageError));
442 spline_drivetrain_.Update(true, known_state, voltage_error);
James Kuszmaul1057ce82019-02-09 17:58:24 -0800443
Alex Perrycb7da4b2019-08-28 19:35:56 -0700444 ::frc971::control_loops::drivetrain::OutputT output;
James Kuszmaul1057ce82019-02-09 17:58:24 -0800445 output.left_voltage = 0;
446 output.right_voltage = 0;
447 spline_drivetrain_.SetOutput(&output);
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800448 Eigen::Vector2d U(output.left_voltage, output.right_voltage);
James Kuszmaul1057ce82019-02-09 17:58:24 -0800449
450 const ::Eigen::Matrix<double, 5, 1> goal_state =
451 spline_drivetrain_.CurrentGoalState();
452 simulation_t_.push_back(
James Kuszmaul651fc3f2019-05-15 21:14:25 -0700453 ::aos::time::DurationInSeconds(t.time_since_epoch()));
James Kuszmaul1057ce82019-02-09 17:58:24 -0800454 spline_x_.push_back(goal_state(0, 0));
455 spline_y_.push_back(goal_state(1, 0));
456 simulation_x_.push_back(state(StateIdx::kX, 0));
457 simulation_y_.push_back(state(StateIdx::kY, 0));
458 simulation_theta_.push_back(state(StateIdx::kTheta, 0));
459 estimated_x_.push_back(known_state(0, 0));
460 estimated_y_.push_back(known_state(1, 0));
461 estimated_theta_.push_back(known_state(StateIdx::kTheta, 0));
462
463 simulation_vl_.push_back(U(0));
464 simulation_vr_.push_back(U(1));
465 U(0, 0) = ::std::max(::std::min(U(0, 0), 12.0), -12.0);
466 U(1, 0) = ::std::max(::std::min(U(1, 0), 12.0), -12.0);
467
468 state = ::frc971::control_loops::RungeKuttaU(
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800469 [this](const ::Eigen::Matrix<double, 12, 1> &X,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800470 const ::Eigen::Matrix<double, 2, 1> &U) { return DiffEq(X, U); },
James Kuszmaul651fc3f2019-05-15 21:14:25 -0700471 state, U, ::aos::time::DurationInSeconds(dt_config_.dt));
James Kuszmaul1057ce82019-02-09 17:58:24 -0800472
473 // Add arbitrary disturbances at some arbitrary interval. The main points of
474 // interest here are that we (a) stop adding disturbances at the very end of
475 // the trajectory, to allow us to actually converge to the goal, and (b)
476 // scale disturbances by the corruent velocity.
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800477 // TODO(james): Figure out how to persist good accelerometer values through
478 // the disturbances.
James Kuszmaulc73bb222019-04-07 12:15:35 -0700479 if (GetParam().disturb && i % 75 == 0) {
James Kuszmaul1057ce82019-02-09 17:58:24 -0800480 // Scale the disturbance so that when we have near-zero velocity we don't
481 // have much disturbance.
482 double disturbance_scale = ::std::min(
483 1.0, ::std::sqrt(::std::pow(state(StateIdx::kLeftVelocity, 0), 2) +
484 ::std::pow(state(StateIdx::kRightVelocity, 0), 2)) /
485 3.0);
486 TestLocalizer::State disturbance;
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800487 disturbance << 0.02, 0.02, 0.001, 0.03, 0.02, 0.0, 0.0, 0.0, 0.0, 0.0,
488 0.0, 0.0;
James Kuszmaul1057ce82019-02-09 17:58:24 -0800489 disturbance *= disturbance_scale;
490 state += disturbance;
491 }
492
493 t += dt_config_.dt;
494 *true_robot_pose_.mutable_pos() << state(StateIdx::kX, 0),
495 state(StateIdx::kY, 0), 0.0;
496 true_robot_pose_.set_theta(state(StateIdx::kTheta, 0));
497 const double left_enc = state(StateIdx::kLeftEncoder, 0);
498 const double right_enc = state(StateIdx::kRightEncoder, 0);
499
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800500 const double gyro = (state(StateIdx::kRightVelocity) -
501 state(StateIdx::kLeftVelocity)) /
James Kuszmaul1057ce82019-02-09 17:58:24 -0800502 dt_config_.robot_radius / 2.0;
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800503 const TestLocalizer::State xdot = DiffEq(state, U);
504 const Eigen::Vector3d accel(
505 localizer_.CalcLongitudinalVelocity(xdot) -
506 gyro * state(StateIdx::kLateralVelocity),
507 gyro * localizer_.CalcLongitudinalVelocity(state), 1.0);
James Kuszmaul1057ce82019-02-09 17:58:24 -0800508
509 localizer_.UpdateEncodersAndGyro(left_enc + Normal(encoder_noise_),
510 right_enc + Normal(encoder_noise_),
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800511 gyro + Normal(gyro_noise_), U, accel, t);
James Kuszmaul1057ce82019-02-09 17:58:24 -0800512
James Kuszmaul6f941b72019-03-08 18:12:25 -0800513 for (size_t cam_idx = 0; cam_idx < camera_queues.size(); ++cam_idx) {
514 auto &camera_queue = camera_queues[cam_idx];
515 // Clear out the camera frames that we are ready to pass to the localizer.
516 while (!camera_queue.empty() && ::std::get<0>(camera_queue.front()) <
517 t - camera_latencies[cam_idx]) {
518 const auto tuple = camera_queue.front();
519 camera_queue.pop();
520 ::aos::monotonic_clock::time_point t_obs = ::std::get<0>(tuple);
521 const TestCamera *camera = ::std::get<1>(tuple);
522 ::aos::SizedArray<TestCamera::TargetView, kNumTargetsPerFrame> views =
523 ::std::get<2>(tuple);
524 localizer_.UpdateTargets(*camera, views, t_obs);
525 }
James Kuszmaul1057ce82019-02-09 17:58:24 -0800526
James Kuszmaul6f941b72019-03-08 18:12:25 -0800527 // Actually take all the images and store them in the queue.
528 if (i % camera_period == 0) {
529 for (size_t jj = 0; jj < true_cameras_.size(); ++jj) {
530 const auto target_views = true_cameras_[jj].target_views();
531 ::aos::SizedArray<TestCamera::TargetView, kNumTargetsPerFrame>
532 pass_views;
533 for (size_t ii = 0;
534 ii < ::std::min(kNumTargetsPerFrame, target_views.size());
535 ++ii) {
536 TestCamera::TargetView view = target_views[ii];
537 Noisify(&view);
538 pass_views.push_back(view);
539 }
540 camera_queue.emplace(t, &robot_cameras_[jj], pass_views);
James Kuszmaul1057ce82019-02-09 17:58:24 -0800541 }
James Kuszmaul1057ce82019-02-09 17:58:24 -0800542 }
543 }
James Kuszmaul1057ce82019-02-09 17:58:24 -0800544 }
545
546 const auto end = ::std::chrono::steady_clock::now();
547 avg_time_ = (end - begin) / i;
548 TestLocalizer::State estimate_err = state - localizer_.X_hat();
549 EXPECT_LT(estimate_err.template topRows<7>().norm(),
550 GetParam().estimate_tolerance);
551 // Check that none of the states that we actually care about (x/y/theta, and
552 // wheel positions/speeds) are too far off, individually:
James Kuszmaul7f1a4082019-04-14 10:50:44 -0700553 EXPECT_LT(estimate_err.template topRows<3>().cwiseAbs().maxCoeff(),
James Kuszmaul1057ce82019-02-09 17:58:24 -0800554 GetParam().estimate_tolerance / 3.0)
555 << "Estimate error: " << estimate_err.transpose();
556 Eigen::Matrix<double, 5, 1> final_trajectory_state;
557 final_trajectory_state << state(StateIdx::kX, 0), state(StateIdx::kY, 0),
558 state(StateIdx::kTheta, 0), state(StateIdx::kLeftVelocity, 0),
559 state(StateIdx::kRightVelocity, 0);
560 const Eigen::Matrix<double, 5, 1> final_trajectory_state_err =
561 final_trajectory_state - spline_drivetrain_.CurrentGoalState();
562 EXPECT_LT(final_trajectory_state_err.norm(), GetParam().goal_tolerance)
563 << "Goal error: " << final_trajectory_state_err.transpose();
564}
565
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700566// NOTE: Following the 2019 season, we stopped maintaining this as rigorously.
567// This means that changes to either the base HybridEKF class or the spline
568// controller can cause us to violate the tolerances specified here. We
569// currently just up the tolerances whenever they cause issues, so long as
570// things don't appear to be unstable (since these tests do do a test of the
571// full localizer + spline system, we should pay attention if there is actual
572// instability rather than just poor tolerances).
James Kuszmaul1057ce82019-02-09 17:58:24 -0800573INSTANTIATE_TEST_CASE_P(
574 LocalizerTest, ParameterizedLocalizerTest,
575 ::testing::Values(
576 // Checks a "perfect" scenario, where we should track perfectly.
577 LocalizerTestParams({
578 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
579 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700580 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800581 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800582 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700583 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800584 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800585 .finished(),
586 /*noisify=*/false,
587 /*disturb=*/false,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800588 /*estimate_tolerance=*/1e-2,
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700589 /*goal_tolerance=*/0.2,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800590 }),
591 // Checks "perfect" estimation, but start off the spline and check
592 // that we can still follow it.
593 LocalizerTestParams({
594 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
595 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700596 (TestLocalizer::State() << 0.0, -4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800597 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800598 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700599 (TestLocalizer::State() << 0.0, -4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800600 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800601 .finished(),
602 /*noisify=*/false,
603 /*disturb=*/false,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800604 /*estimate_tolerance=*/1e-2,
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700605 /*goal_tolerance=*/0.4,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800606 }),
607 // Repeats perfect scenario, but add sensor noise.
608 LocalizerTestParams({
609 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
610 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700611 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800612 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800613 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700614 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800615 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800616 .finished(),
617 /*noisify=*/true,
618 /*disturb=*/false,
James Kuszmaulea314d92019-02-18 19:45:06 -0800619 /*estimate_tolerance=*/0.4,
James Kuszmaul07b40442020-03-08 22:20:21 -0700620 /*goal_tolerance=*/0.8,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800621 }),
622 // Repeats perfect scenario, but add initial estimator error.
623 LocalizerTestParams({
624 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
625 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700626 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800627 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800628 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700629 (TestLocalizer::State() << 0.1, -5.1, -0.01, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800630 0.0, 0.0, 0.0, 0.0, 0.0)
James Kuszmaul074429e2019-03-23 16:01:49 -0700631 .finished(),
632 /*noisify=*/false,
633 /*disturb=*/false,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800634 /*estimate_tolerance=*/1e-2,
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700635 /*goal_tolerance=*/0.2,
James Kuszmaul074429e2019-03-23 16:01:49 -0700636 }),
637 // Repeats perfect scenario, but add voltage + angular errors:
638 LocalizerTestParams({
639 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
640 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700641 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5,
642 0.25, 0.02, 0.0, 0.0)
James Kuszmaul074429e2019-03-23 16:01:49 -0700643 .finished(),
644 (TestLocalizer::State() << 0.1, -5.1, -0.01, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800645 0.0, 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800646 .finished(),
647 /*noisify=*/false,
648 /*disturb=*/false,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800649 /*estimate_tolerance=*/1e-2,
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700650 /*goal_tolerance=*/0.3,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800651 }),
652 // Add disturbances while we are driving:
653 LocalizerTestParams({
654 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
655 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700656 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800657 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800658 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700659 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800660 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800661 .finished(),
662 /*noisify=*/false,
663 /*disturb=*/true,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800664 /*estimate_tolerance=*/4e-2,
James Kuszmaulaa2499d2020-06-02 21:31:19 -0700665 /*goal_tolerance=*/0.25,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800666 }),
667 // Add noise and some initial error in addition:
668 LocalizerTestParams({
669 /*control_pts_x=*/{{0.0, 3.0, 3.0, 0.0, 1.0, 1.0}},
670 /*control_pts_y=*/{{-5.0, -5.0, 2.0, 2.0, 2.0, 3.0}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700671 (TestLocalizer::State() << 0.0, -5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800672 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800673 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700674 (TestLocalizer::State() << 0.1, -5.1, 0.03, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800675 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800676 .finished(),
677 /*noisify=*/true,
678 /*disturb=*/true,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800679 /*estimate_tolerance=*/0.5,
680 /*goal_tolerance=*/1.0,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800681 }),
682 // Try another spline, just in case the one I was using is special for
683 // some reason; this path will also go straight up to a target, to
684 // better simulate what might happen when trying to score:
685 LocalizerTestParams({
686 /*control_pts_x=*/{{0.5, 3.5, 4.0, 8.0, 11.0, 10.2}},
687 /*control_pts_y=*/{{1.0, 1.0, -3.0, -2.0, -3.5, -3.65}},
James Kuszmaul074429e2019-03-23 16:01:49 -0700688 (TestLocalizer::State() << 0.6, 1.01, 0.01, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800689 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800690 .finished(),
James Kuszmaul074429e2019-03-23 16:01:49 -0700691 (TestLocalizer::State() << 0.5, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
James Kuszmaul3c5b4d32020-02-11 17:22:14 -0800692 0.0, 0.0, 0.0, 0.0)
James Kuszmaul1057ce82019-02-09 17:58:24 -0800693 .finished(),
694 /*noisify=*/true,
695 /*disturb=*/false,
James Kuszmaul7f1a4082019-04-14 10:50:44 -0700696 // TODO(james): Improve tests so that we aren't constantly
697 // readjusting the tolerances up.
James Kuszmaulc40c26e2019-03-24 16:26:43 -0700698 /*estimate_tolerance=*/0.3,
James Kuszmaul074429e2019-03-23 16:01:49 -0700699 /*goal_tolerance=*/0.7,
James Kuszmaul1057ce82019-02-09 17:58:24 -0800700 })));
701
702} // namespace testing
703} // namespace control_loops
704} // namespace y2019