Optimal control problems
CTModels defines and builds optimal control problems (OCPs) through four focused modules: CTModels.Components, CTModels.Models, CTModels.Building, and CTModels.Solutions. An OCP is assembled incrementally into a mutable PreModel, then frozen into an immutable Model by build.
CTModels builds a model through a three-stage pipeline:
PreModel → declare components → build → Model
(mutable) state!/control!/... (immutable)The OCP layer is organised by responsibility across four modules:
| Module | Directory | What it provides |
|---|---|---|
CTModels.Components | src/Components/ | Component types (StateModel, TimeDependence, …) and their accessors |
CTModels.Models | src/Models/ | Immutable Model type and all model accessor methods |
CTModels.Building | src/Building/ | PreModel, declaration verbs (state!, control!, …), name validation, build |
CTModels.Solutions | src/Solutions/ | build_solution, solution types, dual model, interpolation |
Reading order
| Page | Topic | Key symbols |
|---|---|---|
| Types and traits | The noun/trait architecture | TimeDependence, AbstractStateModel, Empty* |
| Components | Declaring the spaces | state!, control!, variable!, time! |
| Dynamics and objective | The equations of motion and cost | dynamics!, objective! |
| Constraints | Path, boundary and box constraints | constraint! |
| Building a model | Freezing the PreModel | build, Model |
| Displaying models | Base.show and mathematical rendering | Model, PreModel display |
Qualified access
CTModels exports nothing at the package level: every public symbol is reached through a qualified path CTModels.symbol. Bring the package into scope and call its verbs qualified:
using CTModelsMinimal end-to-end example
We build the beam problem: minimise
# 1. A fresh, mutable pre-model
pre = CTModels.PreModel()
# 2. Declare the time interval, the spaces and (here) no optimisation variable
CTModels.variable!(pre, 0)
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 2) # x ∈ ℝ²
CTModels.control!(pre, 1) # u ∈ ℝ
# 3. Dynamics ẋ = (x₂, u), written in place
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
beam_lagrange(t, x, u, v) = u[1]^2
CTModels.objective!(pre, :min; lagrange=beam_lagrange)
# 5. Boundary and box constraints
function beam_boundary!(r, x0, xf, v)
r[1] = x0[1] # x₁(0) = 0
r[2] = x0[2] - 1 # x₂(0) = 1
r[3] = xf[1] # x₁(1) = 0
r[4] = xf[2] + 1 # x₂(1) = -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 the system autonomous and freeze it 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.Once built, the Model answers queries through accessors:
julia> CTModels.state_dimension(ocp)
2
julia> CTModels.control_dimension(ocp)
1
julia> CTModels.variable_dimension(ocp)
0
julia> CTModels.is_autonomous(ocp)
trueThe shortcut CTModels.build_model(pre) is an alias for CTModels.build(pre); see Building a model for what build checks and guarantees.
Mathematical setting
CTModels represents a continuous-time OCP in Bolza form. With state
Two orthogonal axes shape the representation:
Time dependence — whether
, depend explicitly on ( AutonomousvsNonAutonomous).Time structure — whether
/ are fixed or free ( FixedTimeModelvsFreeTimeModel).
How these axes become traits rather than separate types is the subject of Types and traits.