Skip to content

Solve overview ​

solve is the entry point for the direct methods: transcribe the problem, hand it to an NLP solver, get a Solution back. This page shows the quickest way to call it, how to read what it prints, and the two ways to steer it away from its defaults.

Quick start ​

julia
using OptimalControl
using NLPModelsIpopt

t0 = 0
tf = 1
x0 = [-1, 0]

ocp = @def begin
    t ∈ [t0, tf], time
    x = (q, v) ∈ R², state
    u ∈ R, control
    x(t0) == x0
    x(tf) == [0, 0]
    ẋ(t) == [v(t), u(t)]
    0.5∫(u(t)^2) → min
end

sol = solve(ocp)
▫ This is OptimalControl 2.2.1-beta, solving with: collocation → adnlp → ipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation
   ├─ Modeler: adnlp
   └─ Solver: ipopt

▫ This is Ipopt version 3.14.19, running with linear solver MUMPS 5.9.0.

Number of nonzeros in equality constraint Jacobian...:     1754
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:      250

Total number of variables............................:      752
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        0
                     variables with only upper bounds:        0
Total number of equality constraints.................:      504
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  5.0000000e-03 1.10e+00 2.24e-14   0.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  6.0000960e+00 2.22e-16 1.78e-15 -11.0 6.08e+00    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 1

                                   (scaled)                 (unscaled)
Objective...............:   6.0000960015360381e+00    6.0000960015360381e+00
Dual infeasibility......:   1.7763568394002505e-15    1.7763568394002505e-15
Constraint violation....:   2.2204460492503131e-16    2.2204460492503131e-16
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   1.7763568394002505e-15    1.7763568394002505e-15


Number of objective function evaluations             = 2
Number of objective gradient evaluations             = 2
Number of equality constraint evaluations            = 2
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 2
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 1
Total seconds in IPOPT                               = 4.672

EXIT: Optimal Solution Found.

A solver package must be loaded before calling solve: here using NLPModelsIpopt provides the default :ipopt. Without it, solve raises an ExtensionError naming the missing package and the using statement that fixes it (see Installation).

Reading the display ​

Before running, solve prints the method it is about to use, then a Configuration box with one line per strategy: the discretizer, the modeler and the solver. Each line lists the options that differ from the strategy's defaults. Above, no option was passed, so the box only names the strategies. Pass some options, and they appear on the line of the strategy that receives them:

julia
sol = solve(ocp; grid_size=50, scheme=:trapeze, max_iter=100, print_level=0)
▫ This is OptimalControl 2.2.1-beta, solving with: collocation → adnlp → ipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation (grid_size = 50, scheme = trapeze)
   ├─ Modeler: adnlp
   └─ Solver: ipopt (max_iter = 100, print_level = 0)

The box now shows:

  • Discretizer: collocation, with grid_size = 50 and scheme = trapeze;

  • Modeler: adnlp, with no option of its own;

  • Solver: ipopt, with max_iter = 100 and print_level = 0.

Each option was sent to the strategy that declares it: this is the automatic routing described in Options. Some defaults depend on the :cpu/:gpu parameter; they are shown too, tagged [cpu-dependent] (or [gpu-dependent]). For instance, MadNLP's linear solver:

julia
using MadNLP
sol = solve(ocp, :madnlp; print_level=MadNLP.ERROR)
▫ This is OptimalControl 2.2.1-beta, solving with: collocation → adnlp → madnlp (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation
   ├─ Modeler: adnlp
   └─ Solver: madnlp (linear_solver = MumpsSolver [cpu-dependent], print_level = ERROR)

Default values are not shown. To list every option of a strategy with its default, use describe, for instance describe(:ipopt) (see Options).

Turning the display off ​

julia
sol = solve(ocp; display=false)

It silences both the configuration box and the solver's own log. Useful once you trust a configuration and are solving in a loop, a test, or a script.

The defaults ​

Calling solve(ocp) with no strategy tokens is equivalent to:

julia
solve(ocp, :collocation, :adnlp, :ipopt, :cpu)

This quadruplet is the first entry of methods(), and completion always takes the first match, top to bottom (see Choosing a method for the full list and how partial descriptions are completed).

Two ways to steer it ​

solve can be pointed at a different strategy in two styles:

  • descriptive: symbolic tokens, e.g. solve(ocp, :madnlp) (see Choosing a method),

  • explicit: typed components, e.g. solve(ocp; solver=OptimalControl.MadNLP()) (see Explicit mode).

Use one style or the other in a call, not both: a typed component together with symbolic tokens is rejected.

julia
julia> solve(ocp, :collocation; solver=OptimalControl.MadNLP())
IncorrectArgument → _explicit_or_descriptive, mode_detection.jl:60
│
│  Cannot mix explicit components with symbolic description
│
│  Got       explicit components + symbolic description (:collocation,)
│  Expected  either explicit components OR symbolic description
│
│  Context   solve function call
│  Hint      Use either solve(ocp; discretizer=..., modeler=..., solver=...) OR solve(ocp, :collocation, :adnlp, :ipopt)
└─

When it fails ​

A solve that does not converge still returns a Solution. Check it rather than assuming success. Here we stop Ipopt before its first iteration:

julia
sol = solve(ocp; max_iter=0, display=false, print_level=0)
println("successful: ", successful(sol))    # did the solver report success?
println("status:     ", status(sol))        # a Symbol, e.g. :first_order, :max_iter
println("violation:  ", constraints_violation(sol))
successful: false
status:     max_iter
violation:  1.1

The solution holds the initial guess, which violates the boundary conditions. A converged solve reports successful(sol) == true, the status :first_order and a negligible violation:

julia
sol = solve(ocp; display=false, print_level=0)
println("successful: ", successful(sol))
println("status:     ", status(sol))
println("violation:  ", constraints_violation(sol))
successful: true
status:     first_order
violation:  2.220446049250313e-16

See also ​

  • Choosing a method — the full list of strategies and how partial descriptions are completed.

  • Options — routing, inspection with describe, disambiguation.

  • Explicit mode — build and pass typed components directly.

  • Initial guess — every way to hand solve a starting point.