Solve: advanced options
This manual covers advanced option management for the solve function: how option routing works, how to disambiguate shared options with route_to, how to pass unknown options with bypass, and how to use introspection tools.
For basic usage, see Solve a problem.
Option routing system
When you call solve with keyword arguments, OptimalControl.jl automatically routes each option to the appropriate strategy (discretizer, modeler, or solver).
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
# Options are automatically routed
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 1.3.3-beta, solving with: collocation → adnlp → ipopt (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.421513713 seconds;
hprod backend ADNLPModels.EmptyADbackend: 4.022e-6 seconds;
jprod backend ADNLPModels.EmptyADbackend: 1.775e-6 seconds;
jtprod backend ADNLPModels.EmptyADbackend: 1.2e-6 seconds;
• Sparsity pattern detection of the Jacobian: 0.000136757 seconds.
• Coloring of the sparse Jacobian: 1.6444e-5 seconds.
• Allocation of the AD buffers for the sparse Jacobian: 5.46e-7 seconds.
jacobian backend ADNLPModels.SparseADJacobian: 0.615462672 seconds;
• Sparsity pattern detection of the Hessian: 0.666796044 seconds.
• Coloring of the sparse Hessian: 2.296e-5 seconds.
• Allocation of the AD buffers for the sparse Hessian: 2.690529252 seconds.
hessian backend ADNLPModels.SparseReverseADHessian: 3.665984569 seconds;
ghjvprod backend ADNLPModels.EmptyADbackend: 4.355e-6 seconds.How routing works
Each strategy declares its available options via metadata. When you pass an option:
- Lookup: The system checks which strategies recognize this option name
- Route: If exactly one strategy family (discretizer/modeler/solver) recognizes it, the option is routed there
- Validate: The option value is validated against the declared type and constraints
- Error: If no strategy recognizes the option, or if multiple families claim it, an error is raised
You can inspect a strategy's declared options using describe:
using CUDA
describe(:exa)Exa{CPU} (strategy)
├─ id: :exa
├─ hierarchy: Exa → AbstractNLPModeler → AbstractStrategy
├─ family: AbstractNLPModeler
├─ default parameter: CPU
├─ parameters: CPU, GPU
│
┌ Warning: CUDA is loaded but not functional. GPU backend may not work properly.
└ @ CTSolversCUDA ~/.julia/packages/CTSolvers/oXAcJ/ext/CTSolversCUDA.jl:29
├─ common options (1 option):
│ └─ base_type::DataType (default: Float64)
│ description: Base floating-point type used by ExaModels
│
├─ computed options for CPU:
│ └─ backend (exa_backend)::Any (default: nothing [computed])
│ description: Execution backend for ExaModels (CPU, GPU, etc.)
│
└─ computed options for GPU:
└─ backend (exa_backend)::Union{Nothing, KernelAbstractions.Backend} (default: CUDA.CUDAKernels.CUDABackend(false, false) [computed])
description: Execution backend for ExaModels (CPU, GPU, etc.)The output shows:
- Strategy ID: The symbol used to reference this strategy (
:exa) - Family: The abstract type family (
AbstractNLPModeler) - Default parameter: Default execution backend (
CPU) - Parameters: Available execution backends (
CPU,GPU) - Common options: Options shared across all parameters
- Option name and type
- Default value
- Description
- Computed options: Options that vary by parameter
- Parameter-specific defaults
- Whether the value is computed automatically
Ambiguous options and route_to
Ambiguity occurs when an option name exists in multiple strategies within the same method. Since a method always has exactly one discretizer, one modeler, and one solver, ambiguity only happens when strategies from different families share an option name.
For example, suppose :exa (modeler) and :madnlp (solver) both have an option called common_option_name. If you try to use it without disambiguation, you'll get an error:
# This will raise an error
solve(ocp, :exa, :madnlp; common_option_name=12)
# ERROR: IncorrectArgument: Option 'common_option_name' is ambiguous...Using route_to for disambiguation
Use route_to to explicitly specify which strategy should receive the option:
# Explicitly route to the :exa strategy
sol = solve(ocp, :exa, :madnlp;
common_option_name=route_to(exa=12),
max_iter=500,
print_level=MadNLP.ERROR
)The route_to function accepts keyword arguments with strategy names:
route_to(collocation=value)— route to the Collocation discretizerroute_to(adnlp=value)— route to the ADNLP modelerroute_to(exa=value)— route to the Exa modelerroute_to(ipopt=value)— route to the Ipopt solverroute_to(madnlp=value)— route to the MadNLP solverroute_to(uno=value)— route to the Uno solverroute_to(madncl=value)— route to the MadNCL solverroute_to(knitro=value)— route to the Knitro solver
You can use route_to even for non-ambiguous options, and combine routed and non-routed options:
using MadNLP
sol = solve(ocp, :madnlp;
grid_size=50, # auto-routed to discretizer
max_iter=route_to(madnlp=1000), # explicitly routed to solver
print_level=MadNLP.ERROR # auto-routed to solver
)▫ This is OptimalControl 1.3.3-beta, solving with: collocation → adnlp → madnlp (cpu)
📦 Configuration:
├─ Discretizer: collocation (grid_size = 50)
├─ Modeler: adnlp
└─ Solver: madnlp
linear_solver = MumpsSolver [cpu-dependent], max_iter = 1000, print_level = ERRORThe bypass mechanism
By default, solve uses strict validation: any option not recognized by a registered strategy raises an error. This prevents typos and ensures you're using valid options.
However, NLP solvers have many options, and not all of them are declared in OptimalControl's strategy metadata. For example, Ipopt has an option mumps_print_level for controlling MUMPS debug output:
mumps_print_level: Debug printing level for the linear solver MUMPS0: no printing; 1: Error messages only; 2: Error, warning, and main statistic messages; 3: Error and warning messages and terse diagnostics; ≥4: All information.
This option is not in the Ipopt strategy metadata. If you try to use it directly, you'll get an error:
julia> sol = solve(ocp, :ipopt; max_iter=100, mumps_print_level=1)ERROR: Control Toolbox Error ❌ Error: CTBase.Exceptions.IncorrectArgument, 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 💡 Suggestion: 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>))
To pass undeclared options, combine route_to with bypass:
julia> sol = solve(ocp, :ipopt; max_iter=100, mumps_print_level=route_to(ipopt=bypass(1)))▫ This is OptimalControl 1.3.3-beta, solving with: collocation → adnlp → ipopt (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.8.2. 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.03e-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.0000960015360247e+00 6.0000960015360247e+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.004 EXIT: Optimal Solution Found. • Solver: ✓ Successful : true │ Status : first_order │ Message : Ipopt/generic │ Iterations : 1 │ Objective : 6.000096001536025 └─ Constraints violation : 2.220446049250313e-16 • Boundary duals: [12.000192003072046, 6.000096001536023, -12.000192003072046, 6.000096001536035]
You must combine bypass with route_to because:
- If the option is unknown, the system needs to know which strategy should receive it
bypassforces the option through without validation
The bypass mechanism skips validation entirely. Use it only when:
- You need to pass an option to the underlying solver that isn't declared in the strategy metadata
- You're certain the option name and value are correct
Bypassed options are passed directly to the solver without type checking or validation.
Parameter token (CPU/GPU)
The 4th token in a method description specifies the execution backend: :cpu (default) or :gpu.
# Explicitly request CPU execution (this is the default)
sol = solve(ocp, :collocation, :adnlp, :ipopt, :cpu;
grid_size=50,
print_level=0
)▫ This is OptimalControl 1.3.3-beta, solving with: collocation → adnlp → ipopt (cpu)
📦 Configuration:
├─ Discretizer: collocation (grid_size = 50)
├─ Modeler: adnlp
└─ Solver: ipopt (print_level = 0)The parameter token automatically changes default options for GPU-capable strategies. For example:
Exa{GPU}uses a CUDA backend by defaultMadNLP{GPU}usesCUDSSSolveras the linear solver by default
For full GPU usage details, see Solve on GPU.
See also
- Basic solve: descriptive mode basics
- Explicit mode: using typed components
- GPU solving: GPU parameter and types