diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index d3b6a1a1..4feb6c58 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -20,11 +20,12 @@ jobs: registry: control-toolbox/ct-registry - uses: julia-actions/julia-buildpkg@v1 with: - version: '1.8' + version: '1.9' - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' - name: Build and deploy env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key + GKSwstype: 100 # To make GitHub Action work, disable showing a plot window with the GR backend of the Plots package run: julia --project=docs/ -e 'ENV["GKSwstype"]="nul" ; include("docs/make.jl")' diff --git a/docs/make.jl b/docs/make.jl index 7acf9b53..73313891 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,23 +2,21 @@ using Documenter using CTBase makedocs( + warnonly = [:cross_references, :autodocs_block], sitename = "CTBase.jl", format = Documenter.HTML(prettyurls = false), pages = [ "Introduction" => "index.md", "API" => ["api-ctbase.md", - #"api-callbacks.md", - #"api-checking.md", - "api-default.md", + "api-callbacks.md", "api-description.md", "api-diffgeom.md", "api-exceptions.md", - #"api-functions.md", "api-model.md", "api-parser.md", "api-plot.md", "api-print.md", - "api-ctrepl.md", + "api-repl.md", "api-types.md", "api-utils.md"], "Developers" => "api-developers.md" diff --git a/docs/src/api-checking.md b/docs/src/api-checking.md deleted file mode 100644 index d02f5bf3..00000000 --- a/docs/src/api-checking.md +++ /dev/null @@ -1,19 +0,0 @@ -# Checkings - -## Index - -```@index -Pages = ["api-checking.md"] -Modules = [CTBase] -Order = [:module, :constant, :type, :function, :macro] -Private = false -``` - -## Documentation - -```@autodocs -Modules = [CTBase] -Order = [:module, :constant, :type, :function, :macro] -Pages = ["checking.jl"] -Private = false -``` diff --git a/docs/src/api-default.md b/docs/src/api-default.md deleted file mode 100644 index 98c5d3bd..00000000 --- a/docs/src/api-default.md +++ /dev/null @@ -1,17 +0,0 @@ -# Default - -## Index - -```@index -Pages = ["api-default.md"] -Modules = [CTBase] -Order = [:module, :constant, :type, :function, :macro] -``` - -## Documentation - -```@autodocs -Modules = [CTBase] -Order = [:module, :constant, :type, :function, :macro] -Pages = ["default.jl"] -``` diff --git a/docs/src/api-developers.md b/docs/src/api-developers.md index dccd60ba..1d769f73 100644 --- a/docs/src/api-developers.md +++ b/docs/src/api-developers.md @@ -3,10 +3,10 @@ ## Index ```@index -Pages = ["api-callbacks.md"] +Pages = ["api-developers.md"] Modules = [CTBase] Order = [:module, :constant, :type, :function, :macro] -Private = false +Public = false ``` ## Documentation diff --git a/docs/src/api-functions.md b/docs/src/api-functions.md deleted file mode 100644 index 9f972587..00000000 --- a/docs/src/api-functions.md +++ /dev/null @@ -1,19 +0,0 @@ -# Functions - -## Index - -```@index -Pages = ["api-functions.md"] -Modules = [CTBase] -Order = [:module, :constant, :type, :function, :macro] -Private = false -``` - -## Documentation - -```@autodocs -Modules = [CTBase] -Order = [:module, :constant, :type, :function, :macro] -Pages = ["functions.jl"] -Private = false -``` diff --git a/docs/src/api-plot.md b/docs/src/api-plot.md index 839cf7c5..07824457 100644 --- a/docs/src/api-plot.md +++ b/docs/src/api-plot.md @@ -6,138 +6,3 @@ Order = [:module, :constant, :type, :function, :macro] Pages = ["plot.jl"] Private = false ``` - -## Examples - -```@setup main -using CTBase - -# create a solution -n=2 -m=1 -t0=0.0 -tf=1.0 -x0=[-1.0, 0.0] -xf=[0.0, 0.0] -a = x0[1] -b = x0[2] -C = [-(tf-t0)^3/6.0 (tf-t0)^2/2.0 - -(tf-t0)^2/2.0 (tf-t0)] -D = [-a-b*(tf-t0), -b]+xf -p0 = C\D -α = p0[1] -β = p0[2] -x(t) = [a+b*(t-t0)+β*(t-t0)^2/2.0-α*(t-t0)^3/6.0, b+β*(t-t0)-α*(t-t0)^2/2.0] -p(t) = [α, -α*(t-t0)+β] -u(t) = [p(t)[2]] -objective = 0.5*(α^2*(tf-t0)^3/3+β^2*(tf-t0)-α*β*(tf-t0)^2) -# -N=201 -times = range(t0, tf, N) -# - -sol = OptimalControlSolution() -sol.state_dimension = n -sol.control_dimension = m -sol.times = times -sol.time_name="t" -sol.state = x -sol.state_components_names = [ "x" * ctindices(i) for i ∈ range(1, n)] -sol.costate = p -sol.control = u -sol.control_components_names = [ "u" ] -sol.objective = objective -sol.iterations = 0 -sol.stopping = :dummy -sol.message = "ceci est un test" -sol.success = true -``` - -Let us consider we have defined an optimal control problem - -```julia -ocp = Model() -... -``` - -and solve it - -```julia -sol = solve(ocp) -``` - -We can plot the solution with the default layout `:split`. - -```@example main -plot(sol, layout=:split, size=(800, 600)) -``` - -Or with the layout `:group`. - -```@example main -plot(sol, layout=:group, size=(800, 300)) -``` - -You can specify some styles: - -```@example main -plot_sol = plot(sol, - state_style=(color=:blue,), - costate_style=(color=:black, linestyle=:dash), - control_style=(color=:red, linewidth=2), - size=(800, 600)) -``` - -You can also add additional plots with the command `plot!`. - -```@setup main -using CTBase - -# create a other solution -n=2 -m=1 -t0=0.0 -tf=1.0 -x0=[-0.5, 0.0] -xf=[0.0, 0.0] -a = x0[1] -b = x0[2] -C = [-(tf-t0)^3/6.0 (tf-t0)^2/2.0 - -(tf-t0)^2/2.0 (tf-t0)] -D = [-a-b*(tf-t0), -b]+xf -p0 = C\D -α = p0[1] -β = p0[2] -x(t) = [a+b*(t-t0)+β*(t-t0)^2/2.0-α*(t-t0)^3/6.0, b+β*(t-t0)-α*(t-t0)^2/2.0] -p(t) = [α, -α*(t-t0)+β] -u(t) = [p(t)[2]] -objective = 0.5*(α^2*(tf-t0)^3/3+β^2*(tf-t0)-α*β*(tf-t0)^2) -# -N=201 -times = range(t0, tf, N) -# - -sol2 = OptimalControlSolution() -sol2.state_dimension = n -sol2.control_dimension = m -sol2.times = times -sol2.time_name="t" -sol2.state = x -sol2.state_components_names = [ "x" * ctindices(i) for i ∈ range(1, n)] -sol2.costate = p -sol2.control = u -sol2.control_components_names = [ "u" ] -sol2.objective = objective -sol2.iterations = 0 -sol2.stopping = :dummy -sol2.message = "ceci est un test" -sol2.success = true -``` - -```@example main -plot!(plot_sol, sol2, - state_style=(color=:purple,), - costate_style=(color=:grey, linestyle=:dash), - control_style=(color=:pink, linewidth=2), - size=(800, 600)) -``` diff --git a/docs/src/api-ctrepl.md b/docs/src/api-repl.md similarity index 90% rename from docs/src/api-ctrepl.md rename to docs/src/api-repl.md index dbe7d925..c8fbb610 100644 --- a/docs/src/api-ctrepl.md +++ b/docs/src/api-repl.md @@ -3,7 +3,7 @@ ## Index ```@index -Pages = ["api-ctrepl.md"] +Pages = ["api-repl.md"] Modules = [CTBase] Order = [:module, :constant, :type, :function, :macro] Private = false diff --git a/src/ctparser_utils.jl b/src/ctparser_utils.jl index c2ac2825..65c992e3 100644 --- a/src/ctparser_utils.jl +++ b/src/ctparser_utils.jl @@ -208,8 +208,8 @@ has(e, x, t) = begin if :yes ∈ args :yes else @match ee begin -   :( $eee($tt) ) => (tt == t && has(eee, x)) ? :yes : ee -   _ => ee end + :( $eee($tt) ) => (tt == t && has(eee, x)) ? :yes : ee + _ => ee end end end expr_it(e, foo(x, t), x -> x) == :yes diff --git a/src/plot.jl b/src/plot.jl index b7d06dfc..24024649 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -34,35 +34,70 @@ $(TYPEDSIGNATURES) Update the plot `p` with the i-th component of a vectorial function of time `f(t) ∈ Rᵈ` where `f` is given by the symbol `s`. -The argument `s` can be `:state`, `:control` or `:costate`. +- The argument `s` can be `:state`, `:control` or `:costate`. +- `time` can be `:default` or `:normalized`. """ -function __plot_time!(p::Union{Plots.Plot, Plots.Subplot}, sol::OptimalControlSolution, s::Symbol, i::Integer; +function __plot_time!(p::Union{Plots.Plot, Plots.Subplot}, sol::OptimalControlSolution, s::Symbol, i::Integer, time::Symbol; t_label, label::String, kwargs...) - return CTBase.plot!(p, sol, :time, (s, i); xlabel=t_label, label=label, kwargs...) # use simple plot + + # t_label depends if time is normalized or not + t_label = @match time begin + :default => t_label + :normalized => "normalized "*t_label + _ => error("Internal error, no such choice for time: $time. Use :default or :normalized") + end + + # reset ylims: ylims=:auto + CTBase.plot!(p, sol, :time, (s, i), time; ylims=:auto, xlabel=t_label, label=label, kwargs...) # use simple plot + + # change ylims if the gap between min and max is less than a tol + tol = 1e-3 + ymin = Inf + ymax = -Inf + + for s ∈ p.series_list + y = s[:y] + ymin = min(minimum(y), ymin) + ymax = max(maximum(y), ymax) + end + + if (ymin != Inf) && (ymax != -Inf) && (abs(ymax-ymin) ≤ abs(ymin)*tol) + ymiddle = (ymin+ymax)/2.0 + ylims!(p, (0.9*ymiddle, 1.1*ymiddle)) + end + + return p end """ $(TYPEDSIGNATURES) Plot the i-th component of a vectorial function of time `f(t) ∈ Rᵈ` where `f` is given by the symbol `s`. -The argument `s` can be `:state`, `:control` or `:costate`. +- The argument `s` can be `:state`, `:control` or `:costate`. +- `time` can be `:default` or `:normalized`. """ -function __plot_time(sol::OptimalControlSolution, s::Symbol, i::Integer; t_label, label::String, kwargs...) - return __plot_time!(Plots.plot(), sol, s, i; t_label=t_label, label=label, kwargs...) +function __plot_time(sol::OptimalControlSolution, s::Symbol, i::Integer, time::Symbol; t_label, label::String, kwargs...) + return __plot_time!(Plots.plot(), sol, s, i, time; t_label=t_label, label=label, kwargs...) end """ $(TYPEDSIGNATURES) Update the plot `p` with a vectorial function of time `f(t) ∈ Rᵈ` where `f` is given by the symbol `s`. -The argument `s` can be `:state`, `:control` or `:costate`. +- The argument `s` can be `:state`, `:control` or `:costate`. +- `time` can be `:default` or `:normalized`. """ -function __plot_time!(p::Union{Plots.Plot, Plots.Subplot}, sol::OptimalControlSolution, d::Dimension, s::Symbol; +function __plot_time!(p::Union{Plots.Plot, Plots.Subplot}, sol::OptimalControlSolution, d::Dimension, s::Symbol, time::Symbol; t_label, labels::Vector{String}, title::String, kwargs...) + + # Plots.plot!(p; xlabel="time", title=title, kwargs...) + + # for i in range(1, d) - __plot_time!(p, sol, s, i; t_label=t_label, label=labels[i], kwargs...) + __plot_time!(p, sol, s, i, time; t_label=t_label, label=labels[i], kwargs...) end + return p end @@ -72,8 +107,9 @@ $(TYPEDSIGNATURES) Plot a vectorial function of time `f(t) ∈ Rᵈ` where `f` is given by the symbol `s`. The argument `s` can be `:state`, `:control` or `:costate`. """ -function __plot_time(sol::OptimalControlSolution, d::Dimension, s::Symbol; t_label, labels::Vector{String}, title::String, kwargs...) - return __plot_time!(Plots.plot(), sol, d, s; t_label=t_label, labels=labels, title=title, kwargs...) +function __plot_time(sol::OptimalControlSolution, d::Dimension, s::Symbol, time::Symbol; + t_label, labels::Vector{String}, title::String, kwargs...) + return __plot_time!(Plots.plot(), sol, d, s, time; t_label=t_label, labels=labels, title=title, kwargs...) end """ @@ -222,10 +258,13 @@ Plot the optimal control solution `sol` using the layout `layout`. **Notes.** - The argument `layout` can be `:group` or `:split` (default). +- `control` can be `:components`, `:norm` or `:all`. +- `time` can be `:default` or `:normalized`. - The keyword arguments `state_style`, `control_style` and `costate_style` are passed to the `plot` function of the `Plots` package. The `state_style` is passed to the plot of the state, the `control_style` is passed to the plot of the control and the `costate_style` is passed to the plot of the costate. """ function CTBase.plot!(p::Plots.Plot, sol::OptimalControlSolution; layout::Symbol=:split, - control::Symbol=:components ,state_style=(), control_style=(), costate_style=(), kwargs...) + control::Symbol=:components, time::Symbol=:default, + state_style=(), control_style=(), costate_style=(), kwargs...) # n = sol.state_dimension @@ -237,18 +276,18 @@ function CTBase.plot!(p::Plots.Plot, sol::OptimalControlSolution; layout::Symbol if layout==:group - __plot_time!(p[1], sol, n, :state; t_label=t_label, labels=x_labels, title="state", lims=:auto, state_style...) - __plot_time!(p[2], sol, n, :costate; t_label=t_label, labels="p".*x_labels, title="costate", lims=:auto, costate_style...) + __plot_time!(p[1], sol, n, :state, time; t_label=t_label, labels=x_labels, title="state", lims=:auto, state_style...) + __plot_time!(p[2], sol, n, :costate, time; t_label=t_label, labels="p".*x_labels, title="costate", lims=:auto, costate_style...) @match control begin :components => begin - __plot_time!(p[3], sol, m, :control; t_label=t_label, labels=u_labels, title="control", lims=:auto, control_style...) + __plot_time!(p[3], sol, m, :control, time; t_label=t_label, labels=u_labels, title="control", lims=:auto, control_style...) end :norm => begin - __plot_time!(p[3], sol, :control_norm, -1; t_label=t_label, label="‖"*u_label*"‖", title="control norm", lims=:auto, control_style...) + __plot_time!(p[3], sol, :control_norm, -1, time; t_label=t_label, label="‖"*u_label*"‖", title="control norm", lims=:auto, control_style...) end :all => begin - __plot_time!(p[3], sol, m, :control; t_label=t_label, labels=u_labels, title="control", lims=:auto, control_style...) - __plot_time!(p[4], sol, :control_norm, -1; t_label=t_label, label="‖"*u_label*"‖", title="control norm", lims=:auto, control_style...) + __plot_time!(p[3], sol, m, :control, time; t_label=t_label, labels=u_labels, title="control", lims=:auto, control_style...) + __plot_time!(p[4], sol, :control_norm, -1, time; t_label=t_label, label="‖"*u_label*"‖", title="control norm", lims=:auto, control_style...) end _ => throw(IncorrectArgument("No such choice for control. Use :components, :norm or :all")) end @@ -256,23 +295,23 @@ function CTBase.plot!(p::Plots.Plot, sol::OptimalControlSolution; layout::Symbol elseif layout==:split for i ∈ 1:n - __plot_time!(p[i], sol, :state, i; t_label=t_label, label=x_labels[i], state_style...) - __plot_time!(p[i+n], sol, :costate, i; t_label=t_label, label="p"*x_labels[i], costate_style...) + __plot_time!(p[i], sol, :state, i, time; t_label=t_label, label=x_labels[i], state_style...) + __plot_time!(p[i+n], sol, :costate, i, time; t_label=t_label, label="p"*x_labels[i], costate_style...) end @match control begin :components => begin for i ∈ 1:m - __plot_time!(p[i+2*n], sol, :control, i; t_label=t_label, label=u_labels[i], control_style...) + __plot_time!(p[i+2*n], sol, :control, i, time; t_label=t_label, label=u_labels[i], control_style...) end end :norm => begin - __plot_time!(p[2*n+1], sol, :control_norm, -1; t_label=t_label, label="‖"*u_label*"‖", control_style...) + __plot_time!(p[2*n+1], sol, :control_norm, -1, time; t_label=t_label, label="‖"*u_label*"‖", control_style...) end :all => begin for i ∈ 1:m - __plot_time!(p[i+2*n], sol, :control, i; t_label=t_label, label=u_labels[i], control_style...) + __plot_time!(p[i+2*n], sol, :control, i, time; t_label=t_label, label=u_labels[i], control_style...) end - __plot_time!(p[2*n+m+1], sol, :control_norm, -1; t_label=t_label, label="‖"*u_label*"‖", control_style...) + __plot_time!(p[2*n+m+1], sol, :control_norm, -1, time; t_label=t_label, label="‖"*u_label*"‖", control_style...) end _ => throw(IncorrectArgument("No such choice for control. Use :components, :norm or :all")) end @@ -298,9 +337,13 @@ Plot the optimal control solution `sol` using the layout `layout`. - The argument `layout` can be `:group` or `:split` (default). - The keyword arguments `state_style`, `control_style` and `costate_style` are passed to the `plot` function of the `Plots` package. The `state_style` is passed to the plot of the state, the `control_style` is passed to the plot of the control and the `costate_style` is passed to the plot of the costate. """ -function CTBase.plot(sol::OptimalControlSolution; layout::Symbol=:split, state_style=(), control_style=(), costate_style=(), kwargs...) +function CTBase.plot(sol::OptimalControlSolution; layout::Symbol=:split, + control::Symbol=:components, time::Symbol=:default, state_style=(), control_style=(), costate_style=(), kwargs...) + # p = __initial_plot(sol; layout=layout, kwargs...) - return plot!(p, sol; layout=layout, state_style=state_style, control_style=control_style, costate_style=costate_style, kwargs...) + # + return plot!(p, sol; layout=layout, control=control, time=time, + state_style=state_style, control_style=control_style, costate_style=costate_style, kwargs...) end """ @@ -317,14 +360,29 @@ corresponding respectively to the argument `xx` and the argument `yy`. """ @recipe function f(sol::OptimalControlSolution, xx::Union{Symbol,Tuple{Symbol,Integer}}, - yy::Union{Symbol,Tuple{Symbol,Integer}}) - x = __get_data_plot(sol, xx) - y = __get_data_plot(sol, yy) + yy::Union{Symbol,Tuple{Symbol,Integer}}, time::Symbol=:default) + + # + x = __get_data_plot(sol, xx, time=time) + y = __get_data_plot(sol, yy, time=time) + + # + label = recipe_label(sol, xx, yy) + + return x, y +end + +function recipe_label(sol::OptimalControlSolution, xx::Union{Symbol,Tuple{Symbol,Integer}}, yy::Union{Symbol,Tuple{Symbol,Integer}}) + # + label = false + # if xx isa Symbol && xx==:time + s, i = @match yy begin ::Symbol => (yy, 1) _ => yy end + label = @match s begin :state => sol.state_components_names[i] :control => sol.control_components_names[i] @@ -332,15 +390,23 @@ corresponding respectively to the argument `xx` and the argument `yy`. :control_norm => "‖"*sol.control_name*"‖" _ => error("Internal error, no such choice for label") end - # change ylims if the gap between min and max is less than a tol - tol = 1e-3 - ymin = minimum(y) - ymax = maximum(y) - if abs(ymax-ymin) ≤ abs(ymin)*tol - ymiddle = (ymin+ymax)/2.0 - ylims --> (0.9*ymiddle, 1.1*ymiddle) - end + end + # + return label +end + +@recipe function f(sol::OptimalControlSolution, + xx::Union{Symbol,Tuple{Symbol,Integer}}, + yy::Union{Symbol,Tuple{Symbol,Integer}}) + + # + x = __get_data_plot(sol, xx) + y = __get_data_plot(sol, yy) + + # + label = recipe_label(sol, xx, yy) + return x, y end @@ -350,7 +416,7 @@ $(TYPEDSIGNATURES) Get the data for plotting. """ function __get_data_plot(sol::OptimalControlSolution, - xx::Union{Symbol,Tuple{Symbol,Integer}}) + xx::Union{Symbol,Tuple{Symbol,Integer}}; time::Symbol=:default) T = sol.times X = sol.state.(T) @@ -364,7 +430,13 @@ function __get_data_plot(sol::OptimalControlSolution, m = size(T, 1) return @match vv begin - :time => T + :time => begin + @match time begin + :default => T + :normalized => (T .- T[1]) ./ (T[end] - T[1]) + _ => error("Internal error, no such choice for time: $time. Use :default or :normalized") + end + end :state => [X[i][ii] for i in 1:m] :control => [U[i][ii] for i in 1:m] :costate => [P[i][ii] for i in 1:m] diff --git a/test/debug_scripts/plot.jl b/test/debug_scripts/plot.jl new file mode 100644 index 00000000..88c44707 --- /dev/null +++ b/test/debug_scripts/plot.jl @@ -0,0 +1,35 @@ +using OptimalControl: solve +using CTBase + +tf = 2.0 + +@def ocp begin + t ∈ [ 0, tf ], time + x ∈ R², state + u ∈ R, control + x(0) == [ -1, 0 ] + x(tf) == [ 0, 0 ] + ẋ(t) == [ x₂(t), u(t) ] + ∫( 0.5u(t)^2 ) → min +end + +sol = solve(ocp, display=false) + +@def ocp begin + t ∈ [ 0, tf ], time + x ∈ R², state + u ∈ R, control + x(0) == [ -0.5, -0.5 ] + x(tf) == [ 0, 0 ] + ẋ(t) == [ x₂(t), u(t) ] + ∫( 0.5u(t)^2 ) → min +end + +sol2 = solve(ocp, display=false) + +# first plot +plt = plot(sol, size=(700, 450), time=:default) + +# second plot +style = (linestyle=:dash, ) +plot!(plt, sol2, time=:default, state_style=style, costate_style=style, control_style=style) \ No newline at end of file