Skip to content

Options

Every keyword argument passed to solve ends up on exactly one strategy — the discretizer, the modeler, or the solver. This page covers how that routing works, the two escape hatches for the cases it doesn't handle automatically, and how to inspect where a value came from.

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

Option routing

solve runs in strict mode: every keyword you pass must be recognized by exactly one of the three strategies in play, or it's an error — never a silent no-op.

julia
sol = solve(ocp;
    grid_size=100,   # → Collocation (discretizer)
    show_time=true,  # → ADNLP (modeler)
    max_iter=500,    # → Ipopt (solver)
    print_level=0,   # → Ipopt (solver)
)
▫ This is OptimalControl 2.1.0-beta, solving with: collocationadnlpipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation (grid_size = 100)
   ├─ Modeler: adnlp (show_time = true)
   └─ Solver: ipopt (max_iter = 500, print_level = 0)

gradient backend ADNLPModels.ReverseDiffADGradient: 0.716547551 seconds;
hprod    backend ADNLPModels.EmptyADbackend: 6.279e-6 seconds;
jprod    backend ADNLPModels.EmptyADbackend: 1.252e-6 seconds;
jtprod   backend ADNLPModels.EmptyADbackend: 9.42e-7 seconds;
  • Sparsity pattern detection of the Jacobian: 0.000110867 seconds.
  • Coloring of the sparse Jacobian: 2.0791e-5 seconds.
  • Allocation of the AD buffers for the sparse Jacobian: 8.51e-7 seconds.
jacobian backend ADNLPModels.SparseADJacobian: 0.982995614 seconds;
  • Sparsity pattern detection of the Hessian: 1.374745245 seconds.
  • Coloring of the sparse Hessian: 2.6329e-5 seconds.
  • Allocation of the AD buffers for the sparse Hessian: 4.054680078 seconds.
hessian  backend ADNLPModels.SparseReverseADHessian: 5.752094379 seconds;
ghjvprod backend ADNLPModels.EmptyADbackend: 5.638e-6 seconds.

An option nobody recognizes is rejected with a "did you mean" suggestion:

julia
julia> solve(ocp, :ipopt; max_iter=100, mumps_print_level=1, display=false)
IncorrectArgument  _route_descriptive_options, descriptive_routing.jl:259

│  Unknown option provided

│  Got       option :mumps_print_level in method (:collocation, :adnlp, :ipopt, :cpu)
│  Expected  valid option name for one of the strategies

│  Context   route_options - unknown option validation
│  Hint      Did you mean?
- :print_level [distance: 6]
- :mu_strategy [distance: 11]
- :dual_inf_tol [distance: 11]
│            If you're confident this option exists for a specific strategy, use bypass() to skip validation:
│              custom_opt = route_to(<strategy_id>=bypass(<value>))
└─

Ambiguous options

If two strategies from different families declare the same option name, using it bare is ambiguous — solve won't guess which one you meant. Disambiguate with route_to, which takes a strategy id and a value:

julia
sol = solve(ocp, :exa, :madnlp;
    common_option_name=route_to(:exa, 12),
    max_iter=500,
)

route_to also accepts alternating id/value pairs, to send the same option name to several strategies with different values at once:

julia
sol = solve(ocp, :exa, :madnlp;
    common_option_name=route_to(:exa, 12, :madnlp, true),
)

route_to works even when there's no ambiguity to resolve — it's fine to use it just to be explicit:

