Trajectories
The primal part of a solution is read through accessors that return callables of time (interpolated from the stored samples) for the trajectories, and plain values for the variable and the objective.
| Accessor | Returns | Shape |
|---|---|---|
state | x(t) | callable → |
control | u(t) | callable → |
costate | p(t) | callable → |
variable | v | value |
objective | optimal cost | scalar |
using CTModels
pre = CTModels.PreModel()
CTModels.variable!(pre, 1, "v")
CTModels.time!(pre; t0=0.0, tf=1.0)
CTModels.state!(pre, 2)
CTModels.control!(pre, 1)
CTModels.dynamics!(pre, (r, t, x, u, v) -> (r[1] = x[2]; r[2] = u; nothing))
CTModels.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5u^2)
CTModels.time_dependence!(pre; autonomous=true)
ocp = CTModels.build(pre)
N = 101
T = collect(range(0.0, 1.0; length=N))
X = hcat(cos.(T), -sin.(T))
U = reshape(-cos.(T), N, 1)
P = zeros(N, 2)
v = [2.0] # the optimisation variable value
sol = CTModels.build_solution(ocp, T, X, U, v, P;
objective=0.5, iterations=10, constraints_violation=1e-9,
message="ok", status=:optimal, successful=true)Reading trajectories
The trajectories are callables, so they can be evaluated at any time in the interval, not only at grid points — the samples are interpolated:
x = CTModels.state(sol)
u = CTModels.control(sol)
p = CTModels.costate(sol)julia> x(0.0)
2-element Vector{Float64}:
1.0
-0.0
julia> x(0.123)
2-element Vector{Float64}:
0.9924346132121428
-0.12268878788815202
julia> u(0.5)
-0.8775825618903728
julia> p(1.0)
2-element Vector{Float64}:
0.0
0.0julia> CTModels.variable(sol)
2.0
julia> CTModels.objective(sol)
0.5Interpolation of the control
State and costate are interpolated linearly. The control is piecewise constant by default, as produced by direct methods; indirect methods, whose control is a smooth function of the state and costate, pass control_interpolation=:linear to build_solution. The choice is recorded and read back with control_interpolation:
sol_lin = CTModels.build_solution(ocp, T, X, U, v, P;
objective=0.5, iterations=10, constraints_violation=1e-9,
message="ok", status=:optimal, successful=true,
control_interpolation=:linear)julia> CTModels.control_interpolation(sol) # default
:constant
julia> CTModels.control_interpolation(sol_lin)
:linear
julia> u(0.005) # constant on [T[1], T[2])
-1.0
julia> CTModels.control(sol_lin)(0.005) # linear between T[1] and T[2]
-0.9999750002083326Because the interpolation kind is a stored field — not baked into an opaque closure — downstream code (plotting, serialization) can branch on it explicitly. The Duals & diagnostics page covers the remaining accessors.