Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[deps]
CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656"
DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365"
Expand All @@ -12,6 +13,7 @@ Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[compat]
CTBase = "0.30"
CairoMakie = "0.15"
Documenter = "1"
DocumenterInterLinks = "1"
DocumenterVitepress = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion docs/api_reference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function generate_api_reference(src_dir::String, ext_dir::String)

# Conditional extensions
for (sym, files) in [
(:CTModelsPlots, ext("CTModelsPlots.jl", joinpath("case", "plot.jl"))),
(:CTModelsPlots, ext("CTModelsPlots.jl")),
(:CTModelsMakie, ext("CTModelsMakie.jl")),
(:CTModelsJSON, ext("CTModelsJSON.jl")),
(:CTModelsJLD, ext("CTModelsJLD.jl")),
Expand Down
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ end
# ═══════════════════════════════════════════════════════════════════════════════
# Docstrings from external packages
# ═══════════════════════════════════════════════════════════════════════════════
using JLD2, JSON3, Plots
using JLD2, JSON3, Plots, CairoMakie
const CTModelsJLD = Base.get_extension(CTModels, :CTModelsJLD)
const CTModelsJSON = Base.get_extension(CTModels, :CTModelsJSON)
const CTModelsPlots = Base.get_extension(CTModels, :CTModelsPlots)
const CTModelsMakie = Base.get_extension(CTModels, :CTModelsMakie)

# ═══════════════════════════════════════════════════════════════════════════════
# Paths
Expand Down
2 changes: 1 addition & 1 deletion docs/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ It provides:
- **Types** and **building blocks** for states, controls, variables, time grids, constraints, and cost functionals.
- An immutable `Model` / `Solution` hierarchy for optimal control problems and their numerical solutions.
- Tools to build **initial guesses** for warm-starting a solver.
- Optional extensions for **serialization** (JSON, JLD2) and **plotting**.
- Optional extensions for **serialization** (JSON, JLD2) and **plotting** (Plots, Makie).

Two things to keep in mind:

Expand Down
9 changes: 4 additions & 5 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,9 @@ by the corresponding packages:
variables in a consistent, configurable way.

- **CTModelsMakie.jl** (requires `Makie.jl`, e.g. via `CairoMakie` / `GLMakie`):
a second, backend-agnostic rendering path for the same figure —
`Makie.plot(sol::CTModels.Solution, ...)` returns a `Makie.Figure`. Proof of
concept ([#366](https://github.com/control-toolbox/CTModels.jl/issues/366)):
`plot` only, no reference lines or `plot!` yet.
a second rendering path for the same figure, at feature parity with
`CTModelsPlots` — `Makie.plot(sol::CTModels.Solution, ...)` returns a
`Makie.Figure`, and `Makie.plot!` overlays onto an existing one.

If the corresponding extension package is not loaded, the public wrappers
`export_ocp_solution`, `import_ocp_solution`, and the generic `RecipesBase.plot`
Expand Down Expand Up @@ -190,7 +189,7 @@ the details of a particular function or type.

- **I want to save/load or plot solutions**
See the [Serialization & extensions](serialization/overview.md) guide for `export_ocp_solution`,
`import_ocp_solution`, and `plot(sol)`.
`import_ocp_solution`, and `Plots.plot(sol)` / `Makie.plot(sol)`.

- **I want to solve an optimal control problem**
Use [CTSolvers.jl](https://github.com/control-toolbox/CTSolvers.jl) which provides discretization, NLP backends, and optimization strategies.
Expand Down
5 changes: 4 additions & 1 deletion docs/src/model/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ end
```

When `Plots.jl` is loaded, the `CTModelsPlots` extension provides full plot
recipes. See [Plotting](@ref) for details.
recipes. A Makie backend (`CTModelsMakie`, activated by loading `CairoMakie` or
`GLMakie`) renders the same figure as a `Makie.Figure` through a separate
`Makie.plot(sol)` method — not the `RecipesBase.plot` stub above. See
[Plotting](@ref) for both backends.

## See also

Expand Down
9 changes: 5 additions & 4 deletions docs/src/serialization/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,27 @@ when the trigger package is present.
| `CTModelsJSON` | `JSON3` | JSON export/import of a [`Solution`](@ref CTModels.Solutions.Solution) |
| `CTModelsJLD` | `JLD2` | JLD2 (binary) export/import |
| `CTModelsPlots` | `Plots` | `Plots.plot(sol)` / `Plots.plot!(sol)` |
| `CTModelsMakie` | `Makie` (via `CairoMakie`, `GLMakie`, …) | `Makie.plot(sol)` / `Makie.plot!(sol)`, returning a `Makie.Figure` |

The public wrappers [`export_ocp_solution`](@ref CTModels.Serialization.export_ocp_solution),
[`import_ocp_solution`](@ref CTModels.Serialization.import_ocp_solution) and the plot recipe
live in the core; their **implementations** live in the extension. Until the trigger package is
loaded, calling a wrapper raises a descriptive `CTBase.ExtensionError` — the core never hard-
depends on JSON3, JLD2 or Plots.
depends on JSON3, JLD2, Plots or Makie.

```text
core wrapper ──(trigger pkg loaded?)──► extension method
│ │
export_ocp_solution no ─► CTBase.ExtensionError
plot recipe yes ─► JSON3 / JLD2 / Plots implementation
plot / Makie.plot yes ─► JSON3 / JLD2 / Plots / Makie implementation
```

## Reading order

| Page | Topic | Key symbols |
|---|---|---|
| [Export & import](export_import.md) | Persisting solutions | [`export_ocp_solution`](@ref CTModels.Serialization.export_ocp_solution), [`import_ocp_solution`](@ref CTModels.Serialization.import_ocp_solution) |
| [Plotting](plotting.md) | Visualising trajectories | `Plots.plot`, `Plots.plot!` |
| [Plotting](plotting.md) | Visualising trajectories | `Plots.plot`, `Makie.plot` |

## A solution to serialize

Expand Down Expand Up @@ -78,4 +79,4 @@ CTModels.objective(reloaded)
```

See [Export & import](export_import.md) for the formats and the resampling strategy, and
[Plotting](plotting.md) for the Plots recipe.
[Plotting](plotting.md) for the Plots and Makie backends.
47 changes: 29 additions & 18 deletions docs/src/serialization/plotting.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CurrentModule = CTModels

```@setup plt
using Plots
using CairoMakie: CairoMakie, Makie
Base.showable(::MIME"image/png", ::Plots.Plot) = false
```

Expand Down Expand Up @@ -73,27 +74,37 @@ Because the recipe reads the *typed* solution (its time grids, interpolation kin
structure) rather than raw arrays, the same call works for unified- and multiple-grid
solutions alike — see [Time grids](../solution/time_grids.md).

## Makie backend (proof of concept)
## Makie backend

A second backend renders the same figure with [Makie.jl](https://docs.makie.org).
Load a Makie backend package (`CairoMakie`, `GLMakie`, …) instead of `Plots`; the
`CTModelsMakie` extension then provides `plot(sol)`:
A second backend renders the same figure with [Makie.jl](https://docs.makie.org),
at feature parity with the Plots backend (reference lines, step controls, overlay
and forwarded attributes all supported). Load a Makie backend package
(`CairoMakie`, `GLMakie`, …); the `CTModelsMakie` extension then provides
`Makie.plot(sol)`, returning a `Makie.Figure`:

```julia
using CTModels
using CairoMakie # activates the CTModelsMakie extension
```@example plt
Makie.plot(sol)
```

```@example plt
Makie.plot(sol; layout=:group, control=:all)
```

f = plot(sol) # a Makie.Figure
plot(sol; layout=:group, control=:all)
`Makie.plot!` overlays onto an existing figure, the same way `Plots.plot!` does:

```@example plt
f = Makie.plot(sol)
Makie.plot!(f, sol; time=:normalize)
f
```

The backend is chosen by which package is loaded — `Plots.plot(sol)` renders with
Plots, `Makie.plot(sol)` renders with Makie; the `description` and keyword
arguments (`layout`, `control`, `time`, the `*_style` keywords, `color`, `size`)
are identical. Loading both packages at once means `plot` must be qualified.
The `description` and keyword arguments (`layout`, `control`, `time`, the
`*_style` keywords, `color`, `size`) are identical to the Plots backend. When both
`Plots` and a Makie package are loaded, `plot` must be qualified —
`Plots.plot(sol)` renders with Plots, `Makie.plot(sol)` with Makie.

This backend is a proof of concept (issue
[#366](https://github.com/control-toolbox/CTModels.jl/issues/366)). It does not
yet draw the reference lines (box bounds, initial/final time markers), renders
constant-interpolation controls as lines rather than steps, and does not support
`plot!` (overlay). A parity follow-up tracks these.
Style keywords beyond the neutral vocabulary (`color`, `linewidth`, `linestyle`,
`alpha`, `seriestype`) are resolved per backend: the Plots backend forwards any
attribute Plots recognises, while the Makie backend only forwards a fixed whitelist
and drops the rest. See the *Plotting Engine* guide in the CTBase documentation
("User Attributes: Series vs Axis") for the exact rule.
137 changes: 98 additions & 39 deletions ext/CTModelsMakie.jl
Original file line number Diff line number Diff line change
@@ -1,35 +1,78 @@
"""
Weak-dependency extension of CTModels providing `Makie.plot` for solutions
(proof of concept — issue #366).
Weak-dependency extension of CTModels providing `Makie.plot` / `plot!` for solutions.

Loaded automatically when both `CTModels` and `Makie` are available (for example
via `CairoMakie` or `GLMakie`). Thin plumbing on top of the backend-free case
layer [`CTModels.PlotCase`](@extref): `Makie.plot` builds the figure with
[`CTModels.PlotCase.build_figure`](@extref) and renders it through the
`CTBase.Plotting` Makie backend.

## Scope

`plot` only. `plot!` overlay is not implemented (throws `NotImplemented`), and the
Makie backend itself does not yet draw reference-line decorations (box bounds,
initial/final time markers) or step/scatter control curves — see the parity
follow-up of #366.
Loaded automatically when both `CTModels` and `Makie` are available (for example via
`CairoMakie` or `GLMakie`). This is the thin plumbing on top of the backend-free case
layer [`CTModels.PlotCase`](@extref): the public `Makie.plot` / `plot!` methods build
the figure with [`CTModels.PlotCase.build_figure`](@extref) and render it through the
`CTBase.Plotting` Makie backend, which is at feature parity with the Plots backend.
Everything domain-specific (the vocabulary, panels, decorations, layout template)
lives in `CTModels.PlotCase` and is shared with the `CTModelsPlots` extension.
"""
module CTModelsMakie

using DocStringExtensions: TYPEDSIGNATURES

using CTBase: Plotting, Exceptions
using CTBase: Plotting
using CTModels: CTModels, PlotCase
using Makie: Makie

# --- internal implementations (backend-agnostic build + Makie render) --------

"""
$(TYPEDSIGNATURES)

Internal implementation of `Makie.plot(::CTModels.Solution)`.

Builds the figure with [`CTModels.PlotCase.build_figure`](@extref) and renders it
via [`CTBase.Plotting`](@extref) (Makie backend).
"""
function _plot(
sol::CTModels.Solution, description::Symbol...; size=nothing, color=nothing, kwargs...
)
build, render_kwargs = PlotCase.split_plot_kwargs(kwargs)
fig = PlotCase.build_figure(sol, description...; size=size, build...)
# Nothing to draw (empty description, every group :none, or only path/dual in
# :group): return an empty figure, as the Plots backend does.
fig === nothing && return Makie.Figure()
return if color === nothing
Plotting.render(Plotting.MakieBackend(), fig; render_kwargs...)
else
Plotting.render(Plotting.MakieBackend(), fig; color=color, render_kwargs...)
end
end

"""
$(TYPEDSIGNATURES)

Internal implementation of `Makie.plot!(::Makie.Figure, ::CTModels.Solution)`.

Builds the figure with [`CTModels.PlotCase.build_figure`](@extref) and overlays it
onto `f` via [`CTBase.Plotting`](@extref) (Makie backend); an empty `f` is filled as
if by `plot`.
"""
function _plot!(
f::Makie.Figure, sol::CTModels.Solution, description::Symbol...; color=nothing, kwargs...
)
build, render_kwargs = PlotCase.split_plot_kwargs(kwargs)
fig = PlotCase.build_figure(sol, description...; build...)
fig === nothing && return f
return if color === nothing
Plotting.render!(Plotting.MakieBackend(), f, fig; render_kwargs...)
else
Plotting.render!(Plotting.MakieBackend(), f, fig; color=color, render_kwargs...)
end
end

# --- public methods (thin; forward to _plot / _plot!) ------------------------

"""
$(TYPEDSIGNATURES)

Plot the components of an optimal control [`CTModels.Solution`](@extref) with a
Makie backend.

Same `description` and keyword arguments as `Plots.plot(::CTModels.Solution)`
Same `description` and keyword arguments as [`Plots.plot(::CTModels.Solution)`](@extref)
(`layout`, `control`, `time`, the `*_style` / `*_bounds_style` keywords, `color`,
`size`). Returns a `Makie.Figure`.

Expand All @@ -42,36 +85,52 @@ julia> plot(sol)
julia> plot(sol, :state, :control; layout=:group, control=:all)
```
"""
function Makie.plot(
sol::CTModels.Solution, description::Symbol...; size=nothing, color=nothing, kwargs...
)
build, render_kwargs = PlotCase.split_plot_kwargs(kwargs)
fig = PlotCase.build_figure(sol, description...; size=size, build...)
fig === nothing && return Makie.Figure()
return if color === nothing
Plotting.render(Plotting.MakieBackend(), fig; render_kwargs...)
else
Plotting.render(Plotting.MakieBackend(), fig; color=color, render_kwargs...)
end
function Makie.plot(sol::CTModels.Solution, description::Symbol...; kwargs...)
return _plot(sol, description...; kwargs...)
end

"""
$(TYPEDSIGNATURES)

Overlay is not implemented by the Makie proof-of-concept backend.
Overlay the optimal control solution `sol` onto the existing `Makie.Figure` `f`. Same
behaviour and keyword arguments as [`Plots.plot(::CTModels.Solution)`](@extref); an
empty `f` is filled as if by `plot`.
"""
function Makie.plot!(
f::Makie.Figure, sol::CTModels.Solution, description::Symbol...; kwargs...
)
return _plot!(f, sol, description...; kwargs...)
end

# Throws
- `CTBase.Exceptions.NotImplemented`: always — use the Plots backend for overlays,
or wait for the parity follow-up of CTModels#366.
"""
function Makie.plot!(::CTModels.Solution, args...; kwargs...)
return throw(
Exceptions.NotImplemented(
"Makie.plot!(::CTModels.Solution) (overlay) is not implemented";
suggestion="use the Plots backend for overlays, or wait for the parity follow-up of CTModels#366",
context="CTModelsMakie",
),
)
$(TYPEDSIGNATURES)

Overlay the optimal control solution `sol` onto the current Makie figure
(`Makie.current_figure()`), creating one if none exists.
"""
function Makie.plot!(sol::CTModels.Solution, description::Symbol...; kwargs...)
f = Makie.current_figure()
return _plot!(f === nothing ? Makie.Figure() : f, sol, description...; kwargs...)
end

"""
$(TYPEDSIGNATURES)

Create an empty `Makie.Figure`, forwarding keyword arguments (`size`, …).

Mirror of `Plots.plot(; kwargs...)`: a blank canvas to overlay solutions onto with
`Makie.plot!`. Makie has no zero-argument `plot` of its own, so this fills that gap
for the `Plots`-style workflow `f = plot(; size=…); plot!(f, sol)`.

# Example
```julia-repl
julia> using CairoMakie

julia> f = plot(; size=(800, 800));

julia> plot!(f, sol)
```
"""
Makie.plot(; kwargs...) = Makie.Figure(; kwargs...)

end # module CTModelsMakie
Loading
Loading