Getting Started¶
leap-c turns a model predictive controller into a differentiable PyTorch layer. It wraps
acados’ state-of-the-art optimal-control solver AcadosOcpSolver into a layer whose
forward pass solves a (batched) optimal control problem and whose backward pass returns
exact gradients of the solution with respect to the problem’s parameters. Because it is an
ordinary torch.nn.Module, you can drop it into any model, compose it with neural networks, and call
loss.backward() straight through the solver — the basis for learning MPC parameters with
reinforcement or imitation learning.
The parametric optimal control problem¶
At each step leap-c solves a discrete-time parametric optimal control problem (OCP): given an initial state \(\bar{x}_0\) and a parameter vector \(p\), find the state and control trajectories that minimize the accumulated cost subject to the system dynamics and constraints.
\(x_k \in \mathbb{R}^{n_x}\) are the states and \(u_k \in \mathbb{R}^{n_u}\) the controls, over a horizon of \(N\) stages.
\(\bar{x}_0\) is the initial state (
x0), fixed for each solve.\(p\) collects the parameters — dynamics constants, cost weights, references — the quantities you set at runtime or learn.
\(\ell, \ell_N\) are the stage and terminal cost, \(f\) the dynamics, and \(h, h_N\) the path and terminal constraints.
Solving this problem defines a solution map from \((\bar{x}_0,\, p)\) to the first optimal control \(u_0^\star(\bar{x}_0,\, p)\) and the optimal value \(V(\bar{x}_0;\, p)\). leap-c makes this map differentiable in \(p\).
The differentiable-MPC interface¶
AcadosDiffMpcTorch implements the solution map above as a
torch.nn.Module. You build one from an AcadosOcp (the problem — dynamics, cost, constraints)
and an AcadosParameterManager (which numbers are
learnable), then call it like any module:
flowchart LR
OCP["AcadosOcp<br/>(dynamics, cost, constraints)"] --> DMPC
PM["AcadosParameterManager<br/>(differentiable vs. runtime params)"] --> DMPC
DMPC["AcadosDiffMpcTorch<br/>(torch.nn.Module)"] --> PIPE["your PyTorch model<br/>/ training loop"]
Its public interface is just a constructor and a forward:
class AcadosDiffMpcTorch(torch.nn.Module):
def __init__(
self,
ocp: AcadosOcp, # the OCP: dynamics, cost, constraints
parameter_manager: AcadosParameterManager, # which parameters are learnable
# ... optional solver / code-generation options
): ...
Its public interface is just a constructor and a forward:
class AcadosDiffMpcTorch(torch.nn.Module):
def __init__(
self,
ocp: AcadosOcp, # the OCP: dynamics, cost, constraints
parameter_manager: AcadosParameterManager, # which parameters are learnable
# ... optional solver / code-generation options
): ...
def forward(
self,
x0: torch.Tensor, # initial state, shape (B, n_x)
u0: torch.Tensor | None = None, # optional fixed first action
params: dict[str, torch.Tensor] | None = None, # parameter overrides p, by name
ctx: AcadosDiffMpcCtx | None = None, # solver context, for warm starts
) -> tuple[AcadosDiffMpcCtx, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
# returns (ctx, u0, x, u, value)
...
forward maps directly onto the OCP: it fixes \(x_0 = \bar{x}_0\) from x0, applies the named
overrides in params as \(p\), solves, and returns the first optimal control u0 \(= u_0^\star\),
the full state and control trajectories x, u, the optimal value \(= V(\bar{x}_0;\, p)\), and a
ctx used to warm-start later solves. (Omitting u0 evaluates the state value \(V(\bar{x}_0)\);
passing it evaluates the state-action value \(Q(\bar{x}_0, u_0)\).) Calling value.sum().backward()
then propagates exact gradients \(\partial u_0^\star / \partial p\) and \(\partial V / \partial p\)
back to the learnable parameter tensors.
Minimal example¶
A complete use of the layer — register the learnable parameters, build the module, solve, and differentiate through the solve — reads like this:
import numpy as np
import torch
from leap_c.torch import AcadosDiffMpcTorch
from leap_c.parameters import AcadosParameterManager
# 1. Register parameters. The manager returns CasADi symbols to use in the OCP.
# differentiable=True -> learnable, exact gradients (shared acados p_global)
# differentiable=False -> runtime model value, not learned (acados p)
manager = AcadosParameterManager(N_horizon=20)
Q = manager.register_parameter("Q", np.array([1.0, 1.0]), differentiable=True)
R = manager.register_parameter("R", np.array([0.1]), differentiable=True)
mass = manager.register_parameter("mass", np.array([1.0]), differentiable=False)
# 2. Build the AcadosOcp from those symbols (dynamics, cost, constraints).
# acados-specific; see "Define a differentiable MPC" for the full definition.
ocp = build_point_mass_ocp(manager, Q, R, mass)
# 3. Wrap the OCP and its parameters into a differentiable torch.nn.Module.
diff_mpc = AcadosDiffMpcTorch(ocp, manager)
# 4. Solve a batch of OCPs for an initial state, overriding a parameter by name.
x0 = torch.tensor([[1.0, 0.0]], dtype=torch.float64) # (batch, n_x)
Q_t = torch.tensor([[1.0, 1.0]], dtype=torch.float64, requires_grad=True)
ctx, u0, x, u, value = diff_mpc(x0, params={"Q": Q_t})
# u0 is the first optimal action; differentiate the solution end to end.
value.sum().backward() # exact gradients w.r.t. differentiable params
print(u0, Q_t.grad)
build_point_mass_ocp stands in for the acados OCP construction, which uses the symbols
returned above in the dynamics and cost. See
Define a differentiable MPC for the full, runnable version
(the OCP body spelled out) and the accompanying explanation.
Next steps¶
Define a differentiable MPC — a complete, runnable example that builds the
AcadosOcp, wraps it, and differentiates through a solve.Parameter management — declaring learnable vs. runtime parameters and varying them across the horizon.
Core API — the main user-facing classes.