Skip to content

feat(Solutions): switching structure of a solution; strict time grids - #463

Merged
ocots merged 7 commits into
mainfrom
feat/switching-structure
Oct 18, 2026
Merged

ocots merged 7 commits into
mainfrom
feat/switching-structure

Conversation

@ocots

@ocots ocots commented Oct 18, 2026 •

Copy link
Copy Markdown
Member

Closes #460. Part of the switching-structure work tracked in control-toolbox/CTFlows.jl#443.

Depends on control-toolbox/CTBase.jl#613 (Plotting.Annotation, used for the phase labels). CI stays red until that CTBase version is released and the compat is bumped. Locally, the tests run against the CTBase branch with Pkg.develop.

What

  • SwitchingModel / EmptySwitchingModel.
  • Solution gets an 11th field, switching. It is the last type parameter. The 10-argument positional constructor is kept and builds an EmptySwitchingModel.
  • build_solution(...; switching=...) works in both variants.
    • Checks: the switching times lie strictly inside the time span, and each matrix of limits has the right number of columns.
    • Matrix (sampled) trajectories are interpolated with the limits (_JumpInterpolant): the interpolation goes from the last grid point up to the left limit, and from the right limit to the next grid point. A jump is therefore not smeared over the next grid cell. Function trajectories are used as given.
  • Accessors. switching_model, switching_times, phase_labels, has_switching, left_limit(sol, c, i) and right_limit(sol, c, i). A 1-D component is returned as a scalar (DIM1).
  • Display. New rows Switching times : [...] and Phases : bang → · → boundary.
  • Plot (PlotCase, so both Plots and Makie).
    • Jumps: the left and right limits are inserted at each tᵢ, so each jump is drawn as a vertical segment. This applies to the state, costate, control, control norm, and the path constraints evaluated at both limits.
    • Lines at the switching times: switching_style. By default they look like the time markers, and a user time_style applies to them too. :none hides them.
    • Phase labels: phase_labels = :control | :all | :none and phase_labels_style. The labels are Plotting.Annotations centred on each phase. When the control is not drawn, :control uses the first drawn group.
  • Serialization (JLD2 and JSON).
    • New keys: switching_times, phase_labels and switching_<component>_<left|right>. They are written only when the solution has a switching structure.
    • format_version becomes 2. Files without these keys load with an EmptySwitchingModel.
    • Reconstruction keeps the jumps.

Breaking

A time grid with a repeated time now raises an IncorrectArgument. It used to be accepted silently. BREAKING.md gives a migration block. Suggested release: 0.20.0.

Checked: CTDirect already rejects grids that are not strictly increasing (DOCPtime) and derives all its grids from that one, so it never passes a repeated time. The OptimalControl suite has not been run.

Tests

  • suite/solutions/test_switching.jl (100 tests): construction and defaults, every error with its exception type, getters (scalar and vector), jump-aware interpolation (τ on or between grid points, piecewise-constant control, function data, unknown limits, multiple grids), strict grid, the 10-argument constructor, display (labels, ·, no rows, separator), @inferred.
  • suite/plotting/test_plot_switching.jl (67): splicing (τ on the grid, inserted, after the last sample, unchanged cases), vertical segments in every panel, the control norm, path constraints at the limits, switching lines (style inheritance and merge, :none, kept when time_style=:none, normalised time), labels (default, :all, :none, style, empty labels, fallback, group layout, single labelled phase), invalid phase_labels, keyword routing.
  • suite/extensions/test_plot_switching_render.jl (15): rendering with Plots (the series holds the vertical segment, annotations on the control subplot) and with Makie (VLines and Text counts).
  • suite/serialization/test_switching_serialization.jl (96): serialized keys, deserialization, JLD and JSON round trips (limits, labels, trajectories with jumps), labels only, a labelled single phase, files without the keys, a version-1 file.

The full suite passes locally: 79,424 existing tests with no regression, plus the new files (one run before the last test fixes, then the new files again). The docs build cleanly (two passes), including the examples of the new page.

Found along the way (not fixed here)

A solution built without iterations or constraints_violation (NotProvided) does not survive an export/import round trip: Float64(NotProvided) fails in JLD2, and Float64(::String) fails in JSON. This is the case for every solution built by CTFlows.Flow(ocp, law). Worth a separate issue.

🤖 Generated with Claude Code

A Solution keeps the switching structure of a solution made of phases:
switching times, phase labels, and left/right limits of the state, costate
and control (`build_solution(...; switching=SwitchingModel(...))`).
Sampled trajectories are interpolated with the limits; the display lists
the switching times and phases; plots draw the jumps as vertical segments,
with lines at the switching times and the phase labels; JLD2/JSON save and
read the structure back.

Breaking: a time grid with a repeated time raises an IncorrectArgument.

Refs #460, control-toolbox/CTFlows.jl#443.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ocots and others added 4 commits October 18, 2026 12:30
- PlotCase uses Plotting.splice_jumps / switching_vlines / phase_annotations instead of
  its own copies (shared with CTFlows); _phase_label_group extracted from _build_root
- left_limit/right_limit: PreconditionError when the limits are not stored (EXC1)
- suggestions on every new error (EXC3); one component check for the limits
- one pass for the repeated-time check; serialized limit keys from one function,
  reused by the JSON extension

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g models

- plot(sol; jumps=:none) breaks the curves between the two limits instead of drawing
  the vertical segment (default :segment); the path constraints are evaluated at both
  limits and spliced the same way
- Base.show for SwitchingModel (tree, as the Solution display) and EmptySwitchingModel;
  the phase sequence comes from one helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ocots ocots closed this Oct 18, 2026
@ocots ocots added run breakage Trigger the Breakage workflow on this PR run ci github-runner Trigger the CI workflow on GitHub-hosted runners run documentation Trigger the Documentation workflow on this PR labels Oct 18, 2026
@ocots ocots reopened this Oct 18, 2026
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Breakage test results
Date: 2026-10-18 19:00:45

Name Latest Stable
CTDirect compat: v0.19.8 compat: v0.19.8
CTFlows compat: v0.19.8 compat: v0.19.8
CTSolvers compat: v0.19.8 compat: v0.19.8
OptimalControl compat: v0.19.8 compat: v0.19.8

…tching_vlines, phase_annotations)

Tested against the registered release: full suite and docs build.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@ocots ocots removed run breakage Trigger the Breakage workflow on this PR run ci github-runner Trigger the CI workflow on GitHub-hosted runners run documentation Trigger the Documentation workflow on this PR labels Oct 18, 2026
@ocots
ocots marked this pull request as ready for review October 18, 2026 18:50
@ocots
ocots merged commit 0728a87 into main Oct 18, 2026
4 checks passed
@ocots
ocots deleted the feat/switching-structure branch October 18, 2026 18:50
@ocots ocots mentioned this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Solution: keep the switching structure (switching times, left/right limits, phase labels); reject duplicate grid times

1 participant