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
4 changes: 3 additions & 1 deletion ext/CTModelsJSON.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const _OPTIONAL_MATRIX_FIELDS = [
]

"Switching limits (present when the solution holds a switching structure), converted like the other matrices."
const _SWITCHING_MATRIX_FIELDS = [first(k) for k in CTModels.Solutions._SWITCHING_LIMIT_KEYS]
const _SWITCHING_MATRIX_FIELDS = [
first(k) for k in CTModels.Solutions._SWITCHING_LIMIT_KEYS
]

"""
Convert Matrix fields to Vector{Vector} for JSON export.
Expand Down
25 changes: 19 additions & 6 deletions src/PlotCase/build.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ function _phase_label_group(
)::Symbol
phase_labels === :all && return :all
control && return :control
for (group, drawn) in ((:state, state), (:costate, costate), (:path, path), (:dual, dual))
for (group, drawn) in
((:state, state), (:costate, costate), (:path, path), (:dual, dual))
drawn && return group
end
return :none
Expand Down Expand Up @@ -117,7 +118,8 @@ function _build_root(
path=do_path,
dual=do_dual,
)
_texts(group) = (label_group === :all || label_group === group) ? texts : Plotting.Annotation[]
_texts(group) =
(label_group === :all || label_group === group) ? texts : Plotting.Annotation[]
ncomp(p) = size(p.data, 2)
L(p; hlines=Vector{Plotting.HLine}[], group::Symbol=:none) = Plotting.lower(
p;
Expand All @@ -132,19 +134,26 @@ function _build_root(
if layout === :group
# No bounds lines in :group (historical); only the time markers, via `vlines`.
cells = Plotting.AbstractLayoutNode[]
do_state && push!(cells, L(_state_panel(sol, state_style; connect=connect); group=:state))
do_state &&
push!(cells, L(_state_panel(sol, state_style; connect=connect); group=:state))
do_costate && push!(
cells,
L(
_costate_panel(
sol, costate_style; layout=layout, state_shown=do_state, connect=connect
sol,
costate_style;
layout=layout,
state_shown=do_state,
connect=connect,
);
group=:costate,
),
)
if do_control
# with `control=:all`, the labels go to the first control cell only
for (k, cp) in enumerate(_control_panels(sol, control, control_style, layout; connect=connect))
for (k, cp) in enumerate(
_control_panels(sol, control, control_style, layout; connect=connect)
)
push!(cells, L(cp; group=(k == 1 ? :control : :none)))
end
end
Expand All @@ -166,7 +175,11 @@ function _build_root(
costate_col = if do_costate
L(
_costate_panel(
sol, costate_style; layout=layout, state_shown=do_state, connect=connect
sol,
costate_style;
layout=layout,
state_shown=do_state,
connect=connect,
);
group=:costate,
)
Expand Down
5 changes: 4 additions & 1 deletion src/PlotCase/decorations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,10 @@ solution has no switching time.
"""
function _switching_vlines(sol, time::Symbol, style::NamedTuple)
return Plotting.switching_vlines(
CTModels.time_grid(sol, :state), CTModels.switching_times(sol); time=time, style=style
CTModels.time_grid(sol, :state),
CTModels.switching_times(sol);
time=time,
style=style,
)
end

