Types and traits
This page explains the type architecture of the CTModels OCP layer submodule, following the package tenet:
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()
(CTModels.dimension(sm), CTModels.name(sm), evm isa CTModels.Components.AbstractVariableModel)(2, "x", true)The two trait axes
Two 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 $\dot{x} = f(x,u)$ and $\dot{x} = f(t,x,u)$ is available at compile time. The extractor is 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)
CTModels.is_autonomous(ocp)trueTime 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 $t_0$ fixed / free? | has_fixed_initial_time / has_free_initial_time |
| Is $t_f$ fixed / free? | has_fixed_final_time / has_free_final_time |
(CTModels.has_fixed_initial_time(ocp), CTModels.has_fixed_final_time(ocp))(true, true)A FreeTimeModel stores the index into the optimisation variable $v$ where the free time lives, rather than a value — see Components.
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 package philosophy (dev/philosophy/types-traits-interfaces.md).