Types and traits
This page explains the type architecture of the CTModels OCP layer submodule, following the package tenet:
One abstract type per noun, one trait-parameter per axis
Conceptual variants ("is this a state model or a control model?") are encoded as types. Orthogonal yes/no axes ("autonomous?", "free final time?") are encoded as traits carried in a type parameter, and selected by dispatch through an extractor.
Noun families
Each noun of an OCP has one abstract supertype and a small family of concrete subtypes. The pattern is uniform: a definition type (structure only) and a solution type (structure + a numerical value), plus an empty sentinel where a component may be absent.
The empty sentinel lets dispatch stay total: a control-free problem carries an EmptyControlModel rather than a nothing, so accessors like control_dimension return 0 without a special case.
using CTModels
sm = CTModels.StateModel("x", ["x₁", "x₂"])
evm = CTModels.EmptyVariableModel()julia> CTModels.dimension(sm)
2
julia> CTModels.name(sm)
"x"
julia> evm isa CTModels.Components.AbstractVariableModel
trueThe trait axes
Orthogonal yes/no axes are not modelled as separate types but as traits.
Time dependence
TimeDependence has the two values Autonomous and NonAutonomous. It is carried as the first type parameter of Model, so the distinction between is_autonomous:
pre = CTModels.PreModel()
CTModels.variable!(pre, 0)
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 1)
CTModels.control!(pre, 1)
CTModels.dynamics!(pre, (r, t, x, u, v) -> (r[1] = u[1]; nothing))
CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> u[1]^2)
CTModels.time_dependence!(pre; autonomous=true)
ocp = CTModels.build(pre)julia> CTModels.is_autonomous(ocp)
trueControl dependence
Whether the problem carries a control input is the type of the AbstractControlModel inside the Model: an EmptyControlModel means control-free, any other control model means with control. This is exposed through the CTBase.Traits.ControlDependence axis (values ControlFree / WithControl), shared ecosystem-wide, with the extractors is_control_free and has_control:
julia> CTModels.is_control_free(ocp)
false
julia> CTModels.has_control(ocp)
trueLike time dependence, the predicates are generic functions owned by CTBase.Traits; the Model only declares the trait and reports its value (read from the control model type, not from the control dimension).
Time structure
Whether each end of the interval is fixed or free is the type of the corresponding AbstractTimeModel inside the TimesModel. The extractors read the structure without exposing the concrete type:
| Question | Extractor |
|---|---|
| Is | has_fixed_initial_time / has_free_initial_time |
| Is | has_fixed_final_time / has_free_final_time |
| Is | is_initial_time_fixed / is_initial_time_free |
| Is | is_final_time_fixed / is_final_time_free |
julia> CTModels.has_fixed_initial_time(ocp)
true
julia> CTModels.has_fixed_final_time(ocp)
trueA FreeTimeModel stores the index into the optimisation variable
Why traits, not twin types
Modelling "autonomous vs non-autonomous" as two unrelated Model types would duplicate every method and break as soon as a third axis appears (the combinatorial explosion of 2 × 2 × … types). Keeping each axis a trait-parameter means:
methods are written once on the abstract type and dispatch only where the axis matters;
adding an axis adds a parameter, not a new type hierarchy;
the public surface stays the nouns (
StateModel,Model, …) and the extractors (is_autonomous,has_free_final_time), never the raw parameters.
This mirrors the ecosystem-wide design described in the control-toolbox Handbook.