Skip to content

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

Closed

Description

@ocots

Summary

An indirect method fixes the structure of the solution: switching times, jumps of the state
or costate, and changes of control law. That is what sets it apart from a direct method.
Today a Solution built from a multi-phase flow (CTFlows.Flow(ocp, law)) loses all of
it:

  • x, p and u are functions of t, so they cannot return two values at tᵢ;
  • the grid passed in contains duplicates, which issorted(T; lt=<) lets through without
    any warning;
  • the plot draws a slanted segment instead of a vertical jump.

Part of the switching-structure work tracked in control-toolbox/CTFlows.jl#443 (detailed
plan there).

Proposal

abstract type AbstractSwitchingModel end
struct EmptySwitchingModel <: AbstractSwitchingModel end
struct SwitchingModel <: AbstractSwitchingModel     # built through a keyword constructor
    times::Vector{Float64}                 # strictly increasing, in (t0, tf); may be empty
    labels::Vector{String}                 # one per phase (length(times) + 1); "" = no label
    state::NTuple{2,Matrix{Float64}}       # (left, right), one row per tᵢ
    costate::NTuple{2,Matrix{Float64}}
    control::NTuple{2,Matrix{Float64}}
end
  • Solution. New last field switching, which is also the last type parameter.
  • build_solution. Gains the keyword
    switching::AbstractSwitchingModel=EmptySwitchingModel(), in both the matrix and the
    function variants. It is validated: tᵢ in (t0, tf) and strictly increasing, the
    number of labels, and the matrix dimensions.
  • Left values are stored too. The grid T need not contain the tᵢ, and an imported
    solution can rebuild both limits.
  • Keyword constructor. It lets fields be added later without breaking anything:
    constraint duals at tᵢ, see Flow(ocp, law) Solution: reconstruct the constraint duals from the multipliers CTFlows.jl#472.
  • Getters.
    • switching_times(sol), empty if there are none;
    • phase_labels(sol);
    • has_switching(sol);
    • the left and right limits per component.
  • Strict grid (breaking → 0.20.0). A duplicate time in a grid raises
    IncorrectArgument. A non-sorted grid keeps the current behaviour (sort + @warn).
    Check the CTDirect and OptimalControl suites before the release.
  • Display.
    • New row: ├─ Switching times : [0.31, 0.72].
    • New row: ├─ Phases : bang → singular → bang, shown only if at least one label is
      non-empty.
  • Plot (PlotCase, so Plots and Makie are both covered).
    • Jumps: after sampling on T, splice in the rows (tᵢ, left) then (tᵢ, right).
      This applies to the state, costate, control, control norm, and to the path
      constraints evaluated at the limits.
    • Vertical lines at tᵢ: new keyword switching_style.
      • Its default has the same look as the existing time lines, _TIME_STYLE, merged with
        the user's time_style.
      • A NamedTuple is merged on top; :none hides the lines.
      • The lines appear in every cell, including when time=:normalize.
    • Phase labels: phase_labels ∈ (:control, :all, :none) (default :control), plus
      phase_labels_style.
  • Serialization (JSON and JLD).
    • New keys: switching_times, phase_labels, switching_{state,costate,control}_{left,right}.
    • A file without these keys loads with EmptySwitchingModel, so old files still load.
    • Reconstruction interpolates phase by phase, starting each interval (tᵢ, tᵢ₊₁] from
      the right limit, so an imported solution keeps its jumps.

Tests

  • Construction. Nominal cases with 0, 1 and n switching times, with and without
    labels. Each validation error with its exception type. Duplicate in a grid →
    IncorrectArgument. Unsorted grid → warning. Default EmptySwitchingModel.
    Getters, including the empty case. @inferred on the getters.
  • Show. Reference strings with and without switching, with and without labels, and with
    some labels empty.
  • Plot (Plots and Makie).
    • Panel data have the vertical rows; a tᵢ already in T is not duplicated.
    • VLines at tᵢ, including with time=:normalize.
    • switching_style: the default, :none, and a merged NamedTuple.
    • phase_labels set to :control, :all and :none, plus the fallback.
    • Path constraints at the limits.
  • Serialization. JSON and JLD round trips with jumps and labels. Import of an old
    file. The reconstructed value just after tᵢ is close to the right limit.

Depends on / consumers

Activity

  1. ocots commented on Oct 18, 2026

    @ocots
    MemberAuthor

    Implemented in #463 (draft). Points to validate:

    1. Fallback of phase_labels = :control. When the control is not drawn, the labels go to the first drawn group (usually the state, at the top of the figure). The plan in CTFlows#443 said the last panel. I chose the top because it reads better. OK?
    2. has_switching(sol) is false for a labelled single phase. Such a solution has no switching time, while phase_labels(sol) == ["bang"]. OK, or should a single phase with a label count as "having a switching structure"?
    3. Strict grid (breaking). 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 against this branch.
    4. format_version is now 2. Files without the switching keys still load, with an EmptySwitchingModel.
    5. Found along the way, unrelated to this issue. A solution built without iterations / constraints_violation (NotProvided) does not survive export + import: Float64(NotProvided) fails in JLD2 and Float64(::String) fails in JSON. Every solution of CTFlows.Flow(ocp, law) is affected. Should it go in a separate issue?
    6. Default styles. Switching-time lines look like the t0/tf markers; phase labels are gray30 at font size 9.

    Local status: the full suite passes (no regression), and the docs build cleanly (two passes).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

featureNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions