Skip to content

Trajectories

A trajectory integration returns a solution object that wraps the raw ODE result and exposes semantic accessors. The CTFlows.Trajectories submodule provides these wrappers.


Solution types

A trajectory call returns one of three CTFlows containers, depending on the flow:

TypeProduced byContent
VectorFieldTrajectoryStateFlow trajectory callstate trajectory
HamiltonianVectorFieldTrajectoryHamiltonianFlow trajectory callstate + costate trajectories
StateFlowTrajectoryControlledFlow trajectory callstate + reconstructed control (+ objective when from an OCP)

A fourth result appears one level up: a trajectory call on an OptimalControlFlow (a control-free Flow(ocp), or Flow(ocp, law) with a DynClosedLoop law) returns a CTModels.Solutions.Solution rather than a CTFlows container — see Optimal control. All four share the same accessor vocabulary (state, costate, control, objective) and the same plot recipe.


VectorFieldTrajectory

julia
sol   # produced by flow((t0, tf), x0)
VectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 16
└─ final state: [0.36787944127643124, 0.0]

Accessors

AccessorReturnsNotes
state(sol)callable x(t)returns sol itself, which is callable
time_grid(sol)vector of time pointsalias for times(sol)
final_state(sol)Vectorthe final state x(tf)
status(sol) / successful(sol)retcode / Boolsolver outcome (see Solver status)
julia
ts = Trajectories.time_grid(sol)   # vector of time points
ts[1], ts[end]
(0.0, 1.0)
julia
x = Trajectories.state(sol)        # callable: x(t) → state at time t
x(0.0)                             # initial state (exact)
2-element Vector{Float64}:
 1.0
 0.0
julia
x(0.5)                             # interpolated at t = 0.5
2-element Vector{Float64}:
 0.6065306592843308
 0.0
julia
x.(ts)                             # broadcast over the time grid
16-element Vector{Vector{Float64}}:
 [1.0, 0.0]
 [0.9877640316387758, 0.0]
 [0.9586392120306756, 0.0]
 [0.9219104181991034, 0.0]
 [0.8795807933369617, 0.0]
 [0.8322170230625516, 0.0]
 [0.7816374670004691, 0.0]
 [0.7290594161179201, 0.0]
 [0.6758982310332805, 0.0]
 [0.6231970367115284, 0.0]
 [0.571858602091371, 0.0]
 [0.5225200133759598, 0.0]
 [0.47564070023807775, 0.0]
 [0.431502170190499, 0.0]
 [0.39025901687118947, 0.0]
 [0.36787944127643135, 0.0]

state(sol) returns sol itself, which is callable. The two forms state(sol)(t) and sol(t) are equivalent. time_grid is an alias for times.

Point integration vs trajectory

Point integration (flow(t0, x0, tf)) returns the final state directly as a Vector, not a solution object:

julia
xf = flow(0.0, x0, 1.0)   # Vector, not VectorFieldTrajectory
typeof(xf)
Vector{Float64} (alias for Array{Float64, 1})

Use trajectory integration (flow((t0, tf), x0)) when you need the full history.


HamiltonianVectorFieldTrajectory

julia
hsol   # produced by hflow((t0, tf), x0, p0)
HamiltonianVectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 18
├─ final state: [0.5403023057842606, 0.8414709847533869]
└─ final costate: [-0.8414709847533869, 0.5403023057842606]

Accessors

AccessorReturnsNotes
state(sol)callable x(t)state trajectory
costate(sol)callable p(t)costate trajectory
time_grid(sol)vector of time pointsalias for times(sol)
final_state(sol)(xf, pf)final state–costate pair
status(sol) / successful(sol)retcode / Boolsolver outcome
julia
ts_h = Trajectories.time_grid(hsol)
18-element Vector{Float64}:
 0.0
 0.010717734625362933
 0.03678924587914857
 0.07082975288098367
 0.11171621300174836
 0.15977761725931772
 0.214066312625275
 0.27414099662100727
 0.339163563060794
 0.40850604198933105
 0.48147597812035586
 0.557505923126691
 0.6360680321223098
 0.716722082543547
 0.7990884851440951
 0.8828514711671517
 0.9677468775678137
 1.0
