ilqgames
A new real-time solver for large-scale differential games.
minimally_invasive_receding_horizon_simulator.cpp
1 /*
2  * Copyright (c) 2020, The Regents of the University of California (Regents).
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  * 1. Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above
13  * copyright notice, this list of conditions and the following
14  * disclaimer in the documentation and/or other materials provided
15  * with the distribution.
16  *
17  * 3. Neither the name of the copyright holder nor the names of its
18  * contributors may be used to endorse or promote products derived
19  * from this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS
22  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31  * POSSIBILITY OF SUCH DAMAGE.
32  *
33  * Please contact the author(s) of this library if you have any questions.
34  * Authors: David Fridovich-Keil ( dfk@eecs.berkeley.edu )
35  */
36 
37 ///////////////////////////////////////////////////////////////////////////////
38 //
39 // Utility for solving a problem using a receding horizon, simulating dynamics
40 // forward at each stage to account for the passage of time and also switching
41 // to a minimally-invasive control *for only the ego vehicle* if its safety
42 // problem detects iminent danger.
43 //
44 // This class is intended as a facsimile of a real-time, online receding horizon
45 // problem in which short horizon problems are solved asynchronously throughout
46 // operation.
47 //
48 ///////////////////////////////////////////////////////////////////////////////
49 
50 #include <ilqgames/examples/minimally_invasive_receding_horizon_simulator.h>
51 #include <ilqgames/solver/ilq_solver.h>
52 #include <ilqgames/solver/problem.h>
53 #include <ilqgames/solver/solution_splicer.h>
54 #include <ilqgames/utils/solver_log.h>
55 #include <ilqgames/utils/strategy.h>
56 #include <ilqgames/utils/types.h>
57 
58 #include <glog/logging.h>
59 #include <chrono>
60 #include <memory>
61 #include <typeinfo>
62 #include <vector>
63 
64 namespace ilqgames {
65 
66 using clock = std::chrono::system_clock;
67 
68 std::vector<ActiveProblem> MinimallyInvasiveRecedingHorizonSimulator(
69  Time final_time, Time planner_runtime, GameSolver* original,
70  GameSolver* safety,
71  std::vector<std::shared_ptr<const SolverLog>>* original_logs,
72  std::vector<std::shared_ptr<const SolverLog>>* safety_logs) {
73  CHECK_NOTNULL(original);
74  CHECK_NOTNULL(safety);
75  CHECK_NOTNULL(original_logs);
76  CHECK_NOTNULL(safety_logs);
77 
78  // Make sure the two problems have the same initial condition and time.
79  CHECK(original->GetProblem().InitialState().isApprox(
80  safety->GetProblem().InitialState(), constants::kSmallNumber));
81  CHECK_NEAR(original->GetProblem().InitialTime(),
82  safety->GetProblem().InitialTime(), constants::kSmallNumber);
83 
84  // Unpack dynamics, and ensure that the two problems actually share the same
85  // dynamics object type.
86  const auto& dynamics = *original->GetProblem().Dynamics();
87  const auto& safety_dynamics = *safety->GetProblem().Dynamics();
88  CHECK(typeid(dynamics) == typeid(safety_dynamics));
89 
90  // Clear out the log arrays for us to save in.
91  original_logs->clear();
92  safety_logs->clear();
93 
94  // Initial run of the solver. Keep track of time in order to know how much to
95  // integrate dynamics forward. Ensure that both solvers succeed at the first
96  // invocation.
97  auto solver_call_time = clock::now();
98  bool success = false;
99  original_logs->push_back(original->Solve(&success));
100  CHECK(success);
101  Time elapsed_time =
102  std::chrono::duration<Time>(clock::now() - solver_call_time).count();
103  VLOG(1) << "Solved initial original problem in " << elapsed_time
104  << " seconds, with " << original_logs->back()->NumIterates()
105  << " iterations.";
106 
107  solver_call_time = clock::now();
108  safety_logs->push_back(safety->Solve(&success));
109  CHECK(success);
110  elapsed_time =
111  std::chrono::duration<Time>(clock::now() - solver_call_time).count();
112  VLOG(1) << "Solved initial safety problem in " << elapsed_time
113  << " seconds, with " << safety_logs->back()->NumIterates()
114  << " iterations.";
115 
116  // Keep a solution splicer to incorporate new receding horizon solutions.
117  // NOTE: by default, this always just starts with the original controller.
118  SolutionSplicer splicer(*original_logs->front());
119  std::vector<ActiveProblem> active_problem = {ActiveProblem::ORIGINAL};
120 
121  // Repeatedly integrate dynamics forward, reset original_problem initial
122  // conditions, and resolve.
123  VectorXf x(original->GetProblem().InitialState());
124  Time t = original->GetProblem().InitialTime();
125 
126  while (true) {
127  // Break the loop if it's been long enough.
128  // Integrate a little more.
129  constexpr Time kExtraTime = 0.25;
130  t += kExtraTime; // + planner_runtime;
131 
132  if (t >= final_time ||
133  !splicer.ContainsTime(t + planner_runtime +
134  time::kTimeStep))
135  break;
136 
137  x = dynamics.Integrate(t - kExtraTime, t, x,
138  splicer.CurrentOperatingPoint(),
139  splicer.CurrentStrategies());
140 
141  // Find the active problem.
142  const bool current_active_problem_flag = active_problem.back();
143  auto current_active_problem =
144  (current_active_problem_flag == ActiveProblem::ORIGINAL) ? original
145  : safety;
146 
147  // Make sure both problems have the current solution from the splicer.
148  original->GetProblem().OverwriteSolution(splicer.CurrentOperatingPoint(),
149  splicer.CurrentStrategies());
150  safety->GetProblem().OverwriteSolution(splicer.CurrentOperatingPoint(),
151  splicer.CurrentStrategies());
152 
153  // Make sure both problems have the active problem's initial state.
154  original->GetProblem().ResetInitialState(
155  current_active_problem->GetProblem().InitialState());
156  safety->GetProblem().ResetInitialState(
157  current_active_problem->GetProblem().InitialState());
158 
159  // Set up next receding horizon problem and solve, and make sure both
160  // problems' initial state matches that of the active problem.
161  original->GetProblem().SetUpNextRecedingHorizon(x, t, planner_runtime);
162  safety->GetProblem().SetUpNextRecedingHorizon(x, t, planner_runtime);
163 
164  solver_call_time = clock::now();
165  original_logs->push_back(original->Solve(&success, planner_runtime));
166  const Time original_elapsed_time =
167  std::chrono::duration<Time>(clock::now() - solver_call_time).count();
168 
169  CHECK_LE(original_elapsed_time, planner_runtime);
170  VLOG(1) << "t = " << t << ": Solved warm-started original problem in "
171  << original_elapsed_time << " seconds.";
172 
173  solver_call_time = clock::now();
174  safety_logs->push_back(safety->Solve(&success, planner_runtime));
175  const Time safety_elapsed_time =
176  std::chrono::duration<Time>(clock::now() - solver_call_time).count();
177 
178  CHECK_LE(safety_elapsed_time, planner_runtime);
179  VLOG(1) << "t = " << t << ": Solved warm-started safety problem in "
180  << safety_elapsed_time << " seconds.";
181 
182  // Break the loop if it's been long enough.
183  elapsed_time = std::max(original_elapsed_time, safety_elapsed_time);
184  t += elapsed_time;
185  if (t >= final_time || !splicer.ContainsTime(t)) break;
186 
187  // Integrate dynamics forward to account for solve time.
188  x = dynamics.Integrate(t - elapsed_time, t, x,
189  splicer.CurrentOperatingPoint(),
190  splicer.CurrentStrategies());
191 
192  // Check if problems converged.
193  if (!original_logs->back()->WasConverged())
194  VLOG(2) << "Original planner was not converged.";
195  if (!safety_logs->back()->WasConverged())
196  VLOG(2) << "Safety planner was not converged.";
197 
198  // Check the safety criterion, i.e., if the safety problem's value function
199  // for P1 is above kSafetyThreshold (which usually has the units of meters).
200  // That said, make sure the next planner has converged if at all possible.
201  constexpr float kSafetyThreshold = -1.0;
202  const float p1_safety_cost = safety_logs->back()->TotalCosts().front();
203 
204  if (p1_safety_cost > kSafetyThreshold ||
205  (safety_logs->back()->WasConverged() &&
206  !original_logs->back()->WasConverged())) {
207  active_problem.push_back(ActiveProblem::SAFETY);
208  splicer.Splice(*safety_logs->back());
209  VLOG(2) << "Using safety controller.";
210  } else {
211  active_problem.push_back(ActiveProblem::ORIGINAL);
212  if (original_logs->back()->WasConverged())
213  splicer.Splice(*original_logs->back());
214  }
215  }
216 
217  return active_problem;
218 }
219 
220 } // namespace ilqgames