Architecture
CTSolvers provides the resolution infrastructure of the control-toolbox ecosystem — solvers, modelers, integrators, and abstract problem types — consumed by CTDirect.jl (direct methods) and CTFlows.jl (flows for indirect methods).
This page provides the complete architectural overview. Read it before diving into any specific guide.
Module Overview
CTSolvers modules, loaded in dependency order:
| # | Module | Responsibility |
|---|---|---|
| 1 | Optimization | Abstract problem types (AbstractOptimizationProblem), BuiltModel/NoCache, build_model/build_solution |
| 2 | Modelers | NLP backend adapters: Modelers.ADNLP, Modelers.Exa |
| 3 | DOCP | AbstractDiscretizer, DiscretizedModel (an OCP paired with its discretizer), discretize stub, nlp_model/ocp_solution conveniences |
| 4 | Solvers | NLP solver wrappers: Solvers.Ipopt, Solvers.MadNLP, Solvers.MadNCL, Solvers.Knitro, Solvers.Uno |
| 5 | Integrators | ODE integrator wrapper Integrators.SciML, integration results (AbstractIntegrationResult, PiecewiseIntegrationResult) |
CTSolvers relies on CTBase for its generic infrastructure (CTBase.Options, CTBase.Strategies, CTBase.Orchestration). See the CTBase documentation for details on the strategy contract, option system, and orchestration.
All access is qualified — CTSolvers does not export symbols at the top level:
using CTSolvers
CTSolvers.Solvers.Ipopt(max_iter = 1000) # ✓ qualified
CTBase.Strategies.id(CTSolvers.Solvers.Ipopt) # ✓ qualified
CTSolvers.Optimization.build_model(docp, x0, m) # ✓ qualified
id(CTSolvers.Solvers.Ipopt) # ERROR: UndefVarError — not exportedType Hierarchies
Strategy Branch
All configurable components (modelers, solvers, integrators) in CTSolvers are strategies. They share a common contract defined by AbstractStrategy:
AbstractStrategy
├─ id(::Type) → Symbol
├─ metadata(::Type) → StrategyMetadata
├─ options(inst) → StrategyOptions
│
├─► AbstractNLPModeler
│ ├─ build_model(prob, x0, modeler) → BuiltModel
│ ├─ build_solution(built, stats, modeler) → OCP Solution
│ ├─► Modelers.ADNLP{P<:CPU}
│ └─► Modelers.Exa{P<:Union{CPU,GPU}}
│
├─► AbstractNLPSolver
│ ├─ solve(nlp, solver; display) → ExecutionStats
│ ├─► Solvers.Ipopt{P<:CPU}
│ ├─► Solvers.MadNLP{P<:Union{CPU,GPU}}
│ ├─► Solvers.MadNCL{P<:Union{CPU,GPU}}
│ ├─► Solvers.Knitro{P<:CPU}
│ └─► Solvers.Uno{P<:CPU}
│
├─► AbstractIntegrator
│ ├─ solve(prob, integ; options, unsafe) → AbstractIntegrationResult
│ └─► AbstractSciMLIntegrator
│ └─► Integrators.SciML{P<:Union{CPU,GPU}}
│
└─► AbstractDiscretizer (DOCP — no concrete type in CTSolvers)
└─ discretize(ocp, discretizer) → DiscretizedModelDiscretizers live elsewhere
CTSolvers only declares AbstractDiscretizer and the discretize stub. The concrete discretizers (e.g. Collocation, DirectShooting) are defined in CTDirect.jl, which implements discretize and the build_model/build_solution methods for its DiscretizedModels. See the Implementing a Strategy guide in CTBase.jl documentation for the strategy contract.
Optimization Branch
AbstractOptimizationProblem is a marker type with no required methods. The build_model / build_solution contract is satisfied by multiple dispatch: external packages (e.g. CTDirect) implement the typed methods for their own problem types.
AbstractOptimizationProblem (marker — no interface methods)
└─► DiscretizedModel (DOCP module; its cache is populated by CTDirect)
build_model(prob, x0, modeler) → BuiltModel (generic stub; CTDirect provides typed methods)
build_solution(built, stats, modeler) → Solution (generic stub; CTDirect provides typed methods)
BuiltModel{TP, TN, TC} (problem + NLP + cache, immutable)
├─ .problem → TP <: AbstractOptimizationProblem
├─ .nlp → TN (backend NLP model, e.g. ADNLPModel)
└─ .cache → TC <: AbstractCache
└─► NoCache (for backends needing no auxiliary storage)Module Dependencies
The loading order is strict and acyclic:
| # | Module | Depends on (inside CTSolvers) | External |
|---|---|---|---|
| 1 | Optimization | — | CTBase |
| 2 | Modelers | Optimization | CTBase |
| 3 | DOCP | Optimization, Modelers | CTBase, CTModels |
| 4 | Solvers | Optimization, Modelers | CTBase, SolverCore |
| 5 | Integrators | — | CTBase, CTModels (status/successful) |
Each module only depends on modules loaded before it. Integrators is independent of the NLP side. This strict ordering ensures no circular dependencies and makes extensions straightforward to reason about.
Data Flow
The complete resolution pipeline, from user call to optimal control solution:
User
│
▼ solve(docp, x0, modeler, solver) ← orchestration.jl
CommonSolve.solve
│
├─► build_model(docp, x0, modeler) → BuiltModel
│ (CTDirect provides typed method for DiscretizedModel + modeler pair)
│
├─► CommonSolve.solve(built.nlp, solver) → ExecutionStats
│ (backend extension provides typed method, e.g. CTSolversIpopt)
│
└─► build_solution(built, stats, modeler) → OCP Solution
(CTDirect provides typed method for DiscretizedModel + modeler pair)The two levels of CommonSolve.solve on the NLP side, plus the integrator one:
| Level | Signature | Purpose |
|---|---|---|
| High | solve(problem, x0, modeler, solver; display) | Full pipeline: build NLP → solve → build solution |
| Mid | solve(nlp, solver; display) | Solve an NLP directly; implemented by each backend extension |
| ODE | solve(prob, integrator; options, unsafe) | Integrate an ODE problem; implemented by the integrator extension |
Architectural Patterns
Two-Level Contract
Every strategy implements a two-level contract separating static metadata from dynamic configuration:
Type-Level (static, called on the type itself)
├─ id(::Type{MyStrategy}) → :my_strategy (unique symbol identifier)
└─ metadata(::Type{MyStrategy}) → StrategyMetadata (option definitions, defaults)
│
▼ used for: introspection, routing, validation before construction
Constructor
│
▼
Instance-Level (dynamic, called on a constructed instance)
└─ options(strategy) → StrategyOptions (actual values with provenance)
│
▼ used for: backend call, options_dict extraction
ExecutionType-level methods (
id,metadata) are called on the type — they enable introspection, routing, and validation without creating objects.Instance-level methods (
options) are called on instances — they provide the actual configuration with provenance tracking.
See the Implementing a Strategy guide in CTBase.jl documentation for a step-by-step tutorial.
Strategy Parameters (Overview)
Strategies can be parameterized to specialize behavior based on execution context (e.g., CPU vs GPU). Parameters are singleton types enabling compile-time dispatch:
AbstractStrategyParameter
├─► CPU (singleton type)
└─► GPU (singleton type)
metadata(Solvers.MadNLP{CPU}) → CPU defaults → Solvers.MadNLP{CPU}(max_iter=1000)
metadata(Solvers.MadNLP{GPU}) → GPU defaults → Solvers.MadNLP{GPU}(max_iter=1000)The parameter is a type parameter of the strategy (Solvers.MadNLP{P}), not a separate argument. Solvers.MadNLP(...) resolves P from default_parameter (here CPU).
See the Strategy Parameters guide in CTBase.jl documentation for a complete guide.
NotImplemented Pattern
All contract methods have default implementations that throw NotImplemented with helpful error messages:
# If you forget to implement `id` for your strategy:
julia> CTBase.Strategies.id(IncompleteStrategy)
# ERROR: NotImplemented
# Strategy ID method not implemented
# Required method: id(::Type{<:IncompleteStrategy})
# Suggestion: Implement id(::Type{<:IncompleteStrategy}) to return
# a unique Symbol identifierThis pattern ensures that missing implementations are detected immediately with clear guidance — no silent failures or incorrect defaults.
Tag Dispatch
Solvers (and integrators) use Tag Dispatch to separate type definitions (in src/Solvers/) from backend implementations (in ext/).
src/Solvers/ipopt.jl — type definition and stubs (always loaded):
struct Ipopt{P<:CPU} <: AbstractNLPSolver
options::StrategyOptions
end
struct IpoptTag <: Core.AbstractTag end
CTBase.Strategies.id(::Type{<:Solvers.Ipopt}) = :ipopt
CTBase.Strategies.default_parameter(::Type{<:Solvers.Ipopt}) = CPU
CTBase.Strategies.parameter(::Type{<:Solvers.Ipopt{P}}) where {P<:CPU} = P
# Constructor chain: resolve P, then dispatch on the tag and parameter
# TYPES
function Solvers.Ipopt(; kwargs...)
P = CTBase.Strategies.default_parameter(Solvers.Ipopt)
return Solvers.Ipopt{P}(; kwargs...)
end
Solvers.Ipopt{P}(; kwargs...) where {P<:CPU} =
_build_ipopt_solver(IpoptTag, P; kwargs...)
_build_ipopt_solver(
::Type{<:Core.AbstractTag},
::Type{<:AbstractStrategyParameter};
kwargs...,
) = throw(ExtensionError(:NLPModelsIpopt))ext/CTSolversIpopt.jl — real implementations (loaded only with using NLPModelsIpopt):
metadata(::Type{Solvers.Ipopt{P}}) where {P<:CPU} = StrategyMetadata(...)
_build_ipopt_solver(
::Type{Solvers.IpoptTag},
P::Type{<:AbstractStrategyParameter};
kwargs...,
) = Solvers.Ipopt{P}(validated_opts)
CommonSolve.solve(nlp, solver::Solvers.Ipopt; display) =
ipopt(nlp; options_dict(solver)...)This keeps CTSolvers lightweight — backend dependencies are optional weak deps loaded on demand.