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:
| Type | Produced by | Content |
|---|---|---|
VectorFieldTrajectory | StateFlow trajectory call | state trajectory |
HamiltonianVectorFieldTrajectory | HamiltonianFlow trajectory call | state + costate trajectories |
StateFlowTrajectory | ControlledFlow trajectory call | state + 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 / plot! entry point (Plots or Makie backend — see Plotting below).
VectorFieldTrajectory
sol # produced by flow((t0, tf), x0)VectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 250
└─ final state: [0.36787944127643135, 0.0]Accessors
| Accessor | Returns | Notes |
|---|---|---|
state(sol) | callable x(t) | returns sol itself, which is callable |
time_grid(sol) | vector of time points | alias for times(sol) |
final_state(sol) | Vector | the final state x(tf) |
status(sol) / successful(sol) | retcode / Bool | solver outcome (see Solver status) |
ts = Trajectories.time_grid(sol) # vector of time points
ts[1], ts[end](0.0, 1.0)x = Trajectories.state(sol) # callable: x(t) → state at time t
x(0.0) # initial state (exact)2-element Vector{Float64}:
1.0
0.0x(0.5) # interpolated at t = 0.52-element Vector{Float64}:
0.6065306592843307
0.0x.(ts) # broadcast over the time grid250-element Vector{Vector{Float64}}:
[1.0, 0.0]
[0.9967730010725656, 0.0]
[0.9935521895112224, 0.0]
[0.9903370204920466, 0.0]
[0.9871268086302376, 0.0]
[0.9839227424297489, 0.0]
[0.9807243663048495, 0.0]
[0.9775314665276257, 0.0]
[0.9743443003960651, 0.0]
[0.9711628628361813, 0.0]
⋮
[0.38293583678388415, 0.0]
[0.38103479550956176, 0.0]
[0.37913925927240216, 0.0]
[0.3772478199918917, 0.0]
[0.3753626308696598, 0.0]
[0.3734832353076279, 0.0]
[0.3716076425288823, 0.0]
[0.36973965018347954, 0.0]
[0.36787944127643146, 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:
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
hsol # produced by hflow((t0, tf), x0, p0)HamiltonianVectorFieldTrajectory
├─ result: SciMLIntegrationResult
├─ tspan: (0.0, 1.0)
├─ time points: 250
├─ final state: [0.540302305784261, 0.8414709847533872]
└─ final costate: [-0.8414709847533872, 0.540302305784261]Accessors
| Accessor | Returns | Notes |
|---|---|---|
state(sol) | callable x(t) | state trajectory |
costate(sol) | callable p(t) | costate trajectory |
time_grid(sol) | vector of time points | alias for times(sol) |
final_state(sol) | (xf, pf) | final state–costate pair |
status(sol) / successful(sol) | retcode / Bool | solver outcome |
ts_h = Trajectories.time_grid(hsol)250-element Vector{Float64}:
0.0
0.0038151535430030166
0.007630324715607665
0.011445536655583819
0.015260803127120314
0.019076136418076293
0.02289155574304958
0.026707077723737527
0.030522726858067315
0.034338525825492454
⋮
0.9645805076199191
0.9689856533715782
0.9733995565914632
0.9778197130206121
0.9822452943232209
0.9866763216940486
0.991112816103696
0.995554798286404
1.0x_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])x_h(0.5), p_h(0.5)([0.8775825617654902, 0.479425538879898], [-0.479425538879898, 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
| Accessor | Returns | Notes |
|---|---|---|
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) | Real | Mayer + Lagrange — errors unless built from an OCP |
time_grid(sol) | vector of time points | alias for times(sol) |
final_state(sol) | Vector | the final state x(tf) |
status(sol) / successful(sol) | retcode / Bool | solver 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:
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:
Integrators.status(sol), Integrators.successful(sol)(:Success, true)Integrators.final_state(sol) # xf — a state flow returns the state2-element Vector{Float64}:
0.36787944127643135
0.0Integrators.final_state(hsol) # (xf, pf) — a Hamiltonian flow returns the pair([0.540302305784261, 0.8414709847533872], [-0.8414709847533872, 0.540302305784261])status returns the integrator's return code and successful a Bool. Both call styles raise a CTBase.Exceptions.SolverFailure on a non-success code; with unsafe=true the call returns anyway, and successful(sol) tells whether the integration converged (see Integrating).
Plotting
Trajectory plotting is split into a backend-free case layer and a backend:
CTFlows.TrajectoryPlots(insrc/) turns a trajectory plus adescriptioninto aCTBase.Plotting.Figure— vocabulary (:state,:costate,:control), panels, layout. It has no plotting dependency. The single entry point isCTFlows.TrajectoryPlots.build_figure, which also owns the user-facing defaults.a weak-dependency extension wires a rendering backend to
plot/plot!on the trajectory types:
| Extension | Trigger | plot(traj) returns | Renders via |
|---|---|---|---|
CTFlowsPlots | using Plots | Plots.Plot | CTBase.Plotting.PlotsBackend |
CTFlowsMakie | using CairoMakie / GLMakie (any Makie) | Makie.Figure | CTBase.Plotting.MakieBackend |
Both extensions call build_figure and hand the figure to CTBase.Plotting.render(<backend>, fig); they expose the same description and keyword surface, with the same features. Until a backend extension is loaded, plot on a trajectory throws a CTBase.Exceptions.ExtensionError.
When only one backend is loaded, call plot unqualified; when both are loaded (as in this build), qualify — Plots.plot(traj) vs Makie.plot(traj).
Plots.plot(sol) # each state component — Plots backendMakie.plot(hsol) # state and costate components — Makie backend, a Makie.FigurePlot 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 — identical on both backends:
| Keyword | Values | Effect |
|---|---|---|
layout | :split (default) / :group | one subplot per component, or all in one cell |
control | :components (default) / :norm / :all | each control, its norm, or both |
time | :default (default) / :normalize | real time, or rescaled to |
state_style, costate_style, control_style | NamedTuple / :none | per-group backend attributes, or hide the group |
size | (w, h) | figure size in pixels |
Any further keyword is forwarded to the backend (see CTFlows.TrajectoryPlots.split_plot_kwargs).
# only the state, grouped, time rescaled to [0, 1]
Plots.plot(hsol, :state; layout=:group, time=:normalize)Makie.plot(hsol, :state; layout=:group, time=:normalize) # same call, Makie backendplot! overlays a second trajectory onto an existing figure, on either backend:
f = Makie.plot(hsol)
Makie.plot!(f, hsol; state_style=(linestyle=:dash,))Low-level: integration result
Under the hood, solution objects wrap an AbstractIntegrationResult which exposes:
Integrators.times(result)— the time gridIntegrators.evaluate_at(result, t)— interpolated value attIntegrators.final_state(result)— the final value
These are used internally by the solution wrappers and normally not called directly.
See also
CTFlows.Trajectories.VectorFieldTrajectory,CTFlows.Trajectories.HamiltonianVectorFieldTrajectory,CTFlows.Trajectories.StateFlowTrajectory— solution container types.CTFlows.Trajectories.state,CTFlows.Trajectories.costate,CTFlows.Trajectories.control,CTFlows.Trajectories.objective— trajectory accessors.CTFlows.Trajectories.time_grid,CTSolvers.Integrators.times— time grid accessors.CTSolvers.Integrators.final_state,CTSolvers.Integrators.status,CTSolvers.Integrators.successful,CTSolvers.Integrators.evaluate_at— result / status accessors.