Components
The four declaration verbs populate the spaces of the problem on a PreModel. Each may be called once; calling it twice, or with inconsistent dimensions, raises a structured exception.
| Verb | Declares | Stores into pre.… |
|---|---|---|
state! | state space | pre.state |
control! | control space | pre.control |
variable! | optimisation variable | pre.variable |
time! | the interval | pre.times |
using CTModels
import CTBase
pre = CTModels.PreModel()Dimension, name and components
A space has a dimension, a display name, and per-component names. Omitted names are generated ("x", then "x₁", "x₂", …). Each declaration accepts String or Symbol.
julia> CTModels.state!(pre, 2, "x", ["q", "w"])
julia> CTModels.dimension(pre.state)
2
julia> CTModels.name(pre.state)
"x"
julia> CTModels.components(pre.state)
2-element Vector{String}:
"q"
"w"julia> CTModels.control!(pre, 1) # default name "u"
julia> CTModels.components(pre.control)
1-element Vector{String}:
"u"The optimisation variable
variable! declares the time-independent decision variable 0 means no variable: pre.variable then stays an EmptyVariableModel.
julia> CTModels.variable!(pre, 2, "v")
julia> CTModels.dimension(pre.variable)
2
julia> CTModels.components(pre.variable)
2-element Vector{String}:
"v₁"
"v₂"Time: fixed and free ends
time! sets the interval. Each end is either fixed (a value t0=/tf=) or free (an index ind0=/indf= into
julia> CTModels.time!(pre; t0=0.0, tf=1.0) # both ends fixed
julia> CTModels.time_name(pre.times)
"t"
julia> CTModels.has_fixed_initial_time(pre.times)
true
julia> CTModels.has_fixed_final_time(pre.times)
trueA free final time stored at index 2 of
pre2 = CTModels.PreModel()
CTModels.variable!(pre2, 2, "v")
CTModels.time!(pre2; t0=0.0, indf=2)julia> CTModels.has_free_final_time(pre2.times)
true
julia> CTModels.final_time(pre2.times, [0.0, 1.5])
1.5For a free time the value is read from final_time takes the variable vector. The companion accessor initial_time works the same way. See Types and traits for the Fixed/Free distinction.
Naming rules
Names must be unique across all components of the problem. The validation in CTModels.Building (see __validate_name_uniqueness) rejects empty names, duplicates within a declaration, and collisions with names already declared elsewhere:
pre3 = CTModels.PreModel()
CTModels.state!(pre3, 2, "x", ["a", "b"])julia> CTModels.control!(pre3, 1, "a") # "a" already names a state component
IncorrectArgument → __validate_name_uniqueness, name_validation.jl:203
│
│ control name conflicts with existing names
│
│ Got name='a'
│ Expected unique name not in: ["x", "a", "b"]
│
│ Context control! global name validation
│ Hint Choose a different name that doesn't conflict with existing components
└─Each verb may be called at most once per PreModel. A second call raises a PreconditionError:
pre4 = CTModels.PreModel()
CTModels.state!(pre4, 2)julia> CTModels.state!(pre4, 1) # state already declared
PreconditionError → state!, state.jl:71
│
│ State already set
│
│ Reason state has already been defined for this OCP
│
│ Context state! function - duplicate definition check
│ Hint Create a new OCP instance or use the existing state definition
└─Dimensions must be strictly positive:
julia> CTModels.state!(CTModels.PreModel(), 0)
IncorrectArgument → state!, state.jl:77
│
│ Invalid dimension: must be positive
│
│ Got n=0
│ Expected n > 0 (positive integer)
│
│ Context state!(ocp, n=0, name="x") - validating dimension parameter
│ Hint Use state!(ocp, n=3) with n > 0
└─These rules guarantee that a label like :a resolves unambiguously to one component when reading a solution or a constraint.
Symbolic definitions
A Definition wraps a Julia Expr that captures the original problem definition (e.g. from a macro-based DSL). The wrapped expression is retrieved with expression; an EmptyDefinition returns an empty block :(begin end). Use definition! on the PreModel to attach one before building.