How to plot a solution
In this tutorial, we explain the different options for plotting the solution of an optimal control problem using the plot
and plot!
functions, which are extensions of the Plots.jl package. Use plot
to create a new plot object, and plot!
to add to an existing one:
plot(args...; kw...) # creates a new Plot, and set it to be the `current`
plot!(args...; kw...) # modifies Plot `current()`
plot!(plt, args...; kw...) # modifies Plot `plt`
More precisely, the signature of plot
, to plot a solution, is as follows.
RecipesBase.plot
— Methodplot(
sol::Solution,
description::Symbol...;
layout,
control,
time,
state_style,
state_bounds_style,
control_style,
control_bounds_style,
costate_style,
time_style,
path_style,
path_bounds_style,
dual_style,
size,
kwargs...
) -> Plots.Plot
Plot the components of an optimal control solution.
This is the main user-facing function to visualise the solution of an optimal control problem solved with the control-toolbox ecosystem.
It generates a set of subplots showing the evolution of the state, control, costate, path constraints, and dual variables over time, depending on the problem and the user’s choices.
Arguments
sol::CTModels.Solution
: The optimal control solution to visualise.description::Symbol...
: A variable number of symbols indicating which components to include in the plot. Common values include::state
– plot the state.:costate
– plot the costate (adjoint).:control
– plot the control.:path
– plot the path constraints.:dual
– plot the dual variables (or Lagrange multipliers) associated with path constraints.
If no symbols are provided, a default set is used based on the problem and styles.
Keyword Arguments (Optional)
layout::Symbol = :group
: Specifies how to arrange plots.:group
: Fewer plots, grouping similar variables together (e.g., all states in one subplot).:split
: One plot per variable component, stacked in a layout.
control::Symbol = :components
: Defines how to represent control inputs.:components
: One curve per control component.:norm
: Single curve showing the Euclidean norm ‖u(t)‖.:all
: Plot both components and norm.
time::Symbol = :default
: Time normalisation for plots.:default
: Real time scale.:normalize
or:normalise
: Normalised to the interval [0, 1].
Style Options (Optional)
All style-related keyword arguments can be either a NamedTuple
of plotting attributes or the Symbol
:none
referring to not plot the associated element. These allow you to customise color, line style, markers, etc.
time_style
: Style for vertical lines at initial and final times.state_style
: Style for state components.costate_style
: Style for costate components.control_style
: Style for control components.path_style
: Style for path constraint values.dual_style
: Style for dual variables.
Bounds Decorations (Optional)
Use these options to customise bounds on the plots if applicable and defined in the model. Set to :none
to hide.
state_bounds_style
: Style for state bounds.control_bounds_style
: Style for control bounds.path_bounds_style
: Style for path constraint bounds.
Returns
- A
Plots.Plot
object, which can be displayed, saved, or further customised.
Example
# basic plot
julia> plot(sol)
# plot only the state and control
julia> plot(sol, :state, :control)
# customise layout and styles, no costate
julia> plot(sol;
layout = :group,
control = :all,
state_style = (color=:blue, linestyle=:solid),
control_style = (color=:red, linestyle=:dash),
costate_style = :none)
RecipesBase.plot!
— Methodplot!(
sol::Solution,
description::Symbol...;
layout,
control,
time,
state_style,
state_bounds_style,
control_style,
control_bounds_style,
costate_style,
time_style,
path_style,
path_bounds_style,
dual_style,
kwargs...
) -> Any
Modify Plot current()
with the optimal control solution sol
.
See plot
for full behavior and keyword arguments.
RecipesBase.plot!
— Methodplot!(
p::Plots.Plot,
sol::Solution,
description::Symbol...;
layout,
control,
time,
state_style,
state_bounds_style,
control_style,
control_bounds_style,
costate_style,
time_style,
path_style,
path_bounds_style,
dual_style,
kwargs...
) -> Plots.Plot
Modify Plot p
with the optimal control solution sol
.
See plot
for full behavior and keyword arguments.
Argument Overview
The table below summarizes the main plotting arguments and links to the corresponding documentation sections for detailed explanations:
Section | Relevant Arguments |
---|---|
Basic concepts | size , state_style , costate_style , control_style , time_style , kwargs... |
Split vs. group layout | layout |
Plotting control norm | control |
Normalised time | time |
Constraints | state_bounds_style , control_bounds_style , path_style , path_bounds_style , dual_style |
What to plot | description... |
You can plot solutions obtained from the solve
function or from a flow computed using an optimal control problem and a control law. See the Basic Concepts and From Flow function sections for details.
To overlay a new plot on an existing one, use the plot!
function (see Add a plot).
If you prefer full control over the visualisation, you can extract the state, costate, and control to create your own plots. Refer to the Custom plot section for guidance. You can also access the subplots.
The problem and the solution
Let us start by importing the packages needed to define and solve the problem.
using OptimalControl
using NLPModelsIpopt
We consider the simple optimal control problem from the basic example page.
t0 = 0 # initial time
tf = 1 # final time
x0 = [-1, 0] # initial condition
xf = [ 0, 0] # final condition
ocp = @def begin
t ∈ [t0, tf], time
x ∈ R², state
u ∈ R, control
x(t0) == x0
x(tf) == xf
ẋ(t) == [x₂(t), u(t)]
∫( 0.5u(t)^2 ) → min
end
sol = solve(ocp, display=false)
Basic concepts
The simplest way to plot the solution is to use the plot
function with the solution as the only argument.
The plot
function for a solution of an optimal control problem extends the plot
function from Plots.jl. Therefore, you need to import this package in order to plot a solution.
using Plots
plot(sol)
In the figure above, we have a grid of subplots: the left column displays the state component trajectories, the right column shows the costate component trajectories, and the bottom row contains the control component trajectory.
As in Plots.jl, input data is passed positionally (for example, sol
in plot(sol)
), and attributes are passed as keyword arguments (for example, plot(sol; color = :blue)
). After executing using Plots
in the REPL, you can use the plotattr()
function to print a list of all available attributes for series, plots, subplots, or axes.
# Valid Operations
plotattr(:Plot)
plotattr(:Series)
plotattr(:Subplot)
plotattr(:Axis)
Once you have the list of attributes, you can either use the aliases of a specific attribute or inspect a specific attribute to display its aliases and description.
julia> plotattr("color") # Specific Attribute Example
:seriescolor The base color for this series. `:auto` (the default) will select a color from the subplot's `color_palette`, based on the order it was added to the subplot. Also describes the colormap for surfaces. Aliases: (:c, :cmap, :color, :colormap, :colour, :seriescolors). Type: Union{Integer, Symbol, ColorSchemes.ColorScheme, Colorant}. `Series` attribute, defaults to `auto`.
Some attributes have different default values in OptimalControl.jl compared to Plots.jl. For instance, the default figure size is 600x400 in Plots.jl, while in OptimalControl.jl, it depends on the number of states and controls.
You can also visit the Plot documentation online to get the descriptions of the attributes:
- To pass attributes to the plot, see the attributes plot documentation. For instance, you can specify the size of the figure.
List of plot attributes.
background_color
background_color_outside
display_type
dpi
extra_kwargs
extra_plot_kwargs
fontfamily
foreground_color
html_output_format
inset_subplots
layout
link
overwrite_figure
plot_title
plot_titlefontcolor
plot_titlefontfamily
plot_titlefonthalign
plot_titlefontrotation
plot_titlefontsize
plot_titlefontvalign
plot_titleindex
plot_titlelocation
plot_titlevspan
pos
show
size
tex_output_standalone
thickness_scaling
warn_on_unsupported
window_title
- You can pass attributes to all subplots at once by referring to the attributes subplot documentation. For example, you can specify the location of the legends.
List of subplot attributes.
annotationcolor
annotationfontfamily
annotationfontsize
annotationhalign
annotationrotation
annotations
annotationvalign
aspect_ratio
background_color_inside
background_color_subplot
bottom_margin
camera
clims
color_palette
colorbar
colorbar_continuous_values
colorbar_discrete_values
colorbar_fontfamily
colorbar_formatter
colorbar_scale
colorbar_tickfontcolor
colorbar_tickfontfamily
colorbar_tickfonthalign
colorbar_tickfontrotation
colorbar_tickfontsize
colorbar_tickfontvalign
colorbar_ticks
colorbar_title
colorbar_title_location
colorbar_titlefontcolor
colorbar_titlefontfamily
colorbar_titlefonthalign
colorbar_titlefontrotation
colorbar_titlefontsize
colorbar_titlefontvalign
extra_kwargs
fontfamily_subplot
foreground_color_subplot
foreground_color_title
framestyle
left_margin
legend_background_color
legend_column
legend_font
legend_font_color
legend_font_family
legend_font_halign
legend_font_pointsize
legend_font_rotation
legend_font_valign
legend_foreground_color
legend_position
legend_title
legend_title_font
legend_title_font_color
legend_title_font_family
legend_title_font_halign
legend_title_font_pointsize
legend_title_font_rotation
legend_title_font_valign
margin
projection
projection_type
right_margin
subplot_index
title
titlefontcolor
titlefontfamily
titlefonthalign
titlefontrotation
titlefontsize
titlefontvalign
titlelocation
top_margin
- Similarly, you can pass axis attributes to all subplots. See the attributes axis documentation. For example, you can remove the grid from every subplot.
List of axis attributes.
discrete_values
draw_arrow
flip
foreground_color_axis
foreground_color_border
foreground_color_grid
foreground_color_guide
foreground_color_minor_grid
foreground_color_text
formatter
grid
gridalpha
gridlinewidth
gridstyle
guide
guide_position
guidefontcolor
guidefontfamily
guidefonthalign
guidefontrotation
guidefontsize
guidefontvalign
lims
link
minorgrid
minorgridalpha
minorgridlinewidth
minorgridstyle
minorticks
mirror
rotation
scale
showaxis
tick_direction
tickfontcolor
tickfontfamily
tickfonthalign
tickfontrotation
tickfontsize
tickfontvalign
ticks
unit
unitformat
widen
- Finally, you can pass series attributes to all subplots. Refer to the attributes series documentation. For instance, you can set the width of the curves using
linewidth
.
List of series attributes.
arrow
bar_edges
bar_position
bar_width
bins
colorbar_entry
connections
contour_labels
contours
extra_kwargs
fill_z
fillalpha
fillcolor
fillrange
fillstyle
group
hover
label
levels
line_z
linealpha
linecolor
linestyle
linewidth
marker_z
markeralpha
markercolor
markershape
markersize
markerstrokealpha
markerstrokecolor
markerstrokestyle
markerstrokewidth
normalize
orientation
permute
primary
quiver
ribbon
series_annotations
seriesalpha
seriescolor
seriestype
show_empty_bins
smooth
stride
subplot
weights
x
xerror
y
yerror
z
z_order
zerror
plot(sol, size=(700, 450), label="sol", legend=:bottomright, grid=false, linewidth=2)
To specify series attributes for a specific group of subplots (state, costate or control), you can use the optional keyword arguments state_style
, costate_style
, and control_style
, which correspond to the state, costate, and control trajectories, respectively.
plot(sol;
state_style = (color=:blue,), # style: state trajectory
costate_style = (color=:black, linestyle=:dash), # style: costate trajectory
control_style = (color=:red, linewidth=2)) # style: control trajectory
Vertical axes at the initial and final times are automatically plotted. The style can me modified with the time_style
keyword argument. Additionally, you can choose not to display for instance the state and the costate trajectories by setting their styles to :none
. You can set to :none
any style.
plot(sol;
state_style = :none, # do not plot the state
costate_style = :none, # do not plot the costate
control_style = (color = :red,), # plot the control in red
time_style = (color = :green,)) # vertical axes at initial and final times in green
To select what to display, you can also use the description
argument by providing a list of symbols such as :state
, :costate
, and :control
.
plot(sol, :state, :control) # plot the state and the control
For more details on how to choose what to plot, see the What to plot section.
From Flow function
The previous solution of the optimal control problem was obtained using the solve
function. If you prefer using an indirect shooting method and solving shooting equations, you may also want to plot the associated solution. To do this, you need to use the Flow
function to reconstruct the solution. See the manual on how to compute flows for more details. In our case, you must provide the maximizing control $(x, p) \mapsto p_2$ along with the optimal control problem. For an introduction to simple indirect shooting, see the indirect simple shooting tutorial for an example.
using OrdinaryDiffEq
p = costate(sol) # costate as a function of time
p0 = p(t0) # costate solution at the initial time
f = Flow(ocp, (x, p) -> p[2]) # flow from an ocp and a control law in feedback form
sol_flow = f((t0, tf), x0, p0) # compute the solution
plot(sol_flow) # plot the solution from a flow
We may notice that the time grid contains very few points. This is evident from the subplot of $x_2$, or by retrieving the time grid directly from the solution.
time_grid(sol_flow)
6-element Vector{Float64}:
0.0
0.0024073859177725933
0.017290220629033154
0.08302422054370538
0.3587721160724497
1.0
To improve visualisation (without changing the accuracy), you can provide a finer grid.
fine_grid = range(t0, tf, 100)
sol_flow = f((t0, tf), x0, p0; saveat=fine_grid)
plot(sol_flow)
Split vs. group layout
If you prefer to get a more compact figure, you can use the layout
optional keyword argument with :group
value. It will group the state, costate and control trajectories in one subplot for each.
plot(sol; layout=:group)
The default layout value is :split
which corresponds to the grid of subplots presented above.
plot(sol; layout=:split)
Add a plot
You can plot the solution of a second optimal control problem on the same figure if it has the same number of states, costates and controls. For instance, consider the same optimal control problem but with a different initial condition.
ocp = @def begin
t ∈ [t0, tf], time
x ∈ R², state
u ∈ R, control
x(t0) == [-0.5, -0.5]
x(tf) == xf
ẋ(t) == [x₂(t), u(t)]
∫( 0.5u(t)^2 ) → min
end
sol2 = solve(ocp; display=false)
We first plot the solution of the first optimal control problem, then, we plot the solution of the second optimal control problem on the same figure, but with dashed lines.
plt = plot(sol; label="sol1", size=(700, 500))
plot!(plt, sol2; label="sol2", linestyle=:dash)
You can also, implicitely, use the current plot.
plot(sol; label="sol1", size=(700, 500))
plot!(sol2; label="sol2", linestyle=:dash)
Plotting the control norm
For some problem, it is interesting to plot the (Euclidean) norm of the control. You can do it by using the control
optional keyword argument with :norm
value.
plot(sol; control=:norm, size=(800, 300), layout=:group)
The default value is :components
.
plot(sol; control=:components, size=(800, 300), layout=:group)
You can also plot the control and is norm.
plot(sol; control=:all, layout=:group)
Custom plot and subplots
You can, of course, create your own plots by extracting the state
, costate
, and control
from the optimal control solution. For instance, let us plot the norm of the control.
using LinearAlgebra
t = time_grid(sol)
u = control(sol)
plot(t, norm∘u; label="‖u‖", xlabel="t")
You can also get access to the subplots. The order is as follows: state, costate, control, path constraints (if any) and their dual variables.
plt = plot(sol)
plot(plt[1]) # x₁
plt = plot(sol)
plot(plt[2]) # x₂
plt = plot(sol)
plot(plt[3]) # p₁
plot(plt[4]) # p₂
plot(plt[5]) # u
Normalised time
We consider a LQR example and solve the problem for different values of the final time tf
. Then, we plot the solutions on the same figure using a normalised time $s = (t - t_0) / (t_f - t_0)$, enabled by the keyword argument time = :normalize
(or :normalise
) in the plot
function.
# definition of the problem, parameterised by the final time
function lqr(tf)
ocp = @def begin
t ∈ [0, tf], time
x ∈ R², state
u ∈ R, control
x(0) == [0, 1]
ẋ(t) == [x₂(t), - x₁(t) + u(t)]
∫( 0.5(x₁(t)^2 + x₂(t)^2 + u(t)^2) ) → min
end
return ocp
end
# solve the problems and store them
solutions = []
tfs = [3, 5, 30]
for tf ∈ tfs
solution = solve(lqr(tf); display=false)
push!(solutions, solution)
end
# create plots
plt = plot()
for (tf, sol) ∈ zip(tfs, solutions)
plot!(plt, sol; time=:normalize, label="tf = $tf", xlabel="s")
end
# make a custom plot: keep only state and control
px1 = plot(plt[1]; legend=false) # x₁
px2 = plot(plt[2]; legend=true) # x₂
pu = plot(plt[5]; legend=false) # u
using Plots.PlotMeasures # for leftmargin, bottommargin
plot(px1, px2, pu; layout=(1, 3), size=(800, 300), leftmargin=5mm, bottommargin=5mm)
Constraints
We define an optimal control problem with constraints, solve it and plot the solution.
ocp = @def begin
tf ∈ R, variable
t ∈ [0, tf], time
x = (q, v) ∈ R², state
u ∈ R, control
tf ≥ 0
-1 ≤ u(t) ≤ 1
q(0) == -1
v(0) == 0
q(tf) == 0
v(tf) == 0
1 ≤ v(t)+1 ≤ 1.8, (1)
ẋ(t) == [v(t), u(t)]
tf → min
end
sol = solve(ocp)
plot(sol)
On the plot, you can see the lower and upper bounds of the path constraint. Additionally, the dual variable associated with the path constraint is displayed alongside it.
You can customise the plot styles. For style options related to the state, costate, and control, refer to the Basic Concepts section.
plot(sol;
state_bounds_style = (linestyle = :dash,),
control_bounds_style = (linestyle = :dash,),
path_style = (color = :green,),
path_bounds_style = (linestyle = :dash,),
dual_style = (color = :red,),
time_style = :none, # do not plot axes at t0 and tf
)
What to plot
You can choose what to plot using the description
argument. To plot only one subgroup:
plot(sol, :state) # plot only the state
plot(sol, :costate) # plot only the costate
plot(sol, :control) # plot only the control
plot(sol, :path) # plot only the path constraint
plot(sol, :dual) # plot only the path constraint dual variable
You can combine elements to plot exactly what you need:
plot(sol, :state, :control, :path)
Similarly, you can choose what not to plot passing :none
to the corresponding style.
plot(sol; state_style=:none) # do not plot the state
plot(sol; costate_style=:none) # do not plot the costate
plot(sol; control_style=:none) # do not plot the control
plot(sol; path_style=:none) # do not plot the path constraint
plot(sol; dual_style=:none) # do not plot the path constraint dual variable
For instance, let's plot everything except the dual variable associated with the path constraint.
plot(sol; dual_style=:none)