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.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)

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]  # 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.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
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: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 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.