Dynamics and objective
With the spaces declared, two verbs supply the equations of motion and the cost: dynamics! and objective!.
using CTModels
function fresh() # a pre-model with state, control, times set
pre = CTModels.PreModel()
CTModels.variable!(pre, 0)
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 2)
CTModels.control!(pre, 1)
return pre
endDynamics
The right-hand side r is the buffer to fill. The full signature is f!(r, t, x, u, v), even for an autonomous system (the unused t keeps one uniform interface).
pre = fresh()
function f!(r, t, x, u, v)
r[1] = x[2]
r[2] = u[1]
return nothing
end
CTModels.dynamics!(pre, f!)Component-wise dynamics
Alternatively, define the dynamics block by block over disjoint state ranges. Each partial right-hand side fills its own local buffer (r[1] is the first row of its range). This is convenient when components come from different physical models.
pre = fresh()
CTModels.dynamics!(pre, 1:1, (r, t, x, u, v) -> (r[1] = x[2]; nothing))
CTModels.dynamics!(pre, 2:2, (r, t, x, u, v) -> (r[1] = u[1]; nothing))The ranges must tile 1:n without overlap; mixing the full form and the block form, or leaving a gap, raises an Exceptions.PreconditionError. The completeness check is __is_dynamics_complete, run by build.
Objective
objective! takes the optimisation direction (:min or :max) and one or both of a Mayer term and a Lagrange term. The three combinations map to the three objective types of Types and traits:
| Provided | Cost | Objective type |
|---|---|---|
lagrange | LagrangeObjectiveModel | |
mayer | MayerObjectiveModel | |
| both | Bolza: | BolzaObjectiveModel |
The Lagrange integrand has signature f⁰(t, x, u, v); the Mayer term g(x0, xf, v).
pre = fresh()
CTModels.dynamics!(pre, f!)
CTModels.objective!(pre, :min;
mayer = (x0, xf, v) -> xf[1]^2,
lagrange = (t, x, u, v) -> u[1]^2,
)
CTModels.time_dependence!(pre; autonomous=true)
ocp = CTModels.build(pre)julia> CTModels.criterion(ocp)
:min
julia> CTModels.has_mayer_cost(ocp)
true
julia> CTModels.has_lagrange_cost(ocp)
trueBecause the cost is stored as a typed object, downstream code dispatches on it (Mayer / Lagrange / Bolza) instead of inspecting closures — see the Solutions guide for how the objective value is read back.
Error cases
Calling dynamics! before the state is set raises a PreconditionError:
pre_no_state = CTModels.PreModel()
CTModels.time!(pre_no_state; t0=0.0, tf=1.0)julia> CTModels.dynamics!(pre_no_state, (r, t, x, u, v) -> (r[1] = 0.0; nothing))
PreconditionError → dynamics!, dynamics.jl:25
│
│ State must be set before defining dynamics
│
│ Reason state has not been defined yet
│
│ Context dynamics! function - state validation
│ Hint Call state!(ocp, dimension) before dynamics!
└─In block form, overlapping ranges raise a PreconditionError immediately:
pre_overlap = fresh()
CTModels.dynamics!(pre_overlap, 1:1, (r, t, x, u, v) -> (r[1] = x[2]; nothing))julia> CTModels.dynamics!(pre_overlap, 1:2, (r, t, x, u, v) -> (r[1] = x[1]; nothing)) # overlaps 1:1
PreconditionError → dynamics!, dynamics.jl:128
│
│ Dynamics range overlap
│
│ Reason index 1 in range already has assigned dynamics
│
│ Context partial_dynamics! function - range overlap check
│ Hint Use a non-overlapping range or remove existing dynamics first
└─