Expand Down
2 changes: 1 addition & 1 deletion src/Solutions/build_solution.jl
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ Throw the error of a time grid with a repeated time.
- [`CTBase.Exceptions.IncorrectArgument`](@extref): always.
"""
function _throw_repeated_time(t::Float64, component_name::String)
throw(
return throw(
Exceptions.IncorrectArgument(
"repeated time in the $component_name time grid";
got="time $t appears more than once",
Expand Down
6 changes: 1 addition & 5 deletions src/Solutions/interpolation_helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,7 @@ function build_interpolated_function(
if !isnothing(jumps) && data isa AbstractMatrix
sw, component = jumps
func = _with_jumps(
func,
T[1:size(data, 1)],
sw,
component;
constant=(interpolation === :constant),
func, T[1:size(data, 1)], sw, component; constant=(interpolation === :constant)
)
end

Expand Down
36 changes: 25 additions & 11 deletions src/Solutions/switching_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,8 @@ function SwitchingModel(;
times::AbstractVector{<:Real},
labels::AbstractVector{<:AbstractString}=fill("", length(times) + 1),
state::Tuple{AbstractMatrix{<:Real},AbstractMatrix{<:Real}}=_no_limits(length(times)),
costate::Tuple{AbstractMatrix{<:Real},AbstractMatrix{<:Real}}=_no_limits(
length(times)
),
control::Tuple{AbstractMatrix{<:Real},AbstractMatrix{<:Real}}=_no_limits(
length(times)
),
costate::Tuple{AbstractMatrix{<:Real},AbstractMatrix{<:Real}}=_no_limits(length(times)),
control::Tuple{AbstractMatrix{<:Real},AbstractMatrix{<:Real}}=_no_limits(length(times)),
)
ts = collect(Float64, times)
all(isfinite, ts) || throw(
Expand Down Expand Up @@ -371,7 +367,11 @@ function _limit(sol::Solution, component::Symbol, i::Integer, side::Int)
Exceptions.IncorrectArgument(
"no switching time with this index";
got="i = $i",
expected=n == 0 ? "no index: the solution has no switching time" : "1 ≤ i ≤ $n",
expected=if n == 0
"no index: the solution has no switching time"
else
"1 ≤ i ≤ $n"
end,
suggestion="Use an index of switching_times(sol)",
context=(side == 1 ? "left_limit" : "right_limit"),
),
Expand Down Expand Up @@ -463,7 +463,11 @@ Wrap the interpolant `base` of sampled data on `grid` so that it keeps the jumps
switching time, or no limit of `component`.
"""
function _with_jumps(
base, grid::Vector{Float64}, sw::AbstractSwitchingModel, component::Symbol; constant=false
base,
grid::Vector{Float64},
sw::AbstractSwitchingModel,
component::Symbol;
constant=false,
)
has_switching(sw) || return base
left, right = limits(sw, component)
Expand Down Expand Up @@ -493,7 +497,9 @@ Return the components whose limits a switching model stores, e.g. `"state, costa
`"none"`.
"""
function _stored_limits(m::SwitchingModel)::String
stored = [String(c) for c in (:state, :costate, :control) if size(limits(m, c)[1], 2) > 0]
stored = [
String(c) for c in (:state, :costate, :control) if size(limits(m, c)[1], 2) > 0
]
return isempty(stored) ? "none" : join(stored, ", ")
end

Expand All @@ -516,7 +522,13 @@ phase label.
function Base.show(io::IO, ::MIME"text/plain", ::EmptySwitchingModel)
fmt = CTBase.Core.get_format_codes(io)
return print(
io, fmt.name, "EmptySwitchingModel", fmt.reset, fmt.muted, " (no switching)", fmt.reset
io,
fmt.name,
"EmptySwitchingModel",
fmt.reset,
fmt.muted,
" (no switching)",
fmt.reset,
)
end

Expand All @@ -531,7 +543,9 @@ function Base.show(io::IO, m::SwitchingModel)
print(io, fmt.name, "SwitchingModel", fmt.reset, "(")
print(io, fmt.label, "times=", fmt.reset, fmt.value, m.times, fmt.reset)
if any(!isempty, m.labels)
print(io, ", ", fmt.label, "phases=", fmt.reset, fmt.value, _phase_sequence(m.labels))
print(
io, ", ", fmt.label, "phases=", fmt.reset, fmt.value, _phase_sequence(m.labels)
)
print(io, fmt.reset)
end
return print(io, ")")
Expand Down
8 changes: 4 additions & 4 deletions test/suite/extensions/test_plot_switching_render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function _sol()
Building.time!(pre; t0=0.0, tf=1.0)
Building.state!(pre, 1)
Building.control!(pre, 1)
Building.dynamics!(pre, (r, t, x, u, v) -> (r[1] = u; nothing))
Building.dynamics!(pre, (r, t, x, u, v) -> (r[1]=u; nothing))
Building.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2)
ocp = Building.build(pre)
T = collect(range(0.0, 1.0; length=11))
Expand Down Expand Up @@ -70,8 +70,7 @@ function test_plot_switching_render()
# the phase labels are on the control cell only
Test.@test length(plt.subplots[3][:annotations]) == 2
Test.@test isempty(plt.subplots[1][:annotations])
Test.@test sort([a[3].str for a in plt.subplots[3][:annotations]]) ==
["bang+", "bang-"]
Test.@test sort([a[3].str for a in plt.subplots[3][:annotations]]) == ["bang+", "bang-"]
# no labels, no switching lines: still renders
Test.@test Plots.plot(sol; phase_labels=:none, switching_style=:none) isa
Plots.Plot
Expand All @@ -82,7 +81,8 @@ function test_plot_switching_render()
ys = br.subplots[1].series_list[1][:y]
Test.@test count(isnan, ys) == 1
# one color per trajectory: one series for the whole state component
Test.@test count(s -> s[:seriestype] === :path, plt.subplots[1].series_list) == 1
Test.@test count(s -> s[:seriestype] === :path, plt.subplots[1].series_list) ==
1
end

Test.@testset "Makie" begin
Expand Down
3 changes: 1 addition & 2 deletions test/suite/meta/test_no_import.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ function test_no_import()
end
if check_dotted && occursin(DOTTED_USING_RE, line)
push!(
violations,
"$relpath:$line_number: dotted `using`: $(strip(line))",
violations, "$relpath:$line_number: dotted `using`: $(strip(line))"
)
end
end
Expand Down
61 changes: 43 additions & 18 deletions test/suite/plotting/test_plot_switching.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ function _ocp()
Building.time!(pre; t0=0.0, tf=2.0)
Building.state!(pre, 1)
Building.control!(pre, 1)
Building.dynamics!(pre, (r, t, x, u, v) -> (r[1] = u; nothing))
Building.dynamics!(pre, (r, t, x, u, v) -> (r[1]=u; nothing))
Building.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2)
Building.constraint!(
pre, :path; f=(r, t, x, u, v) -> (r[1] = x + u; nothing), lb=[-10.0], ub=[10.0], label=:g
pre,
:path;
f=(r, t, x, u, v) -> (r[1]=x + u; nothing),
lb=[-10.0],
ub=[10.0],
label=:g,
)
return Building.build(pre)
end
Expand Down Expand Up @@ -66,11 +71,13 @@ _leaves(fig) = Plotting.leaves(fig.root)
_leaf(fig, title) = only(l for l in _leaves(fig) if l.axes.title == title)
_series(fig, title) = _leaf(fig, title).axes.series[1]
_decos(fig, title, D) = filter(d -> d isa D, _leaf(fig, title).axes.decorations)
_texts(fig) = reduce(
vcat,
(filter(d -> d isa Plotting.Annotation, l.axes.decorations) for l in _leaves(fig));
init=Plotting.Annotation[],
)
function _texts(fig)
return reduce(
vcat,
(filter(d -> d isa Plotting.Annotation, l.axes.decorations) for l in _leaves(fig));
init=Plotting.Annotation[],
)
end

function test_plot_switching()
Test.@testset "PlotCase: switching structure" verbose=VERBOSE showtiming=SHOWTIMING begin
Expand All @@ -81,7 +88,9 @@ function test_plot_switching()

Test.@testset "_spliced_samples: insertion of the limits" begin
sol = _sol()
Ts, M = CTModels.PlotCase._spliced_samples(sol, T, reshape(_x.(T), :, 1), :state)
Ts, M = CTModels.PlotCase._spliced_samples(
sol, T, reshape(_x.(T), :, 1), :state
)
# τ is in the grid: its sample is replaced by (τ, left), (τ, right)
Test.@test count(==(τ), Ts) == 2
Test.@test length(Ts) == length(T) + 1
Expand All @@ -91,18 +100,24 @@ function test_plot_switching()
Test.@test issorted(Ts)
# τ not in the samples: inserted at its place
Tn = [0.0, 0.5, 1.5, 2.0]
Ts2, M2 = CTModels.PlotCase._spliced_samples(sol, Tn, reshape(_x.(Tn), :, 1), :state)
Ts2, M2 = CTModels.PlotCase._spliced_samples(
sol, Tn, reshape(_x.(Tn), :, 1), :state
)
Test.@test Ts2 == [0.0, 0.5, τ, τ, 1.5, 2.0]
Test.@test M2[:, 1] ≈ [0.0, 0.5, τ, τ + 1.0, 2.5, 3.0]
# τ after the last sample
Ts3, _ = CTModels.PlotCase._spliced_samples(sol, [0.0, 0.5], zeros(2, 1), :state)
Ts3, _ = CTModels.PlotCase._spliced_samples(
sol, [0.0, 0.5], zeros(2, 1), :state
)
Test.@test Ts3 == [0.0, 0.5, τ, τ]
# unchanged when no switching time, or limits not known
plain = _sol(; switching=Solutions.EmptySwitchingModel())
Test.@test CTModels.PlotCase._spliced_samples(plain, T, zeros(11, 1), :state) ==
(T, zeros(11, 1))
nolim = _sol(; switching=_sw(; limits=false))
Test.@test CTModels.PlotCase._spliced_samples(nolim, T, zeros(11, 1), :state)[1] == T
Test.@test CTModels.PlotCase._spliced_samples(
nolim, T, zeros(11, 1), :state
)[1] == T
# samples with other components than the limits: an error
Test.@test_throws Exceptions.IncorrectArgument CTModels.PlotCase._spliced_samples(
sol, T, zeros(11, 2), :state
Expand Down Expand Up @@ -153,7 +168,9 @@ function test_plot_switching()
Test.@test s.y[k] ≈ τ + 1.0 # x⁻ + u⁻
Test.@test s.y[k + 1] ≈ (τ + 1.0) - 1.0 # x⁺ + u⁺
# limits not known: plain samples
fig2 = CTModels.PlotCase.build_figure(_sol(; switching=_sw(; limits=false)), :path)
fig2 = CTModels.PlotCase.build_figure(
_sol(; switching=_sw(; limits=false)), :path
)
Test.@test count(==(τ), _series(fig2, "path constraints").x) == 1
end

Expand All @@ -168,16 +185,23 @@ function test_plot_switching()
Test.@test isnan(s.y[k + 1]) # no vertical segment
Test.@test s.y[k + 2] ≈ right
end
p = _series(CTModels.PlotCase.build_figure(_sol(), :path; jumps=:none), "path constraints")
p = _series(
CTModels.PlotCase.build_figure(_sol(), :path; jumps=:none),
"path constraints",
)
k = findfirst(==(τ), p.x)
Test.@test isnan(p.y[k + 1])
Test.@test (p.y[k], p.y[k + 2]) == (τ + 1.0, (τ + 1.0) - 1.0)
# default: the vertical segment, no NaN
Test.@test !any(isnan, _series(CTModels.PlotCase.build_figure(_sol()), "state").y)
Test.@test !any(
isnan, _series(CTModels.PlotCase.build_figure(_sol()), "state").y
)
end

Test.@testset "no switching structure: unchanged panels" begin
fig = CTModels.PlotCase.build_figure(_sol(; switching=Solutions.EmptySwitchingModel()))
fig = CTModels.PlotCase.build_figure(
_sol(; switching=Solutions.EmptySwitchingModel())
)
Test.@test _series(fig, "state").x == T
Test.@test isempty(_texts(fig))
Test.@test length(_decos(fig, "state", Plotting.VLine)) == 2 # t0 and tf only
Expand Down Expand Up @@ -215,8 +239,7 @@ function test_plot_switching()

Test.@testset "switching lines and labels under time normalisation" begin
fig = CTModels.PlotCase.build_figure(_sol(); time=:normalize)
Test.@test [d.value for d in _decos(fig, "state", Plotting.VLine)] ==
[0.0, 1.0, 0.5]
Test.@test [d.value for d in _decos(fig, "state", Plotting.VLine)] == [0.0, 1.0, 0.5]
Test.@test [a.x for a in _texts(fig)] ≈ [0.25, 0.75]
end

Expand All @@ -238,7 +261,9 @@ function test_plot_switching()
Test.@test length(_decos(fig, "state", Plotting.Annotation)) == 2
Test.@test length(_decos(fig, "costate", Plotting.Annotation)) == 2
Test.@test length(_decos(fig, "control", Plotting.Annotation)) == 2
Test.@test isempty(_texts(CTModels.PlotCase.build_figure(_sol(); phase_labels=:none)))
Test.@test isempty(
_texts(CTModels.PlotCase.build_figure(_sol(); phase_labels=:none))
)
st = CTModels.PlotCase.build_figure(
_sol(); phase_labels_style=(color=:red, fontsize=14)
)
Expand Down
4 changes: 3 additions & 1 deletion test/suite/serialization/test_json_format.jl
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ function test_json_format()
Test.@test sprint(show, Serialization.JSONTag()) == "JSONTag()"
ocp, sol = _small_solution()
path = joinpath(dir, "tag")
Serialization.export_ocp_solution(Serialization.JSONTag(), sol; filename=path)
Serialization.export_ocp_solution(
Serialization.JSONTag(), sol; filename=path
)
back = Serialization.import_ocp_solution(
Serialization.JSONTag(), ocp; filename=path
)
Expand Down
13 changes: 10 additions & 3 deletions test/suite/serialization/test_optional_fields.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function _ocp()
Building.time!(pre; t0=0.0, tf=1.0)
Building.state!(pre, 1)
Building.control!(pre, 1)
Building.dynamics!(pre, (r, t, x, u, v) -> (r[1] = u; nothing))
Building.dynamics!(pre, (r, t, x, u, v) -> (r[1]=u; nothing))
Building.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2)
return Building.build(pre)
end
Expand All @@ -35,8 +35,15 @@ function _solution(ocp; set::Bool)
(;)
end
return Solutions.build_solution(
ocp, T, t -> [t], t -> [1.0], Float64[], t -> [0.0];
objective=0.5, successful=true, extra...,
ocp,
T,
t -> [t],
t -> [1.0],
Float64[],
t -> [0.0];
objective=0.5,
successful=true,
extra...,
)
end

Expand Down
Loading
Loading