|
|
// A simple "hello world" example of computing a finite-time backward reachable
|
|
|
// set for a polynomial system using convex optimization.
|
|
|
//
|
|
|
// This implements the occupation measure-based SDP relaxation described in
|
|
|
// "Convex computation of the region of attraction of polynomial control
|
|
|
// systems" by Didier Henrion and Milan Korda, IEEE Transactions on Automatic
|
|
|
// Control, vol. 59, no. 2, pp. 297-312, Feb. 2014.
|
|
|
//
|
|
|
// TODO(jadecastro) Transcribe this example into Python.
|
|
|
#include <cmath>
|
|
|
#include <iostream>
|
|
|
|
|
|
#include "drake/common/proto/call_python.h"
|
|
|
#include "drake/common/symbolic/polynomial.h"
|
|
|
#include "drake/solvers/mathematical_program.h"
|
|
|
#include "drake/solvers/mosek_solver.h"
|
|
|
#include "drake/solvers/solve.h"
|
|
|
#include "drake/systems/framework/vector_system.h"
|
|
|
|
|
|
namespace drake {
|
|
|
namespace {
|
|
|
|
|
|
using std::cout;
|
|
|
using std::endl;
|
|
|
using std::pow;
|
|
|
|
|
|
using common::CallPython;
|
|
|
using symbolic::Environment;
|
|
|
using symbolic::Expression;
|
|
|
using symbolic::Polynomial;
|
|
|
using symbolic::Variable;
|
|
|
using symbolic::Variables;
|
|
|
|
|
|
using MapType = Polynomial::MapType;
|
|
|
using Bound = std::map<Variable, double>;
|
|
|
using BoundingBox = std::pair<Bound, Bound>;
|
|
|
|
|
|
/// Defines the autonomous cubic polynomial system:
|
|
|
/// ẋ = 100 x(x - 0.5)(x + 0.5) = 100 x³ - 25 x
|
|
|
template <typename T>
|
|
|
class CubicSystem : public systems::VectorSystem<T> {
|
|
|
public:
|
|
|
CubicSystem() : systems::VectorSystem<T>(0, 0) {
|
|
|
// 0 inputs, 0 outputs, 1 continuous state.
|
|
|
this->DeclareContinuousState(1);
|
|
|
}
|
|
|
|
|
|
private:
|
|
|
virtual void DoCalcVectorTimeDerivatives(
|
|
|
const systems::Context<T>&, const Eigen::VectorBlock<const VectorX<T>>&,
|
|
|
const Eigen::VectorBlock<const VectorX<T>>& state,
|
|
|
Eigen::VectorBlock<VectorX<T>>* derivatives) const {
|
|
|
(*derivatives)(0) = 100. * pow(state(0), 3.0) - 25. * state(0);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/// Makes a cost equal to the volume of polynomial @p p over with respect to the
|
|
|
/// indeterminates over the domain defined by @p b. For polynomials, this is
|
|
|
/// equivalent to the product of `coeff(p) * l`, where `coeff(p)` is the vector
|
|
|
/// of coefficients of `p` and `l` is the corresponding vector of moments of the
|
|
|
/// Lebesgue measure over the domain.
|
|
|
Expression MakeVolumetricCost(const Polynomial& p, const BoundingBox& b) {
|
|
|
DRAKE_DEMAND(b.first.size() == b.second.size());
|
|
|
DRAKE_DEMAND(p.indeterminates().size() == b.first.size());
|
|
|
|
|
|
// Build a moment vector.
|
|
|
const MapType& map = p.monomial_to_coefficient_map();
|
|
|
Expression result;
|
|
|
const Variables& indeterminates = p.indeterminates();
|
|
|
for (const auto& monomial : map) {
|
|
|
const std::map<Variable, int>& powers = monomial.first.get_powers();
|
|
|
Expression moment{1.};
|
|
|
for (const auto& var : indeterminates) {
|
|
|
if (powers.find(var) == powers.end()) {
|
|
|
moment *= b.second.at(var) - b.first.at(var); // zero exponent case.
|
|
|
continue;
|
|
|
}
|
|
|
moment *= (pow(b.second.at(var), powers.at(var) + 1) -
|
|
|
pow(b.first.at(var), powers.at(var) + 1)) /
|
|
|
(powers.at(var) + 1);
|
|
|
}
|
|
|
result += moment * monomial.second;
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
/// Solves the backward reachable set over T seconds on the above univariate
|
|
|
/// example, given a terminal set. Specifically, we solve the dual of the
|
|
|
/// polynomial relaxation to an infinite-dimensional LP over the space of
|
|
|
/// occupation measures capturing the reachable set. By restricting the space
|
|
|
/// of functions to polynomials and representing measures by their moments, we
|
|
|
/// solve the dual relaxation over the space of nonnegative functions in order
|
|
|
/// to find a tight over-approximation to the reachable set.
|
|
|
///
|
|
|
/// Given the target set {x | ‖x(1)‖ ≤ 0.1}, the cubic polynomial system
|
|
|
/// ẋ = 100x(x - 0.5)(x + 0.5) is known to have a backward reachable set
|
|
|
/// B = {x | ‖x(0)‖ < 0.5}.
|
|
|
void ComputeBackwardReachableSet() {
|
|
|
// Construct the system.
|
|
|
CubicSystem<Expression> system;
|
|
|
auto context = system.CreateDefaultContext();
|
|
|
auto derivatives = system.AllocateTimeDerivatives();
|
|
|
|
|
|
// Set up the optimization problem and declare the indeterminates.
|
|
|
solvers::MathematicalProgram prog;
|
|
|
const VectorX<Variable> tvec{prog.NewIndeterminates<1>("t")};
|
|
|
const VectorX<Variable> xvec{prog.NewIndeterminates<1>("x")};
|
|
|
const Variable& t = tvec(0);
|
|
|
const Variable& x = xvec(0);
|
|
|
|
|
|
// The desired order of the polynomial function approximations (noting that
|
|
|
// the SOS multipliers, constructed below, have order d-2).
|
|
|
const int d = 8;
|
|
|
|
|
|
// Extract the polynomial dynamics.
|
|
|
context->get_mutable_continuous_state_vector().SetAtIndex(0, x);
|
|
|
system.CalcTimeDerivatives(*context, derivatives.get());
|
|
|
|
|
|
// Define the domain bounds on t: 0 ≤ t ≤ T.
|
|
|
const double T = 1.;
|
|
|
const Polynomial gt = Polynomial{t * (T - t)};
|
|
|
|
|
|
// Define the (symmetric) domain bounds on x: ‖x‖ ≤ x_bound
|
|
|
// (X = {x | gx ≥ 0}).
|
|
|
const double x_bound = 1.;
|
|
|
const Polynomial gx = Polynomial{-(x - x_bound) * (x + x_bound)};
|
|
|
|
|
|
// Define the terminal set: ‖x(T)‖ ≤ 0.1 (X_T = {x | gxT ≥ 0}).
|
|
|
const Polynomial gxT{-(x - 0.1) * (x + 0.1)};
|
|
|
|
|
|
// Define the ground-truth backward reachable set (for plotting only):
|
|
|
// ‖x(0)‖ < 0.5 (a.k.a. X₀).
|
|
|
const Polynomial gx0{-(x - 0.5) * (x + 0.5)};
|
|
|
|
|
|
// Set up and solve the following optimization problem:
|
|
|
// Inf w'l over v, w, qₓ, qt, qT, q₀, sₓ
|
|
|
// s.t. ℒv ≤ 0 on [0, T] × X (see below and Henrion and Korda 2014)
|
|
|
// v ≥ 0 on {T} × X_T
|
|
|
// w ≥ v + 1 on {0} × X
|
|
|
// w ≥ 0 on X
|
|
|
//
|
|
|
// The result is the T-horizon outer-approximated backward reachable set,
|
|
|
// B = X₀ = {x ∈ X | w ≥ 1}.
|
|
|
const Polynomial v{prog.NewFreePolynomial({t, x}, d)};
|
|
|
const Polynomial w{prog.NewFreePolynomial({x}, d)};
|
|
|
|
|
|
// ℒ operator, defined as v ↦ ℒv := ∂v/∂t + Σ_{i=1,…,n} ∂v/∂x fᵢ(t,x).
|
|
|
const Polynomial Lv{v.Jacobian(tvec).coeff(0) +
|
|
|
v.Jacobian(xvec).coeff(0) *
|
|
|
Polynomial((*derivatives)[0])};
|
|
|
|
|
|
// v decreases along trajectories over t ∈ [0, T], x ∈ X.
|
|
|
const Polynomial qx{prog.NewSosPolynomial({t, x}, d - 2).first};
|
|
|
const Polynomial qt{prog.NewSosPolynomial({t, x}, d - 2).first};
|
|
|
prog.AddSosConstraint(-Lv - qx * gx - qt * gt);
|
|
|
|
|
|
// v(t=T, x) is SOS for all x ∈ X_T. Note that this constraint, coupled with
|
|
|
// the one above, implies that v(t=0, x) ≥ 0 for all x ∈ B.
|
|
|
// Notice that, by replacing v_at_T with v, we may solve the "free final-time"
|
|
|
// problem.
|
|
|
const Polynomial v_at_T = v.EvaluatePartial({{t, T}});
|
|
|
const Polynomial qT{prog.NewSosPolynomial({x}, d - 2).first};
|
|
|
prog.AddSosConstraint(v_at_T - qT * gxT);
|
|
|
|
|
|
// w is an outer-approximation to B over x ∈ X.
|
|
|
const Polynomial v_at_0 = v.EvaluatePartial({{t, 0.}});
|
|
|
const Polynomial q0{prog.NewSosPolynomial({x}, d - 2).first};
|
|
|
prog.AddSosConstraint(w - v_at_0 - 1. - q0 * gx);
|
|
|
|
|
|
// w is SOS for all x ∈ X.
|
|
|
const Polynomial sx{prog.NewSosPolynomial({x}, d - 2).first};
|
|
|
prog.AddSosConstraint(w - sx * gx);
|
|
|
|
|
|
// w represents a *tight* outer-approximation.
|
|
|
prog.AddCost(MakeVolumetricCost(
|
|
|
w, std::make_pair(Bound{{x, -x_bound}}, Bound{{x, x_bound}})));
|
|
|
|
|
|
cout << " Program attributes: " << endl;
|
|
|
cout << " number of decision variables: " << prog.num_vars() << endl;
|
|
|
cout << " number of PSD constraints: "
|
|
|
<< prog.positive_semidefinite_constraints().size() << endl;
|
|
|
|
|
|
const solvers::MathematicalProgramResult result = Solve(prog);
|
|
|
DRAKE_DEMAND(result.get_solver_id() == solvers::MosekSolver::id());
|
|
|
DRAKE_DEMAND(result.is_success());
|
|
|
|
|
|
// Print the solution (if one is found).
|
|
|
cout << " Solution found with optimal cost: " << result.get_optimal_cost()
|
|
|
<< endl;
|
|
|
|
|
|
Environment w_env;
|
|
|
for (const auto& var : w.decision_variables()) {
|
|
|
w_env.insert(var, result.GetSolution(var));
|
|
|
}
|
|
|
const Polynomial w_sol{w.EvaluatePartial(w_env)};
|
|
|
|
|
|
// Serialize the result and generate Python plots. In particular, plot the
|
|
|
// true indicator function of B and w (its polynomial outer-approximation).
|
|
|
// Execute:
|
|
|
// > bazel run //examples/cubic_polynomial:backward_reachability -c dbg
|
|
|
// --config mosek
|
|
|
// > bazel run //common/proto:call_python_client_cli
|
|
|
const int N{1000};
|
|
|
Eigen::VectorXd x_val(N), w_val(N), v_val(N), ground_val(N);
|
|
|
for (int i = 0; i < N; i++) {
|
|
|
x_val[i] = x_bound * (2. * i / N - 1.);
|
|
|
w_val[i] = w_sol.Evaluate({{x, x_val[i]}});
|
|
|
ground_val[i] = (gx0.Evaluate({{x, x_val[i]}}) >= 0.) ? 1. : 0.;
|
|
|
}
|
|
|
CallPython("figure", 1);
|
|
|
CallPython("clf");
|
|
|
CallPython("plot", x_val, w_val);
|
|
|
CallPython("setvars", "x_val", x_val, "w_val", w_val);
|
|
|
CallPython("plot", x_val, ground_val);
|
|
|
CallPython("setvars", "x_val", x_val, "ground_val", ground_val);
|
|
|
CallPython("plt.xlabel", "x");
|
|
|
CallPython("plt.ylabel", "w, I_B");
|
|
|
}
|
|
|
|
|
|
} // namespace
|
|
|
} // namespace drake
|
|
|
|
|
|
int main() {
|
|
|
drake::ComputeBackwardReachableSet();
|
|
|
return 0;
|
|
|
}
|