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
21 changes: 21 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@

This document describes breaking changes in CTModels releases and how to migrate your code.

## [0.19.0-beta] - unreleased

### Plotting case-layer helpers moved to `CTModels.PlotCase`

The backend-free plotting helpers (`clean`, `do_plot`, `do_decorate`, the
`__plot_*` defaults, the panel/decoration builders) moved out of the
`CTModelsPlots` extension into a new `src` submodule, `CTModels.PlotCase`.

**Who is affected**: only code reaching these *internal* helpers through
`Base.get_extension(CTModels, :CTModelsPlots)`. The public `Plots.plot(sol)` /
`plot!(sol)` API — signatures, keywords, defaults and behaviour — is unchanged.

**Migration**: replace
`Base.get_extension(CTModels, :CTModelsPlots).clean` (etc.) with
`CTModels.PlotCase.clean`. These helpers are now available without loading `Plots`.

### `CTBase` lower bound raised to 0.30

The Makie backend needs `CTBase.Plotting.MakieBackend` (CTBase 0.30). `[compat]`
is now `CTBase = "0.30"`.

## [0.18.0] - 2026-08-23

### `Components.times(sol)` returns the time grid instead of the times model
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.19.0-beta] - unreleased

### ✨ Added

- **Makie plotting backend (proof of concept)**: new `CTModelsMakie` weak-dependency
extension (trigger: `Makie`). Load a Makie backend package (`CairoMakie`,
`GLMakie`, …) and `plot(sol)` renders a `Makie.Figure`; the backend is chosen by
which package is loaded (`Plots` → Plots, `Makie` → Makie), with identical
`description` and keyword arguments. Part of the plot-engine roadmap
([#366](https://github.com/control-toolbox/CTModels.jl/issues/366)).
- Scope: `plot` only. Not yet handled (parity follow-up): `plot!` (overlay,
throws `NotImplemented`), the reference-line decorations (box bounds,
initial/final time markers), and step/scatter control curves — constant
controls render as lines.
- Requires `CTBase` ≥ 0.30 (`Plotting.MakieBackend` + `CTBaseMakie`).

### ♻️ Internal

- The backend-free plotting case layer (vocabulary, gating, panel/decoration
builders, layout assembly, `build_figure`) moved from the `CTModelsPlots`
extension into a new `src` submodule, `CTModels.PlotCase`, so the Plots and Makie
extensions share it. `Plots.plot(sol)` behaviour, signatures and defaults are
unchanged.

## [0.18.0] - 2026-08-23

### 💥 Changed
Expand Down
11 changes: 8 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "CTModels"
uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d"
version = "0.18.0"
version = "0.19.0-beta"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
Expand All @@ -16,23 +16,27 @@ RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
[weakdeps]
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[extensions]
CTModelsJLD = "JLD2"
CTModelsJSON = "JSON3"
CTModelsMakie = "Makie"
CTModelsPlots = "Plots"

[compat]
Aqua = "0.8"
BenchmarkTools = "1"
CTBase = "0.29"
CTBase = "0.30"
CairoMakie = "0.15"
DocStringExtensions = "0.9"
JET = "0.9, 0.11, 0.12"
JLD2 = "0.6"
JSON3 = "1"
LinearAlgebra = "1"
MLStyle = "0.4"
Makie = "0.24"
MacroTools = "0.5"
OrderedCollections = "1, 2"
Parameters = "0.13"
Expand All @@ -45,6 +49,7 @@ julia = "1.10"
[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
Expand All @@ -53,4 +58,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Aqua", "BenchmarkTools", "JET", "JLD2", "JSON3", "Plots", "Random", "Test"]
test = ["Aqua", "BenchmarkTools", "CairoMakie", "JET", "JLD2", "JSON3", "Plots", "Random", "Test"]
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[compat]
CTBase = "0.29"
CTBase = "0.30"
Documenter = "1"
DocumenterInterLinks = "1"
DocumenterVitepress = "0.3"
Expand Down
27 changes: 16 additions & 11 deletions docs/api_reference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function generate_api_reference(src_dir::String, ext_dir::String)
Symbol("@unpack_PreModel"),
:is_empty,
:time_ns,
:CTModels, # PlotCase binds the parent module name for the moved case files
]
EXCLUDE_INTERNALS = vcat(
EXCLUDE_SYMBOLS,
Expand Down Expand Up @@ -102,6 +103,19 @@ function generate_api_reference(src_dir::String, ext_dir::String)
joinpath("Solutions", "show.jl"),
),
),
(
mod=CTModels.PlotCase,
title="PlotCase",
filename="plotcase",
files=src(
joinpath("PlotCase", "PlotCase.jl"),
joinpath("PlotCase", "vocabulary.jl"),
joinpath("PlotCase", "panels.jl"),
joinpath("PlotCase", "decorations.jl"),
joinpath("PlotCase", "assemble.jl"),
joinpath("PlotCase", "build.jl"),
),
),
(
mod=CTModels.Display,
title="Display",
Expand Down Expand Up @@ -166,17 +180,8 @@ function generate_api_reference(src_dir::String, ext_dir::String)

# Conditional extensions
for (sym, files) in [
(
:CTModelsPlots,
ext(
"CTModelsPlots.jl",
joinpath("case", "vocabulary.jl"),
joinpath("case", "panels.jl"),
joinpath("case", "decorations.jl"),
joinpath("case", "assemble.jl"),
joinpath("case", "plot.jl"),
),
),
(:CTModelsPlots, ext("CTModelsPlots.jl", joinpath("case", "plot.jl"))),
(:CTModelsMakie, ext("CTModelsMakie.jl")),
(:CTModelsJSON, ext("CTModelsJSON.jl")),
(:CTModelsJLD, ext("CTModelsJLD.jl")),
]
Expand Down
6 changes: 6 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ by the corresponding packages:
display the trajectories of state, control, costate, constraints, and dual
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.

If the corresponding extension package is not loaded, the public wrappers
`export_ocp_solution`, `import_ocp_solution`, and the generic `RecipesBase.plot`
throw a descriptive `CTBase.ExtensionError`.
Expand Down
25 changes: 25 additions & 0 deletions docs/src/serialization/plotting.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,28 @@ plt
Because the recipe reads the *typed* solution (its time grids, interpolation kind, and dual
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)

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)`:

```julia
using CTModels
using CairoMakie # activates the CTModelsMakie extension

f = plot(sol) # a Makie.Figure
plot(sol; layout=:group, control=:all)
```

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.

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.
77 changes: 77 additions & 0 deletions ext/CTModelsMakie.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""
Weak-dependency extension of CTModels providing `Makie.plot` for solutions
(proof of concept — issue #366).

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.
"""
module CTModelsMakie

using DocStringExtensions: TYPEDSIGNATURES

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

"""
$(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)`
(`layout`, `control`, `time`, the `*_style` / `*_bounds_style` keywords, `color`,
`size`). Returns a `Makie.Figure`.

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

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
end

"""
$(TYPEDSIGNATURES)

Overlay is not implemented by the Makie proof-of-concept backend.

# 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",
),
)
end

end # module CTModelsMakie
17 changes: 7 additions & 10 deletions ext/CTModelsPlots.jl
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
"""
Weak-dependency extension of CTModels providing `Plots.plot` / `plot!` for solutions.

Loaded automatically when both `CTModels` and `Plots` are available. This is a thin
**case layer**: it names and samples the optimal-control quantities (state, control,
costate, path constraints, duals), chooses a layout template, and delegates all layout
and rendering to the generic `CTBase.Plotting` engine.
Loaded automatically when both `CTModels` and `Plots` are available. This is the thin
plumbing on top of the backend-free case layer [`CTModels.PlotCase`](@extref): the
public `Plots.plot` / `plot!` methods build the figure with
[`CTModels.PlotCase.build_figure`](@extref) and render it through the
`CTBase.Plotting` Plots backend.
"""
module CTModelsPlots

using DocStringExtensions: TYPEDSIGNATURES

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

include(joinpath(@__DIR__, "case", "vocabulary.jl"))
include(joinpath(@__DIR__, "case", "panels.jl"))
include(joinpath(@__DIR__, "case", "decorations.jl"))
include(joinpath(@__DIR__, "case", "assemble.jl"))
include(joinpath(@__DIR__, "case", "plot.jl"))

end
Loading
Loading