Skip to content

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:

julia
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 / Solution hierarchy 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:

  1. No top-level exports. using CTModels loads the package but brings no symbols into scope. Every symbol is accessed via its qualified path:
julia
CTModels.Building.state!     # ✓ always works
CTModels.Solutions.build_solution
CTModels.Init.build_initial_guess
  1. PreModel → build → Model pipeline. An OCP is assembled incrementally on a mutable PreModel, then frozen into an immutable Model by build. The Model is 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   subject to  , fixed boundary conditions, and box constraints.

julia
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
julia> CTModels.state_dimension(ocp)
2

julia> CTModels.control_dimension(ocp)
1

julia> CTModels.is_autonomous(ocp)
true

julia> CTModels.has_lagrange_cost(ocp)
true

Assembling 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:

julia
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-9

Trajectories are returned as callables (interpolated from the samples):

julia
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)
true

Building 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:

julia
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
julia> init.state(0.5)
2-element Vector{Float64}:
 0.0
 0.0

julia> init.control(0.5)
1-element Vector{Float64}:
 0.1

Next Steps

TopicGuide
Types, traits, and the noun architectureTypes & Traits
Declaring spaces (state, control, variable, time)Components
Dynamics and objectiveDynamics & Objective
Path, boundary, and box constraintsConstraints
Freezing a PreModel into a ModelBuilding a Model
Displaying models in the REPLDisplaying Models
Reading state, control, costate trajectoriesTrajectories
Dual variables and solver diagnosticsDuals & Diagnostics
Warm-starting with initial guessesInitial Guesses
Saving and loading solutionsExport & Import
Full API referenceAPI Reference (left sidebar)