julia
x_h = Trajectories.state(hsol)      # state trajectory: x(t)
p_h = Trajectories.costate(hsol)    # costate trajectory: p(t)
x_h(0.0), p_h(0.0)
([1.0, 0.0], [0.0, 1.0])
julia
x_h(0.5), p_h(0.5)
([0.8775825617654902, 0.4794255388798979], [-0.4794255388798979, 0.8775825617654902])

StateFlowTrajectory

A StateFlowTrajectory is produced by a trajectory call on a ControlledFlow (see Control laws). It wraps a state trajectory with a reconstructed control and an optional objective.

Accessors

AccessorReturnsNotes
state(sol)callable x(t)state trajectory (scalar coercion for 1-D)
control(sol)callable u(t)reconstructed from the law: u(t) = law(t, x(t), v)
objective(sol)RealMayer + Lagrange — errors unless built from an OCP
time_grid(sol)vector of time pointsalias for times(sol)
final_state(sol)Vectorthe final state x(tf)
status(sol) / successful(sol)retcode / Boolsolver outcome
costate(sol)errors: a state flow has no costate

The cflow in this page's setup is a ControlledFlow built from Flow(fc, law) (no OCP), so its trajectory csol carries a state and a reconstructed control:

julia
x_c = Trajectories.state(csol)     # x(t)
u_c = Trajectories.control(csol)   # u(t) = law(t, x(t), v)
x_c(0.5), u_c(0.5)
(0.36787944126257494, -0.36787944126257494)

objective(csol) and costate(csol) raise a PreconditionError here: this flow has no OCP (so no cost) and is a state flow (so no costate). Build from an OCP — Flow(ocp, law) — to get the objective; see Control laws.


Solver status and final state

Every trajectory forwards the integrator's outcome and its final value from the underlying result — the same accessors work on all three container types:

julia
Integrators.status(sol), Integrators.successful(sol)
(:Success, true)
julia
Integrators.final_state(sol)  # xf — a state flow returns the state
2-element Vector{Float64}:
 0.36787944127643124
 0.0
julia
Integrators.final_state(hsol) # (xf, pf) — a Hamiltonian flow returns the pair
([0.5403023057842606, 0.8414709847533869], [-0.8414709847533869, 0.5403023057842606])

status returns the integrator's return code and successful a Bool. A point call raises a SolverFailure on a non-success code unless unsafe=true; a trajectory call always returns, so inspect successful(sol) to check convergence (see Integrating).


Plotting

Load Plots (or any Plots-compatible backend) to unlock plot on solution objects. The plot recipe is provided by the CTFlowsPlots extension (activated automatically when Plots is loaded):

julia
plot(sol)    # plots each component of the state trajectory

julia
plot(hsol)   # plots state and costate components

Plot options

Each container draws a default set of panels: VectorFieldTrajectory → state, HamiltonianVectorFieldTrajectory → state + costate, StateFlowTrajectory → state + control. Pass description symbols (:state, :costate, :control) to select panels, and keywords to tune the layout:

KeywordValuesEffect
layout:split (default) / :groupone subplot per component, or all in one cell
control:components (default) / :norm / :alleach control, its norm, or both
time:default (default) / :normalizereal time, or rescaled to
state_style, costate_style, control_styleNamedTuple / :noneper-group Plots attributes, or hide the group
size(w, h)figure size in pixels
julia
plot(hsol, :state; layout=:group, time=:normalize)   # only the state, grouped, [0,1] time


Low-level: integration result

Under the hood, solution objects wrap an AbstractIntegrationResult which exposes:

  • Integrators.times(result) — the time grid

  • Integrators.evaluate_at(result, t) — interpolated value at t

  • Integrators.final_state(result) — the final value

These are used internally by the solution wrappers and normally not called directly.


See also