Skip to content

Build and optimize

ocp.solve(...) is a one-shot wrapper: it builds the collocation NLP, then optimizes it with IPOPT. Those two steps can be run separately. That is the way to change numeric bounds or pinned parameters without assembling the CasADi graph again.

There is no separate “parameterize” call. Bounds after build are changed with set_constraints. The initial guess is applied inside optimize (initial_guess=, or the previous primal when warm_start=True).

After build

optimize does not write bounds. set_constraints writes bounds; optimize then runs IPOPT.

May change without a new build How
Box on \(x_i\), \(u_i\), \(p_i\), \(z_i\) (path, or t_0 / t_f) set_constraints, then optimize
Numeric bounds on a path or boundary expression already in st same
Pinned parameter value same, e.g. (3.0, "==", sys.p[0])
\(t_0\) / \(T\) pins and bounds t_0(sys.t) / t_f(sys.t) in set_constraints
IPOPT options optimize(ipopt_options=...)
Constant initial guess optimize(initial_guess=...)
Starting value of \(T\) (not its bounds) optimize(tf_guess=...)
Reuse of the last primal optimize(warm_start=True) (default)

Every \(x_i\), \(u_i\), \(p_i\), \(z_i\) already has an NLP box (default \(\pm\infty\)), so a simple bound on those symbols may be set even if it was omitted from the original st. A derived path residual (\(x^2\), \(x-u\), …) may be retuned only if that expression was in st at build.

set_constraints is a patch on ocp.constraints: omitted entries keep their last bounds; matched entries are replaced; a new box on \(x,u,p,z\) is appended. A later build() transcribes this updated list (mesh refine does not restore the original st).

Requires st and/or build again
Model \(f\) or System st + build
max / min those calls drop the compiled NLP
New path or boundary expression st + build
control_intervals, polynomial_degree, collocation_points build
Multi-phase Phase list or junction continuity Phase.constraints + build (set_constraints is single-phase)

Worked cases: path bound Bryson–Denham; parameter pin ligninolytic investment (plot_figure2.py, not the baseline lignin_decomposition.py). Tests: tests/test_nlp_reuse.py.

solve

sol = ocp.solve(
    control_intervals=100,
    polynomial_degree=5,
    collocation_points="legendre",
    initial_guess=None,
    ipopt_options=None,
    tf_guess=1.0,
    nlpsol_options=None,
)
Argument Default Values
control_intervals 100 positive integer
polynomial_degree 5 1–9
collocation_points "legendre" "legendre", "radau"
initial_guess fill (not a process trajectory) dict of symbol → constant
ipopt_options print_level 0 (see Troubleshooting) IPOPT keys
tf_guess 1.0 scale of \(T\) and of free switch times
nlpsol_options None top-level CasADi nlpsol options, e.g. {"expand": True}

solve always starts IPOPT cold (warm_start=False). Mesh meaning: Transcription. Constant guesses: Guesses, mesh, scaling.

build

ocp.build(
    control_intervals=100,
    polynomial_degree=5,
    collocation_points="legendre",
    initial_guess=None,
    tf_guess=1.0,
)

build transcribes \(f\), \(J\), and the constraint expressions declared in st onto a collocation mesh. The compiled NLP is stored on the Ocp. It returns ocp (no solution object). What may change after this call is listed under After build.

set_constraints

ocp.set_constraints([
    (0.0, "==", eco_opt.t_0(sys.x[0])),
    (1.0 / 5.0, ">=", sys.y.position),
    (new_p, "==", sys.p[0]),
])

Same tuple syntax as st. Boxes on decision variables and numeric bounds on expressions already in the NLP are written here; see After build. A new residual requires st and build again.

Call build first. Multi-phase problems cannot use set_constraints; change Phase.constraints and build again.

optimize

sol = ocp.optimize(
    initial_guess=None,
    ipopt_options=None,
    tf_guess=None,
    warm_start=True,
    nlpsol_options=None,
)

nlpsol_options is a dict of top-level CasADi nlpsol options (expand, print_time, jit, ...). None keeps the default solver configuration (print_time=0, IPOPT print_level=0). An "ipopt" entry inside nlpsol_options is merged with ipopt_options; ipopt_options wins on conflicts. The cached solver is rebuilt when either dict changes between calls.

Runs IPOPT on the compiled NLP. After the first successful call, warm_start=True (default) uses the previous primal as \(x_0\). Pass initial_guess to replace that with per-component constants. tf_guess may be omitted after the first run if \(T\) is unchanged.

After set_constraints, pass warm_start=False (and tf_guess if \(T\) is free). A primal that sat on the old bound can make IPOPT exceed its iteration limit when the bound is loosened.

Returns OcpResults, the same type as solve.

Bound sweep (frozen mesh)

Complete Bryson–Denham retune of the position path bound without rebuilding:

import eco_opt


def trolley_model(t, x, u):
    y = eco_opt.Namespace(position=x[0], speed=x[1], acceleration=u[0])
    return [x[1], u[0]], y


sys = eco_opt.System(states=2, controls=1, model=trolley_model, name="trolley")
y = sys.y
ocp = eco_opt.Ocp()
ocp.min([0.5 * y.acceleration ** 2])
ocp.st(
    system=sys,
    constraints=[
        (0, "==", eco_opt.t_0(y.position)),
        (1, "==", eco_opt.t_0(y.speed)),
        (1, "==", eco_opt.t_f(sys.t)),
        (0, "==", eco_opt.t_f(y.position)),
        (-1, "==", eco_opt.t_f(y.speed)),
        (1.0 / 9.0, ">=", y.position),
    ],
)

ocp.build(control_intervals=20, polynomial_degree=5, tf_guess=1.0)
sol_a = ocp.optimize(warm_start=False, tf_guess=1.0)

ocp.set_constraints([(1.0 / 5.0, ">=", y.position)])
sol_b = ocp.optimize(warm_start=False, tf_guess=1.0)

print(sol_a.success, sol_a.objective, sol_a.x[:, 0].max())
print(sol_b.success, sol_b.objective, sol_b.x[:, 0].max())

sol_a kisses \(x=1/9\) with \(J^*\approx 4\). sol_b uses the looser bound \(x\le 1/5\) and a lower cost. Default optimize() (warm_start=True) on this loosening can return Maximum_Iterations_Exceeded.

The same pattern applies to a parameter pin (sys.p[i]) or a terminal state bound. Changing the ODE is not this pattern: that is st and build again.

Mesh refinement is also a new build. The previous sol is not interpolated onto the new grid (Guesses, mesh, scaling).