diff --git a/docs/make.jl b/docs/make.jl index 481389409..49e8c7776 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -94,9 +94,9 @@ Modules = Any[ ] for (pkg, syms) in [ - CTModels => (:CTModelsJLD, :CTModelsJSON, :CTModelsPlots), + CTModels => (:CTModelsJLD, :CTModelsJSON, :CTModelsPlots, :CTModelsMakie), CTSolvers => (:CTSolversIpopt, :CTSolversKnitro, :CTSolversMadNLP, :CTSolversMadNCL), - CTFlows => (:CTFlowsPlots, :CTFlowsSciMLFlows, :CTFlowsSciMLIntegrator), + CTFlows => (:CTFlowsPlots, :CTFlowsSciMLFlows, :CTFlowsSciMLIntegrator, :CTFlowsMakie), ] for s in syms m = _ext(pkg, s) @@ -309,6 +309,7 @@ with_api_reference(src_dir, ext_dir) do api_pages "Results" => [ "Solution object" => "results/solution.md", "Plot" => "results/plot.md", + "Plot with Makie" => "results/plot-makie.md", "Save & load" => "results/save-load.md", ], "Flows (indirect)" => [ diff --git a/docs/src/results/plot-makie.md b/docs/src/results/plot-makie.md new file mode 100644 index 000000000..f825c9d58 --- /dev/null +++ b/docs/src/results/plot-makie.md @@ -0,0 +1,274 @@ +# [Plot with Makie](@id results-plot-makie) + +```@meta +Draft = false +``` + +This site draws with [Plots.jl](@ref results-plot) by default. [Makie.jl](https://docs.makie.org) +is the second backend, at feature parity — same figures, same keywords. The one real +difference is how you call it, and that difference is worth a page of its own. + +## When to use it + +Stay on [Plots](@ref results-plot) unless one of these is true: you want an interactive +window to pan and zoom into a switching time or a stiff region, or you are already drawing in +Makie elsewhere in your project and want one plotting stack, not two. + +## Loading a backend + +::: code-group + +```julia [CairoMakie] +using CairoMakie # static — what this page uses +``` + +```julia [GLMakie] +using GLMakie # interactive — a window you can pan and zoom +``` + +::: + +## Qualify `plot` + +`OptimalControl` re-exports `Plots`' `plot`/`plot!`. Loading a Makie backend brings in *its* +`plot`/`plot!` too — same names, different functions — so the bare call becomes ambiguous: + +```@example main +using OptimalControl +using NLPModelsIpopt +using CairoMakie +nothing # hide +``` + +```@repl main +try # hide +plot +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +Julia's own hint names both modules. The fix is to always qualify the Makie call: + +```julia +Makie.plot(sol) # [!code error] +Makie.plot(sol) # [!code ++] +``` + +Everything else is untouched — `solve`, `state`, `control`, `costate`, `objective`, +`time_grid`, `Flow`, `@def` all resolve exactly as before. `plot` and `plot!` are the only two +names that collide. + +## The same figures + +```@example main +t0 = 0 +tf = 1 +x0 = [-1, 0] +xf = [0, 0] + +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) +nothing # hide +``` + +```@example main +Makie.plot(sol) +``` + +```@example main +Makie.plot(sol; layout=:group, control=:all) +``` + +A problem with a box control constraint and a path constraint, the same one used on +[Plot](@ref results-plot): + +```@example main +ocp_c = @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, (c1) + ẋ(t) == [v(t), u(t)] + tf → min +end +sol_c = solve(ocp_c; display=false) +Makie.plot(sol_c, :state, :costate, :control, :path, :dual) +``` + +Every keyword documented on [Plot](@ref results-plot) — `layout`, `control`, `time`, the +`*_style`/`*_bounds_style` keywords, `size`, `plot!` for overlay — works here unchanged. Only +the call itself, and how you customise beyond it, differ. + +## What differs + +| | Plots | Makie | +| --- | --- | --- | +| the call | `plot(sol)` | `Makie.plot(sol)` — always qualified | +| returns | `Plots.Plot` | `Makie.Figure` | +| reaching one panel | `plt[i]` | `f.content[i]` (an `Axis`) | +| annotating a panel | `Plots.plot!(plt[i], …)`, `hline!` | `lines!(ax, …)`, `hlines!`, `text!` | +| `*_style` contents | any Plots attribute | Makie attributes only — an unknown one throws | +| a solution into an axis you built | `plot!(plt[i], sol)` | not supported — `Makie.plot!` is figure-level (see below) | + +The `*_style` keywords hold backend attributes, not a shared vocabulary — a Plots-only name +raises instead of being silently dropped: + +```@repl main +try # hide +Makie.plot(sol, :state; state_style=(markershape=:circle,)) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +## Annotating the figure + +`plot`/`plot!` build a whole figure in one call — a convenience on top of the backend, not the +backend itself. To add something of your own, reach into the figure and use Makie directly. A +time-minimal problem makes this concrete: mark the control bounds and the switching time. + +```@example main +ocp_bb = @def begin + tf ∈ R, variable + t ∈ [0, tf], time + x ∈ R², state + u ∈ R, control + tf ≥ 0 + -1 ≤ u(t) ≤ 1 + x(0) == [0, 1] + x(tf) == [0, 0] + ẋ(t) == [x₂(t), u(t)] + tf → min +end +sol_bb = solve(ocp_bb; display=false) +nothing # hide +``` + +```@example main +f = Makie.plot(sol_bb, :state, :control) +ax = f.content[3] # the control panel — see "Where the panels are" below +f +``` + +```@example main +Makie.hlines!(ax, [-1.0, 1.0]; color=:red, linestyle=:dash) +tf_bb = variable(sol_bb) +Makie.vlines!(ax, [tf_bb / 2]; color=:green) +Makie.text!(ax, tf_bb / 2, 0.0; text="switch") +f +``` + +Two calls do the annotating here, and they are not interchangeable: + +- `Makie.plot!(f, sol)` overlays another **solution** onto the whole figure — the CT + extension, `Figure`-level, the Makie counterpart of `Plots.plot!(plt, sol2)`. +- `hlines!(ax, …)`, `vlines!(ax, …)`, `lines!(ax, …)`, `text!(ax, …)` add **native** Makie + series onto one panel — plain Makie, `Axis`-level. + +On Plots, `plot!` does both jobs — overlay a solution *and* add a native series, the same +function either way — which is why a reader coming from Plots reaches for `Makie.plot!(ax, +sol)` here. It fails: + +```@repl main +try # hide +Makie.plot!(ax, sol_bb) +catch e # hide +showerror(IOContext(stdout, :color => false), e) # hide +end # hide +``` + +```julia +Makie.plot!(ax, sol_bb) # [!code error] +Makie.lines!(ax, time_grid(sol_bb), …) # [!code ++] +``` + +Use the native call instead — `lines!`, `hlines!`, `scatter!`, `text!` — for anything you draw +yourself. + +## Where the panels are + +Each panel is an `Axis`, in the order the description symbols were given, and it carries the +component's label — so you can check you grabbed the right one instead of counting: + +```@example main +[(title=string(ax.title[]), ylabel=string(ax.ylabel[])) for ax in f.content] +``` + +Same ordering [Plot](@ref results-plot) documents for `plt[i]`. + +## Building a figure from scratch + +When annotating an existing panel is not enough — a custom layout, a different kind of plot +entirely — draw straight from the accessors instead of going through `plot` at all: + +```@example main +using LinearAlgebra +tg = time_grid(sol_bb) +u = control(sol_bb) + +fig = Figure(size=(500, 300)) +axu = Axis(fig[1, 1]; xlabel="t", ylabel="‖u‖") +Makie.lines!(axu, tg, norm.(u.(tg))) +fig +``` + +[The logo](@ref examples-logo) builds a whole figure this way, orbit trajectories and all — the +worked example for this approach. + +## Interactive plots + +`GLMakie` opens a real window instead of rendering to an image — useful to pan and zoom into a +switching structure. It cannot run inside this site's own (headless) build, so this block is +illustrative only: + +```julia +using GLMakie + +f = Makie.plot(sol) # opens a window; unaffected otherwise +``` + +## Flows + +The same call works on a trajectory produced by [`Flow`](@ref) — see +[Flows](@ref flows-overview) for how to build one: + +```@example main +using OrdinaryDiffEqTsit5 + +p = costate(sol) +p0 = p(t0) +flow = Flow(ocp, (x, p) -> p[2]) +sol_flow = flow((t0, tf), x0, p0) +Makie.plot(sol_flow) +``` + +## [Reference](@id results-plot-makie-reference) + +```@docs; canonical=false +CairoMakie.Makie.plot(::CTModels.Solution, ::Symbol...) +CairoMakie.Makie.plot!(::CairoMakie.Makie.Figure, ::CTModels.Solution, ::Symbol...) +``` + +## See also + +- [Plot](@ref results-plot) — the Plots backend and the full keyword reference; every keyword + there applies here too. +- [The logo](@ref examples-logo) — a whole figure built from the accessors, Makie throughout. +- [Flows](@ref flows-overview) — building the `Flow` used in the last section. diff --git a/docs/src/results/plot.md b/docs/src/results/plot.md index 09d98ced9..a62bba98d 100644 --- a/docs/src/results/plot.md +++ b/docs/src/results/plot.md @@ -8,6 +8,11 @@ Draft = false directly — the same call works on a `Flow`-produced trajectory too (last section below). The full signatures are collected under [Reference](@ref results-plot-reference) at the end. +**Choosing a backend.** Plots is the default and what this page documents. A second backend, +[Makie.jl](https://docs.makie.org), draws the same figures at feature parity — worth it for an +interactive window, or if you already draw in Makie elsewhere. See +[Plot with Makie](@ref results-plot-makie). + ## Getting started ```@example main @@ -259,6 +264,17 @@ plot(plt[1]) # x₁ plot(plt[5]) # u ``` +A subplot also accepts native Plots calls directly, to annotate rather than re-plot it — for +example marking a control bound: + +```@example main +plot(plt[5]) +hline!([-1, 1]; linestyle=:dash, color=:red) +``` + +The same idea, in Makie, needs one extra step — a panel there is an `Axis`, not a subplot — +see [Plot with Makie](@ref results-plot-makie). + ## Plotting a flow trajectory The same `plot` call works on a trajectory produced by [`Flow`](@ref) — see