Lightweight C++ utilities for building and solving MPC problems with CasADi. Includes runtime MPC, JIT-compiled MPC, and CMake-integrated compiled MPC solvers.
- CasADi (install script)
- IPOPT or FATROP (optional solver backends)
- Eigen3
- Python3 + NumPy + pybind11
- matplotlibcpp17 (examples/benchmarks use it via pybind11)
- doxygen (for docs; uses
doc/doxygen-awesome-csssubmodule)
mkdir build
cd build
cmake ..
make
sudo make installfind_package(simple_casadi_mpc REQUIRED)
target_link_libraries(my_target PRIVATE simple_casadi_mpc)MPC: simplest runtime solver; easiest for quick validation.JITMPC: JIT-compiles on the first solve for faster subsequent runs; expect a startup lag (cacheable with ccache).CompiledMPC: builds solver code at CMake time; best steady-state speed with no runtime lag.
Limitation for CompiledMPC: the solver backend (IPOPT/FATROP/...) and its parameters are fixed at build time.
The library solves the following discrete-time finite-horizon optimal control problem at every solve() call:
Symbol mapping:
| Symbol | Code |
|---|---|
state at stage Problem::nx()) |
|
control at stage Problem::nu()) |
|
prediction horizon (Problem::horizon()) |
|
Problem::stage_cost(x, u, k) |
|
Problem::terminal_cost(x) |
|
discretization of Problem::dynamics(x, u) per DynamicsType
|
|
each call to Problem::add_constraint(Equality / Inequality, ...) adds one |
|
Problem::set_state_bound(...) |
|
Problem::set_input_bound(...) |
|
runtime parameters via Problem::parameter(...) / reference_trajectory(...)
|
|
first argument to MPC::solve(x, ...)
|
For DynamicsType::ContinuesForwardEuler, ContinuesModifiedEuler, and ContinuesRK4, DynamicsType::Discretized the user supplies
Soft constraints add slack
Define your MPC problem by deriving from simple_casadi_mpc::Problem and overriding dynamics, stage_cost, and (optionally) terminal_cost:
#include "simple_casadi_mpc/simple_casadi_mpc.hpp"
class MyProblem : public simple_casadi_mpc::Problem {
public:
MyProblem()
// DynamicsType, nx, nu, horizon (N), dt
: Problem(DynamicsType::ContinuesRK4, /*nx=*/2, /*nu=*/1, /*N=*/20, /*dt=*/0.05) {
// Optional: per-stage input/state bounds
set_input_bound(Eigen::VectorXd::Constant(1, -1.0),
Eigen::VectorXd::Constant(1, 1.0));
// Optional: runtime-tunable parameters (updated each `solve` call)
x_ref_ = reference_trajectory("x_ref"); // shape (nx, N)
}
// Continuous dynamics: return dx/dt for {ContinuesForwardEuler|ContinuesModifiedEuler|ContinuesRK4}
// Discrete dynamics: return x_{k+1} for {Discretized}
casadi::MX dynamics(casadi::MX x, casadi::MX u) override {
return casadi::MX::vertcat({x(1), u});
}
// Per-stage cost. `k` is the stage index in [0, horizon).
casadi::MX stage_cost(casadi::MX x, casadi::MX u, size_t k) override {
casadi::MX e = x - x_ref_(casadi::Slice(), k);
return casadi::MX::mtimes(e.T(), e) + 0.1 * casadi::MX::mtimes(u.T(), u);
}
// Terminal cost on x_N (default returns 0 if not overridden).
casadi::MX terminal_cost(casadi::MX x) override {
return 10.0 * casadi::MX::mtimes(x.T(), x);
}
private:
casadi::MX x_ref_;
};Solve loop:
auto prob = std::make_shared<MyProblem>();
simple_casadi_mpc::MPC mpc(prob); // or JITMPC / CompiledMPC
Eigen::VectorXd x = /* initial state */;
for (...) {
// Update parameters declared via `parameter(...)` / `reference_trajectory(...)`
casadi::DM x_ref_dm = /* (nx, N) DM */;
Eigen::VectorXd u = mpc.solve(x, {{"x_ref", x_ref_dm}});
x = prob->simulate(x, u, sim_dt);
}Add equality / inequality path constraints applied at every stage:
// In your Problem subclass constructor:
// Inequality g(x, u) <= 0 (e.g. circular obstacle: r^2 - |xy - c|^2 <= 0)
add_constraint(ConstraintType::Inequality, [](casadi::MX x, casadi::MX u) {
using namespace casadi;
MX dx = x(0) - 1.0, dy = x(1) - 0.5;
return MX::vertcat({0.5 * 0.5 - (dx * dx + dy * dy)});
});
// Equality g(x, u) = 0
add_constraint(ConstraintType::Equality, [](casadi::MX x, casadi::MX u) {
return casadi::MX::vertcat({u(0) + u(1)}); // e.g. force a coupling
});To apply a constraint only to a subset of stages, use add_constraint_at(type, func, start, end). Range semantics match set_input_bound:
// Only at stage 0 (single-stage form, omit `end`)
add_constraint_at(ConstraintType::Equality,
[&](casadi::MX x, casadi::MX) { return x - x_target; }, 0);
// Only on stages [N-3, N) (terminal-band)
add_constraint_at(ConstraintType::Inequality,
[&](casadi::MX x, casadi::MX) { return x - x_safe; }, N - 3, N);add_soft_constraint(type, func, w1, w2) introduces non-negative per-stage slack variables
- Inequality
$g(x, u) \le 0 ;\to; g - s \le 0$ . - Equality
$h(x, u) = 0 ;\to; |h| \le s$ , i.e.$h - s \le 0$ and$-h - s \le 0$ .
// Soft inequality with L1 weight 1e3 (default) and no L2 term.
add_soft_constraint(ConstraintType::Inequality, my_constraint);
// Mixed L1 + L2 penalty.
add_soft_constraint(ConstraintType::Inequality, my_constraint, /*w1=*/1e2, /*w2=*/1.0);The default w2 = 0 gives a pure L1 (exact) penalty; w2 > 0 adds an L2 (smooth) term. Large w1 recovers hard-constraint behavior; small w1 lets the optimizer trade violation against the rest of the cost. Hard add_constraint and soft add_soft_constraint may be mixed on the same problem.
Stage-specific variants are also available — same start/end semantics as add_constraint_at:
add_soft_constraint_at(ConstraintType::Inequality, my_constraint,
/*w1=*/1e3, /*w2=*/0.0,
/*start=*/N - 3, /*end=*/N); // only the last 3 stagesfind_package(simple_casadi_mpc REQUIRED)
# Generate a compiled solver (codegen step happens at build time)
add_simple_casadi_mpc_codegen(
<solver_target_name> # e.g., my_problem
<codegen_cpp> # e.g., my_problem_codegen.cpp (derives Problem)
EXPORT_SOLVER_NAME <export_name> # optional, default is <solver_target_name>_compiled_solver
INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} # where your Problem header lives
SOLVER_NAME <casadi_solver> # optional; default is fatrop (e.g., ipopt/fatrop/...)
# LINK_LIBS ... # optional; extra solver libs if needed
)
# Link your executable against the generated solver + simple_casadi_mpc
add_executable(<your_exe> main.cpp
${<solver_target_name>_COMPILED_SOLVER_CONFIG_SOURCE})
target_include_directories(<your_exe> PRIVATE
${CMAKE_CURRENT_SOURCE_DIR} ${<solver_target_name>_CODEGEN_DIR})
target_link_libraries(<your_exe> PRIVATE
simple_casadi_mpc::simple_casadi_mpc
${<solver_target_name>_COMPILED_SOLVER})Drives a frictionless point mass to the origin (position and velocity feedback).
From: example/double_integrator_mpc_example.cpp
Cartpole swing-up and balance (problem setup from the linked gists).
From: example/cartpole_mpc_example.cpp
https://gist.github.com/mayataka/ef178130d52b5b06d4dd8bb2c8384c54 https://gist.github.com/mayataka/bc08faa63a94d8b48ceba77cc79c7ccc
Rotary inverted pendulum swing-up with torque limits that force a multi-phase motion.
From: example/inverted_pendulum_mpc_example.cpp
Differential-drive robot from top-left to bottom-right while avoiding circular obstacles and respecting velocity limits.
From: example/diff_drive_mpc_example.cpp
Same diff-drive setup with a single circular obstacle, comparing add_constraint (hard) and add_soft_constraint (soft) for the obstacle. With a large penalty weight the soft formulation matches the hard one; with a small weight the optimizer prefers cutting through the obstacle if the tracking gain dominates the violation cost.
From: example/diff_drive_soft_constraint_example.cpp
| Backend | Default config | Best for |
|---|---|---|
| IPOPT | default_ipopt_config() |
General-purpose NLP; default and easiest to debug |
| FATROP | default_fatrop_config() |
OCP-structured problems; fastest for MPC |
| qpOASES | default_qpoases_config() |
SQP method backed by qpOASES (linear-quadratic) |
Pass a copy of one of these dicts as the third argument to MPC / JITMPC, mutating any keys you need:
auto cfg = simple_casadi_mpc::MPC::default_fatrop_config();
cfg["fatrop.max_iter"] = 100;
simple_casadi_mpc::MPC mpc(prob, "fatrop", cfg);FATROP requires CasADi to be built with WITH_FATROP=ON and WITH_BLASFEO=ON; see install_casadi.sh.
- Reach for
MPCwhile iterating on the problem itself; no compile step. - Switch to
JITMPConce the model is stable. The firstsolvetriggers agcc -O3 -march=nativecompile; cache it withccache(the defaultdefault_jit_options()already does so). - Use
CompiledMPC(with theadd_simple_casadi_mpc_codegenCMake helper) when you want zero runtime startup cost and the solver backend is fixed.
Override the defaults from JITMPC::default_jit_options():
auto opts = simple_casadi_mpc::JITMPC::default_jit_options();
opts["compiler"] = "clang";
opts["flags"] = "-O2 -fno-fast-math";
opts["verbose"] = true;
simple_casadi_mpc::JITMPC mpc("my_prob", prob, "ipopt",
simple_casadi_mpc::MPC::default_ipopt_config(),
opts);Problem::parameter(name, rows, cols) returns a symbolic MX; update it at each solve:
mpc.solve(x, {{"x_ref", x_ref_dm}, {"obstacle", obs_dm}});reference_trajectory(name) is a shorthand for parameter(name, nx, horizon) whose column k is consumed at stage k.
Two simple-casadi-mpc-specific options can be passed in the config dict (consumed before being forwarded to CasADi):
mapsum_stage_cost(defaulttrue): build the stage-cost sum via MapSum so first-order AD stays loop-shaped.expand_inner_functions(defaulttrue): SX-expand per-stage F/L/G before mapping for faster JIT compilation.
Both default to true. If your stage_cost has stage-dependent branching beyond per-stage parameter slicing, the library auto-falls-back to a per-stage loop (warning emitted) and you can also set mapsum_stage_cost = false explicitly.
Problem(DynamicsType, nx, nu, horizon, dt) uses a uniform std::vector<double> of length horizon instead:
std::vector<double> dts(N);
for (size_t k = 0; k < N; ++k) dts[k] = 0.02 + 0.005 * k; // refine the prediction
MyProblem(N, dts);The continuous integrator (f_d) is built with Problem::dt() returns the stage-0 step for backward compatibility, while Problem::dt(k) and Problem::dts() give per-stage access. Problem::has_uniform_dt() reports whether all stages share the same step.
MPC::solve caches the previous x, lam_x, lam_g internally and feeds them to the next solve, so closed-loop simulations naturally benefit. Solver-side warm start is also enabled in default_ipopt_config() (ipopt.warm_start_init_point = "yes").
Runtime comparisons for cartpole MPC solver variants.
- Fetch submodules:
git submodule update --init --recursive- Generate docs:
cd doc
doxygen Doxyfile- Open
doc/build/html/index.html.









