Skip to content

Explicit mode ​

Instead of symbolic tokens, pass solve typed strategy instances: full control over each component's configuration, no completion to guess.

When you want this ​

  • building the strategy configuration programmatically (from a data structure, a search over hyperparameters, etc.),

  • reusing one carefully-configured strategy instance across several solve calls,

  • avoiding any ambiguity about which options went where.

Basic usage ​

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

discretizer = OptimalControl.Collocation(grid_size=100, scheme=:trapeze)
modeler = OptimalControl.ADNLP(backend=:optimized)
solver = OptimalControl.Ipopt(max_iter=1000, print_level=0)

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

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

Each component goes with its keyword: discretizer, modeler, solver. The component types are not exported, so they are written with the module name: OptimalControl.Collocation(...), OptimalControl.Ipopt(...). This needs nothing loaded beyond using OptimalControl (and the solver package, as always).

Partial components ​

Give one component, and the other two are completed the same way descriptive mode completes a partial token list — first match, top to bottom in methods():

julia
methods()[1]  # what a bare solve(ocp) completes to
(:collocation, :adnlp, :ipopt, :cpu)
julia
sol = solve(
    ocp;
    solver=OptimalControl.Ipopt(max_iter=2000, print_level=0),
    display=true,
)
▫ This is OptimalControl 2.2.1-beta, solving with: collocation → adnlp → ipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation
   ├─ Modeler: adnlp
   └─ Solver: ipopt (max_iter = 2000, print_level = 0)

solver=Ipopt(...) alone completes to Collocation() (first discretizer) and ADNLP() (first modeler compatible with Ipopt) — visible in the printed configuration above. Mixing a custom component with defaults works the same way for any subset:

julia
sol = solve(ocp;
    discretizer=OptimalControl.Collocation(grid_size=200, scheme=:trapeze),
    solver=OptimalControl.Ipopt(max_iter=100, print_level=0),
    display=false,
)

Per-component options ​

Every option a strategy accepts is set when it is constructed, never routed in from solve afterwards, unlike descriptive mode. Here, a fourth-order scheme on a coarser grid, and tighter Ipopt tolerances:

julia
discretizer = OptimalControl.Collocation(grid_size=50, scheme=:gauss_legendre_2)
modeler = OptimalControl.ADNLP(backend=:optimized)
solver = OptimalControl.Ipopt(max_iter=1000, tol=1e-10, acceptable_tol=1e-8, print_level=0)

sol = solve(ocp; discretizer=discretizer, modeler=modeler, solver=solver)
println("objective = ", objective(sol))   # the exact value is 6
▫ This is OptimalControl 2.2.1-beta, solving with: collocation → adnlp → ipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation (grid_size = 50, scheme = gauss_legendre_2)
   ├─ Modeler: adnlp (backend = optimized)
   └─ Solver: ipopt
     acceptable_tol = 1.0e-8, max_iter = 1000, print_level = 0, … (+1)

objective = 6.000000000000005

On 50 steps, the fourth-order scheme recovers the exact cost to about machine precision, where the default second-order scheme on 250 steps is off by about 1e-4.

Undeclared options still need bypass (or its alias force), same reasoning as in descriptive mode — but here it's passed straight into the constructor, not through route_to:

julia
solver = OptimalControl.Ipopt(
    max_iter=500, print_level=0, mumps_print_level=bypass(1)
)

A flat option keyword handed to solve itself, rather than to the component constructor, is rejected — even one a completed default component would otherwise recognize:

julia
julia> solve(
           ocp;
           discretizer=OptimalControl.Collocation(),
           backend=:generic,
           display=false,
       )
IncorrectArgument → _throw_strategy_explicit_option, explicit_validation.jl:90
│
│  Strategy option cannot be passed directly to solve in explicit mode
│
│  Got       option `backend` for modeler
│  Expected  options accepted by the solve action or explicit component instances
│
│  Context   solve explicit option validation
│  Hint      Construct ADNLP with backend=... and pass it as `modeler` to solve
└─

The error names the strategy that owns the option and tells you how to fix it: construct that strategy with the option set, and pass the configured instance in. route_to plays no role here: it only makes sense in descriptive mode, where options do not yet belong to a concrete instance.

Mixing modes is forbidden ​

Symbolic tokens and typed components cannot appear in the same call:

julia
julia> solve(
           ocp, :adnlp, :ipopt;
           discretizer=OptimalControl.Collocation(),
           display=false,
       )
IncorrectArgument → _explicit_or_descriptive, mode_detection.jl:60
│
│  Cannot mix explicit components with symbolic description
│
│  Got       explicit components + symbolic description (:adnlp, :ipopt)
│  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)
└─

Pick one: solve(ocp, :collocation, :adnlp, :ipopt; options...) or solve(ocp; discretizer=..., modeler=..., solver=...).

Inspecting the components you built ​

julia
solver = OptimalControl.Ipopt(max_iter=1000, tol=1e-6, print_level=0)
opts = options(solver)

is_user(opts, :max_iter)
true
julia
is_default(opts, :mu_strategy)
true
julia
opts[:max_iter]
1000
julia
collect(keys(opts))
6-element Vector{Symbol}:
 :max_iter
 :tol
 :linear_solver
 :mu_strategy
 :sb
 :print_level

See also ​

  • Solve overview — the two styles side by side.

  • Options — the descriptive-mode counterpart (route_to, automatic routing) to per-component construction here.

  • Choosing a method — the full strategy catalogue these constructors build from.