Skip to content

Getting started

Install

pip install "blendmpc[crocoddyl]"             # add ,quadruped for the Go2 tasks
pip install -e ".[crocoddyl,test]"            # from a clone, for development

The Crocoddyl backend installs from PyPI wheels. The acados backend needs the acados C library built from source with ACADOS_SOURCE_DIR and LD_LIBRARY_PATH set; blendmpc detects it at import time and all acados features (and tests) are skipped when absent.

Your first blend: residual RL in ~20 lines

import gymnasium as gym
import numpy as np

from blendmpc.blends import ResidualMPCEnv
from blendmpc.envs.pendulum import make_pendulum_problem, obs_to_state
from blendmpc.solvers.crocoddyl import CrocoddylMPC

# 1. An MPC policy: a Crocoddyl shooting problem + receding-horizon driver
mpc = CrocoddylMPC(lambda x0: make_pendulum_problem(x0, horizon=30),
                   max_iter=5, max_iter_first=300, u_init=np.array([0.2]))

# 2. Wrap any Gymnasium env: actions become corrections on top of the MPC
env = ResidualMPCEnv(gym.make("Pendulum-v1"), mpc, obs_to_state)

# 3. Zero residual == pure MPC. Train any RL agent on `env` to improve on it.
obs, _ = env.reset(seed=0)
done = False
while not done:
    obs, reward, terminated, truncated, info = env.step(np.zeros(1))
    done = terminated or truncated

Train SB3 SAC on it exactly as on any other env:

from stable_baselines3 import SAC
SAC("MlpPolicy", env).learn(total_timesteps=15_000)

The core interface

Every backend implements one method; everything else is inherited:

class MPCPolicy(ABC):
    def solve(self, x0, us_init=None, xs_init=None) -> MPCSolution: ...
    def action(self, x0) -> np.ndarray   # receding horizon + warm-start shift
    def reset(self) -> None              # call at episode boundaries

MPCSolution carries xs, us, cost, solved, and solver-specific info. Blends never see anything below this interface.

Practical notes (learned the hard way)

Warm starts can trap receding-horizon MPC

A solver warm-started from its own shifted plan inherits the basin of the first solve forever. If your task has a stationary "do nothing" state (a hanging pendulum, a robot at rest), pass a small symmetry-breaking u_init so the cold solve escapes it. See the regression test test_symmetry_breaking_cold_init_escapes_hanging_start.

Kinked costs break gradient-based solvers

Costs built on normalized angles (angle_normalize(θ)²) have gradient discontinuities that make DDP/SQP flip between plans. Use smooth surrogates (2(1-cos θ)) inside the OCP; evaluate closed-loop performance with whatever metric you like.