Switching an acados optimal control problem (OCP) to differential dynamic programming (DDP) can require a change to the problem itself. A bound represented as a cost penalty lets the optimizer trade a violation against other costs. A hard constraint instead restricts which solutions are admissible. These two formulations can select different trajectories even when they use the same dynamics and target.
The acados feature guide describes its DDP route as requiring an unconstrained OCP and points to L2 and Huber penalty helpers. The current Python implementation makes an initial-state exception explicit: its DDP checks reject path constraints, while allowing initial-state constraints. Here, “unconstrained” concerns the additional optimization constraints; the model dynamics still define how the state evolves.
That distinction matters when moving an existing model predictive controller to DDP. First decide whether you want the original constrained task, a task that tolerates weighted violations, or a feasibility problem whose objective is to reduce violations. Then choose the formulation that expresses that decision.
Adding a penalty and removing a constraint are separate edits
The Python API describes formulate_constraint_as_L2_penalty as adding a penalty to the current cost. The implementation constructs a violation from the supplied lower and upper bounds and adds it at the selected initial, path or terminal stage. That helper does not remove an existing constraint entry.
For a scalar expression g with bounds l ≤ g ≤ u, the L2 helper uses the violation v = max(0, g − u, l − g). The quadratic contribution has the form ½ × weight × v². Inside the interval, this contribution is zero. Outside it, the optimizer pays according to the distance from the violated bound.
Suppose a control bound already appears in the OCP’s constraint fields. Adding a penalty for the same control expression leaves the original bound there too. Merely changing nlp_solver_type to DDP therefore does not convert that constrained OCP into a penalty formulation. Inspect the constraint fields that remain after your intended conversion.
The tools serve different purposes:
| Operation | Effect on the mathematical problem |
|---|---|
| Add an L2 penalty for one expression | Adds a weighted violation term to the chosen stage’s cost; existing constraints remain |
| Add a Huber penalty | Adds a differently shaped penalty; the current implementation requires a CONVEX_OVER_NONLINEAR cost at that stage |
Call translate_to_feasibility_problem() with defaults |
Removes the original cost, turns supported constraints into L2 penalties with weights of 1.0, then removes the constraint entries |
These are not interchangeable ways to enable one solver option. In particular, keeping a tracking or time objective alongside penalties creates a different task from minimizing constraint violations alone.
A larger L2 weight can still leave a smaller violation
Consider a dimensionless scalar example, separate from any acados run. The task prefers y = 2, with objective ½(y − 2)², but also requires y ≤ 1.
With the hard constraint, the best admissible value is y = 1. Now replace the bound with a penalty and minimize:
J(y) = ½(y − 2)² + ½ρ × max(0, y − 1)²
For positive ρ, the minimum lies above 1. Differentiating in that region gives (y − 2) + ρ(y − 1) = 0, so y = (2 + ρ)/(1 + ρ). The remaining violation is 1/(1 + ρ).
| Formulation | Minimizing y | Violation above 1 |
|---|---|---|
| Hard upper bound | 1 | 0 |
| L2 penalty, ρ = 1 | 1.5 | 0.5 |
| L2 penalty, ρ = 9 | 1.1 | 0.1 |
| L2 penalty, ρ = 99 | 1.01 | 0.01 |
The residual violation is a property of this penalized objective, even at its exact mathematical minimum. Running an optimizer for more iterations would not turn that minimum into the hard-constrained answer. Increasing the weight changes the compromise; it does not merely ask for a more accurate solution to the old problem.
This is also why the units of a penalty matter. If the same length error is expressed in millimeters rather than meters, its squared numeric value increases by a factor of one million. Preserving the same quadratic contribution requires dividing its weight by one million. A weight only has meaning together with its residual definition, units and other objective terms.
The hour-glass example solves a feasibility problem in its DDP branch
The official hour-glass example is especially useful because its filename mentions time-optimal motion, while its branches solve different formulations.
The DDP call passes True as solve_feasibility_problem. In that branch, the script calls translate_to_feasibility_problem() before constructing the solver. The terminal time objective is assigned only when that flag is false. The sequential quadratic programming (SQP) calls use that false branch.
The DDP iteration check in this file consequently belongs to the transformed feasibility task. It should not be read as a speed comparison in which DDP and SQP both minimized the same time objective under the same hard constraints. The example is code showing a configuration and an assertion; the numbers in that assertion are not measurements from this article.
The conversion method also has keep_cost, keep_x0 and parametric_x0 options. Their defaults are false in the inspected implementation. Choosing keep_cost=True retains the original objective before adding penalties. Choosing to retain an initial-state constraint changes another part of the formulation. Read those options explicitly when the starting state must stay fixed instead of becoming another penalty term.
Choose what a violation means before comparing solvers
For a tracking preference, a weighted deviation may express exactly the compromise you want. For a bound that defines admissibility, retain a formulation and solver path that represent that bound as a constraint. If the immediate task is finding a trajectory with small violations, a feasibility formulation can be useful—but its objective answers that narrower question.
Before comparing results, write down the original objective, the remaining hard constraints, every penalized expression and its units, the penalty weights, and how the initial state is handled. Evaluate the original constraint expressions on the returned trajectory as well as reading the solver’s status. A low penalty value is interpretable only with those scales and the original bounds in view.
This fits inside the broader MPC control loop: predicting a trajectory and repeatedly applying its first input does not decide which constraints belong in that trajectory problem. That choice is made when the OCP is formulated.
The documentation and moving main source were checked on October 16, 2026. This is a source-based explanation; no acados solve, timing comparison or robot experiment was run. The scalar table is an analytical example defined above.