julia
using MadNLP
sol = solve(ocp, :madnlp;
    grid_size=50,                     # auto-routed to the discretizer
    max_iter=route_to(:madnlp, 1000), # explicitly routed
    print_level=MadNLP.ERROR,         # auto-routed to the solver
)
▫ This is OptimalControl 2.1.0-beta, solving with: collocationadnlpmadnlp (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation (grid_size = 50)
   ├─ Modeler: adnlp
   └─ Solver: madnlp
     linear_solver = MumpsSolver [cpu-dependent], max_iter = 1000, print_level = ERROR

Undeclared solver options

The three strategies here declare their own options, but not every option the underlying solver accepts is declared — Ipopt's mumps_print_level, for instance, isn't in the strategy metadata, so it's rejected by strict validation (shown above). Combine route_to with bypass to force it through, unvalidated:

julia
sol = solve(ocp, :ipopt;
    max_iter=100,
    mumps_print_level=route_to(:ipopt, bypass(1)),
)
▫ This is OptimalControl 2.1.0-beta, solving with: collocationadnlpipopt (cpu)

  📦 Configuration:
   ├─ Discretizer: collocation
   ├─ Modeler: adnlp
   └─ Solver: ipopt (max_iter = 100, mumps_print_level = 1)

▫ This is Ipopt version 3.14.19, running with linear solver MUMPS 5.9.0.

Number of nonzeros in equality constraint Jacobian...:     1754
Number of nonzeros in inequality constraint Jacobian.:        0
Number of nonzeros in Lagrangian Hessian.............:      250

Total number of variables............................:      752
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        0
                     variables with only upper bounds:        0
Total number of equality constraints.................:      504
Total number of inequality constraints...............:        0
        inequality constraints with only lower bounds:        0
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  5.0000000e-03 1.10e+00 2.24e-14   0.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  6.0000960e+00 2.22e-16 1.78e-15 -11.0 6.08e+00    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 1

                                   (scaled)                 (unscaled)
Objective...............:   6.0000960015360381e+00    6.0000960015360381e+00
Dual infeasibility......:   1.7763568394002505e-15    1.7763568394002505e-15
Constraint violation....:   2.2204460492503131e-16    2.2204460492503131e-16
Variable bound violation:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   0.0000000000000000e+00    0.0000000000000000e+00
Overall NLP error.......:   1.7763568394002505e-15    1.7763568394002505e-15


Number of objective function evaluations             = 2
Number of objective gradient evaluations             = 2
Number of equality constraint evaluations            = 2
Number of inequality constraint evaluations          = 0
Number of equality constraint Jacobian evaluations   = 2
Number of inequality constraint Jacobian evaluations = 0
Number of Lagrangian Hessian evaluations             = 1
Total seconds in IPOPT                               = 0.005

EXIT: Optimal Solution Found.

bypass is needed in addition to route_to because route_to alone still validates against the strategy's declared options — bypass is what skips that check. force is a plain alias for bypass (force === bypass); use whichever name reads better: route_to(:ipopt, force(1)).

Both bypass and route_to return values of internal types (BypassValue, RoutedOption) — these types are imported but not exported, so only the functions ever appear in your code, not the type names.

Use bypass sparingly

It skips type checking and validation entirely. Reach for it only when you're certain the option name and value are correct and the strategy genuinely doesn't declare it.

Where a value came from

Every option on a built strategy instance knows whether it was set by you, left at its default, or computed from the problem:

julia
s = OptimalControl.Ipopt(max_iter=200)
println(has_option(s, :max_iter))      # true — Ipopt declares this option
println(has_option(s, :not_an_option)) # false
println(option_value(s, :max_iter))    # 200 — what will actually be used
println(option_value(s, :tol))         # 1.0e-8 — the strategy's own default

opts = options(s)
println(is_user(opts, :max_iter))      # true
println(is_default(opts, :tol))        # true
println(is_computed(opts, :max_iter))  # false
true
false
200
1.0e-8
true
true
false

option_source returns which of the three it was, as a Symbol:

julia
println(option_source(s, :max_iter))
println(option_source(s, :tol))
user
default

This is the same provenance information the 📦 Configuration table shows when solve prints its display (see Overview) — these functions let you query it programmatically instead of reading it off the printout.

Action options vs strategy options

Two keywords — init/initial_guess and display — are handled before routing even starts; they're never sent to a strategy. If a strategy happens to declare an option with the same name, the action option wins by default. route_to is the way around that, if you ever need to target the strategy's own option of that name explicitly instead.

See also

  • Choosing a method — the strategies these options are routed to.

  • Explicit mode — configure a strategy by passing options directly to its constructor instead of routing them through solve.