Skip to content

Transcription

ocp.solve transcribes the continuous problem into a nonlinear program (NLP) and passes that NLP to IPOPT. It does not integrate the ODE and then search over \(u(t)\).

The control is piecewise constant on control_intervals equal pieces of \([t_0,T]\). The state is a polynomial of degree polynomial_degree on each piece, required to satisfy \(\dot{\mathbf{x}}=f\) at the collocation points. The running cost is a quadrature of those pieces. Decision variables are the control on each interval, the state at the knots and collocation nodes, the horizon if \(T\) is free, and any parameters.

The parameterization represents \(u\) as a step on each interval. A plot that interpolates sol.u as a continuous curve misrepresents the discretization. matplotlib.pyplot.stairs or step(..., where="post") matches the transcription. Snippets: Results and plots. sol.time is the coarse knot grid (interval boundaries). Interior collocation nodes enforce the ODE; they are not sol.time.

ocp.simulate(sol) integrates the same piecewise-constant \(u^*\) with CVODES. Agreement between the collocation state and the integrator indicates that the mesh resolved \(f\). It is not a second hypothesis.

Mesh

sol = ocp.solve(
    control_intervals=40,
    polynomial_degree=3,
    collocation_points="legendre",
    tf_guess=1.0,
)

Defaults are 100 intervals, degree 5, Legendre points, tf_guess=1.0. They are not the Goddard archive (59 intervals, tf_guess=200) or First solve (200 intervals, Radau, tf_guess=2). A typical first mesh for a new model is 20–40 intervals and degree 3.

Setting Role
control_intervals Number of piecewise-constant \(u\) pieces
polynomial_degree State polynomial degree on each piece (1–9)
collocation_points "legendre" or "radau"
tf_guess Scale of \(T\) (and of free switch times in multi-phase)

"legendre" matches the Goddard archive in this package. "radau" is often more reliable when \(u^*\) is discontinuous (bang–bang controls, pulses; allocation is one example). See non_diff and bang_free_tf.

Mesh refinement can change \(J\) by a small amount. That change is discretization error, not a change of mechanism.

What may appear in \(f\)

The model function is a CasADi expression graph, not a general Python program. Functions on symbols: CasADi expressions. A Python branch on a symbolic state or control is invalid:

# invalid — Python if on a CasADi MX
if x[0] > 0:
    rate = vmax * x[0] / (km + x[0])

A switch that depends on \(t\), \(\mathbf{x}\), or \(\mathbf{u}\) is written with casadi.if_else (or casadi.fmax / casadi.fabs). A Python if on a numeric value that is not a CasADi symbol (a Python float, not sys.p) is evaluated before the graph is built.

A jump in a forcing (if_else on time) is not a jump in the control. A bang–bang \(u(t)\) (allocation, thrust, a harvest switch) is a control structure; a cutoff written in \(f\) is a nonsmooth forcing. See non_diff.

Named diagnostics belong in the Namespace y. They are not extra ODEs. Algebraic closures (System(algebraics=...), model returns (dx, dz, y)) are collocation residuals \(g=0\), not fast ODEs. See algebraic_qss. Algebraics are single-phase only.

sol

Attribute Meaning
sol.success, sol.return_status IPOPT
sol.time knot times, length \(N+1\)
sol.x states on that grid, (N+1, nx)
sol.u piecewise-constant controls, (N, nu)
sol.tf, sol.objective horizon and \(J\) in the declared sense
sol.signal(expr) any expression in the system symbols, on sol.time

sol.objective already includes the sign of max versus min. If sol.success is false, see Troubleshooting.

Further reading

Direct collocation: Betts, Practical Methods for Optimal Control; Biegler, Nonlinear Programming (SIAM, 2010); Rao (2009). CasADi: Andersson et al. (2019). IPOPT: Wächter and Biegler (2006). The modeling workflow: Yop (Leek, 2016).