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
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
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():
methods()[1] # what a bare solve(ocp) completes to(:collocation, :adnlp, :ipopt, :cpu)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:
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:
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.000000000000005On 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:
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: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> 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
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.