Skip to content

Explicit mode

Instead of symbolic tokens, pass solve typed strategy instances — full control over each component's configuration, no completion-order guessing.

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

disc = OptimalControl.Collocation(grid_size=100, scheme=:trapeze)
mod = OptimalControl.ADNLP(backend=:optimized)
sol = OptimalControl.Ipopt(max_iter=1000, print_level=0)

result = solve(ocp; discretizer=disc, modeler=mod, solver=sol)
▫ This is OptimalControl 2.1.0-beta, solving with: collocationadnlpipopt (cpu)

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

discretizer, modeler, and solver are just three of many keyword names — mode detection never looks at names, only at whether a keyword's value is a typed component (see Overview). Any keyword holding a typed instance triggers explicit mode.

The component types are imported but not @reexported by OptimalControl, which is why every constructor above is written OptimalControl.Collocation(...) rather than bare Collocation(...) — writing using CTDirect: Collocation yourself would also work, but the qualified spelling needs nothing extra loaded beyond using OptimalControl.

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]  # (:collocation, :adnlp, :ipopt, :cpu) — what a bare solve(ocp) completes to
(:collocation, :adnlp, :ipopt, :cpu)
julia
result = solve(ocp; solver=OptimalControl.Ipopt(max_iter=2000, print_level=0), display=true)
▫ This is OptimalControl 2.1.0-beta, solving with: collocationadnlpipopt (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
result = 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's constructed — never routed in from solve afterward, unlike descriptive mode:

julia
disc = OptimalControl.Collocation(grid_size=150, scheme=:gauss_legendre_2)
mod = OptimalControl.ADNLP(backend=:optimized, show_time=true)
sol = OptimalControl.Ipopt(max_iter=1000, tol=1e-8, print_level=5, acceptable_tol=1e-6)

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:70

│  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 exactly 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 don't yet belong to a concrete instance.

Mixing modes is forbidden

Symbolic tokens and typed components can't 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

  • Overview — how mode detection decides between the two styles.

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

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