smooth_feedback
Control and estimation on Lie groups
Loading...
Searching...
No Matches
mpc.hpp
1// Copyright (C) 2022 Petter Nilsson. MIT License.
2
3#pragma once
4
5#include <memory>
6
7#include <smooth/concepts/lie_group.hpp>
8#include <smooth/lie_sparse.hpp>
9
10#include "ocp_to_qp.hpp"
11#include "qp_solver.hpp"
12#include "time.hpp"
13#include "utils/sparse.hpp"
14
15namespace smooth::feedback {
16
17namespace detail {
18
22template<Time T, LieGroup X>
23struct XDes
24{
25 T t0;
26 std::function<X(T)> xdes = [](T) -> X { return Default<X>(); };
27 std::function<Tangent<X>(T)> dxdes = [](T) -> Tangent<X> { return Tangent<X>::Zero(); };
28
29 X operator()(const double t_rel) const
30 {
31 const T t_abs = time_trait<T>::plus(t0, t_rel);
32 return xdes(t_abs);
33 };
34
35 Tangent<X> jacobian(const double t_rel) const
36 {
37 const T t_abs = time_trait<T>::plus(t0, t_rel);
38 return dxdes(t_abs);
39 }
40};
41
45template<Time T, Manifold U>
46struct UDes
47{
48 T t0;
49 std::function<U(T)> udes = [](T) -> U { return Default<U>(); };
50
51 U operator()(const double t_rel) const
52 {
53 const T t_abs = time_trait<T>::plus(t0, t_rel);
54 return udes(t_abs);
55 };
56};
57
68template<LieGroup X>
69struct MPCObj
70{
71 static constexpr auto Nx = Dof<X>;
72
73 // public members
74 X xf_des = Default<X>();
75 Eigen::Matrix<double, Nx, Nx> Qtf = Eigen::Matrix<double, Nx, Nx>::Identity();
76
77 // private members
78 Eigen::SparseMatrix<double> hess{1 + 2 * Nx + 1, 1 + 2 * Nx + 1};
79
80 // functor members
81
82 // function f(t, x0, xf, q) = (1/2) xf' Q xf + q_0
83 double operator()(
84 const double, const X &, [[maybe_unused]] const X & xf, [[maybe_unused]] const Eigen::Vector<double, 1> & q) const
85 {
86 assert(q(0) == 1.);
87 assert(xf_des.isApprox(xf, 1e-4));
88 if constexpr (false) {
89 const auto e = rminus(xf, xf_des);
90 return 0.5 * e.dot(Qtf * e) + q(0);
91 } else {
92 return 1.;
93 }
94 }
95
96 Eigen::RowVector<double, 1 + 2 * Nx + 1>
97 jacobian(const double, const X &, [[maybe_unused]] const X & xf, const Eigen::Vector<double, 1> &)
98 {
99 assert(xf_des.isApprox(xf, 1e-4));
100 return Eigen::RowVector<double, 1 + 2 * Nx + 1>::Unit(1 + 2 * Nx);
101 }
102
103 std::reference_wrapper<const Eigen::SparseMatrix<double>>
104 hessian(const double, const X &, [[maybe_unused]] const X & xf, const Eigen::Vector<double, 1> &)
105 {
106 assert(xf_des.isApprox(xf, 1e-4));
107
108 set_zero(hess);
109
110 for (auto i = 0u; i < Nx; ++i) {
111 for (auto j = 0u; j < Nx; ++j) {
112 if (Qtf(i, j) != 0) { hess.coeffRef(1 + i, 1 + j) = Qtf(i, j); }
113 }
114 }
115
116 hess.makeCompressed();
117 return hess;
118 }
119};
120
124template<Time T, LieGroup X, Manifold U, typename F, diff::Type DT>
125struct MPCDyn
126{
127 static constexpr auto Nx = Dof<X>;
128 static constexpr auto Nu = Dof<U>;
129
130 // public members
131 F f;
132 T t0{};
133
134 // functor members
135 Tangent<X> operator()(const double t, const X & x, const U & u)
136 {
137 if constexpr (requires(F & fvar, T tvar) { fvar.set_time(tvar); }) { f.set_time(time_trait<T>::plus(t0, t)); }
138
139 return f(x, u);
140 }
141
142 Eigen::Matrix<double, Nx, 1 + Nx + Nu> jacobian(const double t, const X & x, const U & u)
143 {
144 if constexpr (requires(F & fvar, T tvar) { fvar.set_time(tvar); }) { f.set_time(time_trait<T>::plus(t0, t)); }
145
146 const auto & [fval, df] = diff::dr<1, DT>(f, smooth::wrt(x, u));
147
148 Eigen::Matrix<double, Nx, 1 + Nx + Nu> ret;
149 ret << Eigen::Vector<double, Nx>::Zero(), df;
150 return ret;
151 }
152};
153
165template<Time T, LieGroup X, Manifold U>
167{
168 static constexpr auto Nx = Dof<X>;
169 static constexpr auto Nu = Dof<U>;
170
171 // public members
172 std::shared_ptr<XDes<T, X>> xdes;
173 std::shared_ptr<UDes<T, U>> udes;
174
175 Eigen::Matrix<double, Nx, Nx> Q = Eigen::Matrix<double, Nx, Nx>::Identity();
176 Eigen::Matrix<double, Nu, Nu> R = Eigen::Matrix<double, Nu, Nu>::Identity();
177
178 // private members
179 Eigen::SparseMatrix<double> hess{1 + Nx + Nu, 1 + Nx + Nu};
180
181 // functor members
182
183 // function f(x, t, u) = (1/2) * (x' Q x + u' R u)
184 Eigen::Vector<double, 1>
185 operator()([[maybe_unused]] const double t_rel, [[maybe_unused]] const X & x, [[maybe_unused]] const U & u) const
186 {
187 assert((*xdes)(t_rel).isApprox(x, 1e-4));
188 assert((*udes)(t_rel).isApprox(u, 1e-4));
189 if constexpr (false) {
190 const auto ex = rminus(x, (*xdes)(t_rel));
191 const auto eu = rminus(u, (*udes)(t_rel));
192 return Eigen::Vector<double, 1>{0.5 * ex.dot(Q * ex) + 0.5 * eu.dot(R * eu)};
193 } else {
194 return Eigen::Vector<double, 1>{0.};
195 }
196 }
197
198 Eigen::RowVector<double, 1 + Nx + Nu>
199 jacobian([[maybe_unused]] const double t_rel, [[maybe_unused]] const X & x, [[maybe_unused]] const U & u)
200 {
201 assert((*xdes)(t_rel).isApprox(x, 1e-4));
202 assert((*udes)(t_rel).isApprox(u, 1e-4));
203 return Eigen::RowVector<double, 1 + Nx + Nu>::Zero();
204 }
205
206 std::reference_wrapper<const Eigen::SparseMatrix<double>>
207 hessian([[maybe_unused]] const double t_rel, [[maybe_unused]] const X & x, [[maybe_unused]] const U & u)
208 {
209 assert((*xdes)(t_rel).isApprox(x, 1e-4));
210 assert((*udes)(t_rel).isApprox(u, 1e-4));
211
212 set_zero(hess);
213
214 for (auto i = 0u; i < Nx; ++i) {
215 for (auto j = 0u; j < Nx; ++j) {
216 if (Q(i, j) != 0) { hess.coeffRef(1 + i, 1 + j) = Q(i, j); }
217 }
218 }
219 for (auto i = 0u; i < Nu; ++i) {
220 for (auto j = 0u; j < Nu; ++j) {
221 if (Q(i, j) != 0) { hess.coeffRef(1 + Nx + i, 1 + Nx + j) = R(i, j); }
222 }
223 }
224
225 hess.makeCompressed();
226 return hess;
227 }
228};
229
233template<Time T, LieGroup X, Manifold U, typename F, diff::Type DT>
234struct MPCCR
235{
236 static constexpr auto Nx = Dof<X>;
237 static constexpr auto Nu = Dof<U>;
238 static constexpr auto Ncr = std::invoke_result_t<F, X, U>::SizeAtCompileTime;
239
240 // public members
241 F f;
242 T t0{};
243
244 // private members
245 Eigen::SparseMatrix<double> jac{Ncr, 1 + Nx + Nu};
246
247 // functor members
248 Eigen::Vector<double, Ncr> operator()(const double t, const X & x, const U & u)
249 {
250 if constexpr (requires(F & fvar, T tvar) { fvar.set_time(tvar); }) { f.set_time(time_trait<T>::plus(t0, t)); }
251
252 return f(x, u);
253 }
254
255 std::reference_wrapper<const Eigen::SparseMatrix<double>> jacobian(const double t, const X & x, const U & u)
256 {
257 if constexpr (requires(F & fvar, T tvar) { fvar.set_time(tvar); }) { f.set_time(time_trait<T>::plus(t0, t)); }
258
259 const auto & [fval, df] = diff::dr<1, DT>(f, smooth::wrt(x, u));
260 block_write(jac, 0, 1, df);
261
262 jac.makeCompressed();
263 return jac;
264 }
265};
266
275template<LieGroup X>
276struct MPCCE
277{
278 static constexpr auto Nx = Dof<X>;
279
280 // public members
281 X x0_fix = Default<X>();
282
283 // private members
284 Eigen::SparseMatrix<double> dexpinv_tmp = d_exp_sparse_pattern<X>;
285 Eigen::SparseMatrix<double> jac{Nx, 1 + 2 * Nx + 1};
286
287 // functor members
288 Tangent<X> operator()(const double, const X & x0, const X &, const Eigen::Vector<double, 1> &) const
289 {
290 return rminus(x0, x0_fix);
291 }
292
293 std::reference_wrapper<const Eigen::SparseMatrix<double>>
294 jacobian(const double, const X & x0, const X &, const Eigen::Vector<double, 1> &)
295 {
296 dr_expinv_sparse<X>(dexpinv_tmp, rminus(x0, x0_fix));
297
298 jac.middleCols(1, Nx) = dexpinv_tmp;
299 jac.makeCompressed();
300 return jac;
301 }
302};
303
304} // namespace detail
305
310{
317 std::size_t K{10};
318
322 double tf{1};
323
327 bool warmstart{true};
328
333};
334
344template<LieGroup X, Manifold U>
346{
347 static constexpr auto Nx = Dof<X>;
348 static constexpr auto Nu = Dof<U>;
349
351 Eigen::Matrix<double, Nx, Nx> Q = Eigen::Matrix<double, Nx, Nx>::Identity();
353 Eigen::Matrix<double, Nx, Nx> Qtf = Eigen::Matrix<double, Nx, Nx>::Identity();
355 Eigen::Matrix<double, Nu, Nu> R = Eigen::Matrix<double, Nu, Nu>::Identity();
356};
357
372template<
373 Time T,
374 LieGroup X,
375 Manifold U,
376 typename F,
377 typename CR,
378 std::size_t Kmesh = 4,
379 diff::Type DT = diff::Type::Default>
380class MPC
381{
382 static constexpr auto Ncr = std::invoke_result_t<CR, X, U>::SizeAtCompileTime;
383
384public:
405 inline MPC(
406 F && f, CR && cr, Eigen::Vector<double, Ncr> && crl, Eigen::Vector<double, Ncr> && cru, MPCParams && prm = {})
407 : xdes_{std::make_shared<detail::XDes<T, X>>()}, udes_{std::make_shared<detail::UDes<T, U>>()},
408 mesh_{(prm.K + Kmesh - 1) / Kmesh},
409 ocp_{
410 .theta = {},
411 .f = {.f = std::forward<F>(f)},
412 .g = {.xdes = xdes_, .udes = udes_},
413 .cr = {.f = std::forward<CR>(cr)},
414 .crl = std::move(crl),
415 .cru = std::move(cru),
416 .ce = {},
417 .cel = Eigen::Vector<double, Dof<X>>::Zero(),
418 .ceu = Eigen::Vector<double, Dof<X>>::Zero(),
419 },
420 prm_{std::move(prm)}, qp_solver_{prm_.qp}
421 {
422 detail::ocp_to_qp_allocate<DT>(qp_, work_, ocp_, mesh_);
423 ocp_to_qp_update<diff::Type::Analytic>(qp_, work_, ocp_, mesh_, prm_.tf, *xdes_, *udes_);
424 qp_solver_.analyze(qp_);
425 }
427 inline MPC(
428 const F & f,
429 const CR & cr,
430 const Eigen::Vector<double, Ncr> & crl,
431 const Eigen::Vector<double, Ncr> & cru,
432 const MPCParams & prm = {})
433 : MPC(F(f), CR(cr), Eigen::Vector<double, Ncr>(crl), Eigen::Vector<double, Ncr>(cru), MPCParams(prm))
434 {}
436 inline MPC() = default;
438 inline MPC(const MPC &) = default;
440 inline MPC(MPC &&) = default;
442 inline MPC & operator=(const MPC &) = default;
444 inline MPC & operator=(MPC &&) = default;
446 inline ~MPC() = default;
447
458 inline std::pair<U, QPSolutionStatus> operator()(
459 const T & t,
460 const X & x,
461 std::optional<std::reference_wrapper<std::vector<U>>> u_traj = std::nullopt,
462 std::optional<std::reference_wrapper<std::vector<X>>> x_traj = std::nullopt)
463 {
464 static constexpr auto Nx = Dof<X>;
465 static constexpr auto Nu = Dof<U>;
466
467 const auto N = mesh_.N_colloc();
468 const auto xvar_L = Nx * (N + 1);
469 const auto xvar_B = 0u;
470 const auto uvar_B = xvar_L;
471
472 // update problem
473 xdes_->t0 = t;
474 udes_->t0 = t;
475 ocp_.theta.xf_des = (*xdes_)(prm_.tf);
476 ocp_.f.t0 = t;
477 ocp_.cr.t0 = t;
478 ocp_.ce.x0_fix = x;
479
480 // transcribe to QP
481 ocp_to_qp_update_dyn<diff::Type::Analytic>(qp_, work_, ocp_, mesh_, prm_.tf, *xdes_, *udes_);
482 if constexpr (requires(CR & crvar, T tvar) { crvar.set_time(tvar); }) {
483 // only update if running constraints are time-dependent
484 ocp_to_qp_update_cr<diff::Type::Analytic>(qp_, work_, ocp_, mesh_, prm_.tf, *xdes_, *udes_);
485 }
486 ocp_to_qp_update_ce<diff::Type::Analytic>(qp_, work_, ocp_, mesh_, prm_.tf, *xdes_, *udes_);
487 qp_.A.makeCompressed();
488 qp_.P.makeCompressed();
489
490 // solve QP
491 const auto & sol = qp_solver_.solve(qp_, warmstart_);
492
493 // output solution trajectories
494 if (u_traj.has_value()) {
495 u_traj.value().get().resize(N);
496 for (const auto & [i, tau] : zip(std::views::iota(0u, N), mesh_.all_nodes())) {
497 const double t_rel = prm_.tf * tau;
498 u_traj.value().get()[i] = (*udes_)(t_rel) + sol.primal.template segment<Nu>(uvar_B + i * Nu);
499 }
500 }
501 if (x_traj.has_value()) {
502 x_traj.value().get().resize(N + 1);
503 for (const auto & [i, tau] : zip(std::views::iota(0u, N + 1), mesh_.all_nodes())) {
504 const double t_rel = prm_.tf * tau;
505 x_traj.value().get()[i] = (*xdes_)(t_rel) + sol.primal.template segment<Nx>(xvar_B + i * Nx);
506 }
507 }
508
509 // save solution to warmstart next iteration
510 if (prm_.warmstart) {
511 // clang-format off
512 if (sol.code == QPSolutionStatus::Optimal || sol.code == QPSolutionStatus::MaxTime || sol.code == QPSolutionStatus::MaxIterations) {
513 warmstart_ = sol;
514 }
515 // clang-format on
516 }
517
518 return {rplus((*udes_)(0), sol.primal.template segment<Nu>(uvar_B)), sol.code};
519 }
520
524 inline void set_udes(std::function<U(T)> && u_des) { udes_->udes = std::move(u_des); }
525
529 inline void set_udes(const std::function<U(T)> & u_des) { set_udes(std::function<U(T)>(u_des)); }
530
537 template<typename Fun>
538 requires(std::is_same_v<std::invoke_result_t<Fun, Scalar<U>>, U>)
539 inline void set_udes_rel(Fun && f, T t0 = T(0))
540 {
541 set_udes([t0 = t0, f = std::forward<Fun>(f)](T t_abs) -> U {
542 const double t_rel = time_trait<T>::minus(t_abs, t0);
543 return std::invoke(f, t_rel);
544 });
545 }
546
550 inline void set_xdes(std::function<X(T)> && x_des, std::function<Tangent<X>(T)> && dx_des)
551 {
552 xdes_->xdes = std::move(x_des);
553 xdes_->dxdes = std::move(dx_des);
554 }
555
559 inline void set_xdes(const std::function<X(T)> & x_des, const std::function<Tangent<X>(T)> & dx_des)
560 {
561 set_xdes(std::function<X(T)>(x_des), std::function<Tangent<X>(T)>(dx_des));
562 }
563
573 template<typename Fun>
574 requires(std::is_same_v<std::invoke_result_t<Fun, Scalar<X>>, X>)
575 inline void set_xdes_rel(Fun && f, T t0 = T(0))
576 {
577 std::function<X(T)> x_des = [t0 = t0, f = f](T t_abs) -> X {
578 const double t_rel = time_trait<T>::minus(t_abs, t0);
579 return std::invoke(f, t_rel);
580 };
581
582 std::function<Tangent<X>(T)> dx_des = [t0 = t0, f = f](T t_abs) -> Tangent<X> {
583 const double t_rel = time_trait<T>::minus(t_abs, t0);
584 return std::get<1>(diff::dr<1, DT>(f, wrt(t_rel)));
585 };
586
587 set_xdes(std::move(x_des), std::move(dx_des));
588 }
589
593 inline void set_weights(const MPCWeights<X, U> & weights)
594 {
595 ocp_.g.R = weights.R;
596 ocp_.g.Q = weights.Q;
597 ocp_.theta.Qtf = weights.Qtf;
598 }
599
603 inline void reset_warmstart() { warmstart_ = {}; }
604
605private:
606 // linearization
607 std::shared_ptr<detail::XDes<T, X>> xdes_;
608 std::shared_ptr<detail::UDes<T, U>> udes_;
609
610 // collocation mesh
611 Mesh<Kmesh, Kmesh> mesh_{};
612
613 // internal optimal control problem
614 OCP<
615 X,
616 U,
617 detail::MPCObj<X>,
618 detail::MPCDyn<T, X, U, F, DT>,
619 detail::MPCIntegrand<T, X, U>,
620 detail::MPCCR<T, X, U, CR, DT>,
621 detail::MPCCE<X>>
622 ocp_;
623
624 // parameters
625 MPCParams prm_{};
626
627 // internal allocation
628 detail::OcpToQpWorkmemory work_;
629 QuadraticProgramSparse<double> qp_;
630
631 // internal QP solver
632 QPSolver<QuadraticProgramSparse<double>> qp_solver_;
633
634 // last solution stored for warmstarting
635 std::optional<QPSolution<-1, -1, double>> warmstart_{};
636};
637
638} // namespace smooth::feedback
Model-Predictive Control (MPC) on Lie groups.
Definition: mpc.hpp:381
~MPC()=default
Default destructor.
MPC & operator=(const MPC &)=default
Default copy assignment.
void set_xdes(const std::function< X(T)> &x_des, const std::function< Tangent< X >(T)> &dx_des)
Set the desired state trajectory (absolute time, rvalue version)
Definition: mpc.hpp:559
void reset_warmstart()
Reset initial guess for next iteration to zero.
Definition: mpc.hpp:603
void set_xdes(std::function< X(T)> &&x_des, std::function< Tangent< X >(T)> &&dx_des)
Set the desired state trajectory and velocity (absolute time)
Definition: mpc.hpp:550
MPC(const MPC &)=default
Default copy constructor.
MPC & operator=(MPC &&)=default
Default move assignment.
MPC(MPC &&)=default
Default move constructor.
void set_xdes_rel(Fun &&f, T t0=T(0))
Set the desired state trajectry (relative time, automatic differentiation).
Definition: mpc.hpp:575
void set_weights(const MPCWeights< X, U > &weights)
Update MPC weights.
Definition: mpc.hpp:593
void set_udes(std::function< U(T)> &&u_des)
Set the desired input trajectory (absolute time)
Definition: mpc.hpp:524
std::pair< U, QPSolutionStatus > operator()(const T &t, const X &x, std::optional< std::reference_wrapper< std::vector< U > > > u_traj=std::nullopt, std::optional< std::reference_wrapper< std::vector< X > > > x_traj=std::nullopt)
Calculate new MPC input.
Definition: mpc.hpp:458
MPC()=default
Default constructor.
MPC(F &&f, CR &&cr, Eigen::Vector< double, Ncr > &&crl, Eigen::Vector< double, Ncr > &&cru, MPCParams &&prm={})
Create an MPC instance.
Definition: mpc.hpp:405
MPC(const F &f, const CR &cr, const Eigen::Vector< double, Ncr > &crl, const Eigen::Vector< double, Ncr > &cru, const MPCParams &prm={})
Same as above but for lvalues.
Definition: mpc.hpp:427
void set_udes_rel(Fun &&f, T t0=T(0))
Set the desired input trajectory (relative time).
Definition: mpc.hpp:539
void set_udes(const std::function< U(T)> &u_des)
Set the desired input trajectory (absolute time, rvalue version)
Definition: mpc.hpp:529
Collocation mesh of interval [0, 1].
Definition: mesh.hpp:63
A Time type supports right-addition with a double, and subtraction of two time types should be conver...
Definition: time.hpp:25
void set_zero(MeshValue< Deriv > &mv)
Reset MeshValue to zeros.
Formulate optimal control problem as a quadratic program.
Quadratic Program solver.
Sparse matrix utilities.
void block_write(Eigen::SparseMatrix< double, Options > &dest, Eigen::Index row0, Eigen::Index col0, const Source &source, double scale=1, bool upper_only=false)
Write block into a sparse matrix.
Definition: sparse.hpp:70
Parameters for MPC.
Definition: mpc.hpp:310
double tf
MPC time horizon (seconds)
Definition: mpc.hpp:322
std::size_t K
Minimum number of collocation points.
Definition: mpc.hpp:317
QPSolverParams qp
QP solvers parameters.
Definition: mpc.hpp:332
bool warmstart
Enable warmstarting.
Definition: mpc.hpp:327
Objective weights for MPC.
Definition: mpc.hpp:346
Eigen::Matrix< double, Nx, Nx > Q
Running state cost.
Definition: mpc.hpp:351
Eigen::Matrix< double, Nx, Nx > Qtf
Final state cost.
Definition: mpc.hpp:353
Eigen::Matrix< double, Nu, Nu > R
Running input cost.
Definition: mpc.hpp:355
Options for solve_qp.
Definition: qp_solver.hpp:30
Eigen::SparseMatrix< Scalar, Eigen::RowMajor > A
Inequality matrix.
Definition: qp.hpp:74
Eigen::SparseMatrix< Scalar > P
Positive semi-definite square cost (only upper trianglular part is used)
Definition: qp.hpp:64
MPC end constraint function.
Definition: mpc.hpp:277
MPC running constraints (jacobian obtained via automatic differentiation).
Definition: mpc.hpp:235
MPC dynamics (derivatives obtained via autodiff).
Definition: mpc.hpp:126
MPC cost function.
Definition: mpc.hpp:70
Wrapper for desired input and its derivative.
Definition: mpc.hpp:47
Wrapper for desired trajectory and its derivative.
Definition: mpc.hpp:24
Trait class to specify Time operations.
Definition: time.hpp:14