Getting Started
Installation
CTModels.jl is typically installed as a dependency of another package in the ecosystem (e.g. OptimalControl.jl). To install it directly:
import Pkg
Pkg.add("CTModels")Requires Julia ≥ 1.10.
Mental Model
CTModels is the mathematical model layer of the control-toolbox ecosystem. It provides:
Types and building blocks for states, controls, variables, time grids, constraints, and cost functionals.
An immutable
Model/Solutionhierarchy for optimal control problems and their numerical solutions.Tools to build initial guesses for warm-starting a solver.
Optional extensions for serialization (JSON, JLD2) and plotting.
Two things to keep in mind:
- No top-level exports.
using CTModelsloads the package but brings no symbols into scope. Every symbol is accessed via its qualified path:
CTModels.Building.state! # ✓ always works
CTModels.Solutions.build_solution
CTModels.Init.build_initial_guessPreModel → build → Modelpipeline. An OCP is assembled incrementally on a mutablePreModel, then frozen into an immutableModelbybuild. TheModelis the object every downstream package (solver, initial-guess builder, serializer) consumes.
5-Minute Walkthrough
Building an optimal control problem
We solve the beam problem: minimise
using CTModels
# 1. Mutable pre-model
pre = CTModels.PreModel()
# 2. Declare the spaces (must be done before dynamics/objective)
CTModels.variable!(pre, 0) # no optimisation variable
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 2) # x ∈ ℝ²
CTModels.control!(pre, 1) # u ∈ ℝ
# 3. Dynamics ẋ = (x₂, u) — in-place form
function beam_dynamics!(r, t, x, u, v)
r[1] = x[2]
r[2] = u[1]
return nothing
end
CTModels.dynamics!(pre, beam_dynamics!)
# 4. Lagrange cost ∫ u² → min
CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> u[1]^2)
# 5. Constraints
function beam_boundary!(r, x0, xf, v)
r[1] = x0[1]; r[2] = x0[2] - 1
r[3] = xf[1]; r[4] = xf[2] + 1
return nothing
end
CTModels.constraint!(pre, :boundary; f=beam_boundary!, lb=zeros(4), ub=zeros(4), label=:bc)
CTModels.constraint!(pre, :state; rg=1:1, lb=[0.0], ub=[0.1], label=:x1_box)
CTModels.constraint!(pre, :control; rg=1:1, lb=[-10.0], ub=[10.0], label=:u_box)
# 6. Mark autonomous and freeze into an immutable Model
CTModels.time_dependence!(pre; autonomous=true)
ocp = CTModels.build(pre)The (autonomous) optimal control problem is of the form:
minimize J(x, u) = ∫ f⁰(x(t), u(t)) dt, over [0.0, 1.0]
subject to
ẋ(t) = f(x(t), u(t)), t in [0.0, 1.0] a.e.,
ϕ₋ ≤ ϕ(x(0.0), x(1.0)) ≤ ϕ₊,
x₋ ≤ x(t) ≤ x₊,
u₋ ≤ u(t) ≤ u₊,
where x(t) ∈ R² and u(t) ∈ R.The built Model exposes its structure through accessors:
julia> CTModels.state_dimension(ocp)
2
julia> CTModels.control_dimension(ocp)
1
julia> CTModels.is_autonomous(ocp)
true
julia> CTModels.has_lagrange_cost(ocp)
trueAssembling a solution
build_solution is the bridge between a solver's raw arrays and a Solution object. Here we fabricate arrays to illustrate the interface:
N = 101
T = collect(range(0.0, 1.0; length=N))
X = hcat(cos.(T), -sin.(T)) # N×2 state samples
U = reshape(-cos.(T), N, 1) # N×1 control samples
P = zeros(N, 2) # N×2 costate samples
sol = CTModels.build_solution(ocp, T, X, U, Float64[], P;
objective=0.5,
iterations=10,
constraints_violation=1e-9,
message="Solve_Succeeded",
status=:Solve_Succeeded,
successful=true,
)Solution ✓ successful
│ Objective : 0.5
│ Iterations : 10
│ Status : Solve_Succeeded
│ Message : Solve_Succeeded
└─ Constraints violation : 1.0e-9Trajectories are returned as callables (interpolated from the samples):
julia> x = CTModels.state(sol)
CoercedTrajectory
inner: Interpolant
coerce: identity
julia> x(0.5)
2-element Vector{Float64}:
0.8775825618903728
-0.479425538604203
julia> CTModels.objective(sol)
0.5
julia> CTModels.successful(sol)
trueBuilding an initial guess
An initial guess provides the solver with a starting point for the state and control trajectories. It is built from the Model and a pair of functions that return the guessed state and control at any time t:
init = CTModels.build_initial_guess(ocp,
(state=t -> [0.0, 0.0], control=t -> [0.1])
)InitialGuess
state: <callable>
control: <callable>
variable: (none)The returned object exposes the guessed trajectories as callables, just like a Solution:
julia> init.state(0.5)
2-element Vector{Float64}:
0.0
0.0
julia> init.control(0.5)
1-element Vector{Float64}:
0.1Next Steps
| Topic | Guide |
|---|---|
| Types, traits, and the noun architecture | Types & Traits |
| Declaring spaces (state, control, variable, time) | Components |
| Dynamics and objective | Dynamics & Objective |
| Path, boundary, and box constraints | Constraints |
Freezing a PreModel into a Model | Building a Model |
| Displaying models in the REPL | Displaying Models |
| Reading state, control, costate trajectories | Trajectories |
| Dual variables and solver diagnostics | Duals & Diagnostics |
| Warm-starting with initial guesses | Initial Guesses |
| Saving and loading solutions | Export & Import |
| Full API reference | API Reference (left sidebar) |