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
solvecalls,avoiding any ambiguity about which options went where.
Basic usage
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: 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():
methods()[1] # (:collocation, :adnlp, :ipopt, :cpu) — what a bare solve(ocp) completes to(:collocation, :adnlp, :ipopt, :cpu)result = solve(ocp; solver=OptimalControl.Ipopt(max_iter=2000, print_level=0), display=true)▫ This is OptimalControl 2.1.0-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:
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:
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:
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> 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> 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
solver = OptimalControl.Ipopt(max_iter=1000, tol=1e-6, print_level=0)
opts = options(solver)
is_user(opts, :max_iter)trueis_default(opts, :mu_strategy)trueopts[:max_iter]1000collect(keys(opts))6-element Vector{Symbol}:
:max_iter
:tol
:linear_solver
:mu_strategy
:sb
:print_levelSee 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.