diff --git a/ext/CTModelsJSON.jl b/ext/CTModelsJSON.jl index b4f7cbaa..14e64aa3 100644 --- a/ext/CTModelsJSON.jl +++ b/ext/CTModelsJSON.jl @@ -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. diff --git a/src/PlotCase/build.jl b/src/PlotCase/build.jl index 7ef4e66b..3a05b121 100644 --- a/src/PlotCase/build.jl +++ b/src/PlotCase/build.jl @@ -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 @@ -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; @@ -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 @@ -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, ) diff --git a/src/PlotCase/decorations.jl b/src/PlotCase/decorations.jl index 2553a24b..a9e5a170 100644 --- a/src/PlotCase/decorations.jl +++ b/src/PlotCase/decorations.jl @@ -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 diff --git a/src/Solutions/build_solution.jl b/src/Solutions/build_solution.jl index 475db59a..c0be94aa 100644 --- a/src/Solutions/build_solution.jl +++ b/src/Solutions/build_solution.jl @@ -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", diff --git a/src/Solutions/interpolation_helpers.jl b/src/Solutions/interpolation_helpers.jl index 444ba3f1..e5856433 100644 --- a/src/Solutions/interpolation_helpers.jl +++ b/src/Solutions/interpolation_helpers.jl @@ -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 diff --git a/src/Solutions/switching_model.jl b/src/Solutions/switching_model.jl index 0e337bde..f38f768a 100644 --- a/src/Solutions/switching_model.jl +++ b/src/Solutions/switching_model.jl @@ -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( @@ -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"), ), @@ -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) @@ -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 @@ -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 @@ -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, ")") diff --git a/test/suite/extensions/test_plot_switching_render.jl b/test/suite/extensions/test_plot_switching_render.jl index 6411df8a..04a94a03 100644 --- a/test/suite/extensions/test_plot_switching_render.jl +++ b/test/suite/extensions/test_plot_switching_render.jl @@ -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)) @@ -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 @@ -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 diff --git a/test/suite/meta/test_no_import.jl b/test/suite/meta/test_no_import.jl index 26cb5172..02a2830a 100644 --- a/test/suite/meta/test_no_import.jl +++ b/test/suite/meta/test_no_import.jl @@ -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 diff --git a/test/suite/plotting/test_plot_switching.jl b/test/suite/plotting/test_plot_switching.jl index 2b73da6d..5090021c 100644 --- a/test/suite/plotting/test_plot_switching.jl +++ b/test/suite/plotting/test_plot_switching.jl @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) ) diff --git a/test/suite/serialization/test_json_format.jl b/test/suite/serialization/test_json_format.jl index 71f946ec..a4bbb07c 100644 --- a/test/suite/serialization/test_json_format.jl +++ b/test/suite/serialization/test_json_format.jl @@ -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 ) diff --git a/test/suite/serialization/test_optional_fields.jl b/test/suite/serialization/test_optional_fields.jl index 9d00b2d5..007bbe8c 100644 --- a/test/suite/serialization/test_optional_fields.jl +++ b/test/suite/serialization/test_optional_fields.jl @@ -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 @@ -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 diff --git a/test/suite/serialization/test_switching_serialization.jl b/test/suite/serialization/test_switching_serialization.jl index 7ac9d6e8..c4f77653 100644 --- a/test/suite/serialization/test_switching_serialization.jl +++ b/test/suite/serialization/test_switching_serialization.jl @@ -19,15 +19,29 @@ function _ocp() Building.time!(pre; t0=0.0, tf=1.0) Building.state!(pre, 2) Building.control!(pre, 1) - Building.dynamics!(pre, (r, t, x, u, v) -> (r[1] = x[2]; r[2] = u; nothing)) + Building.dynamics!(pre, (r, t, x, u, v) -> (r[1]=x[2]; r[2]=u; nothing)) Building.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2) return Building.build(pre) end const τ1, τ2 = 0.3, 0.7 -_x(t) = t <= τ1 ? [t, 1.0] : t <= τ2 ? [t + 1.0, 1.0] : [t + 1.0, 3.0] +_x(t) = + if t <= τ1 + [t, 1.0] + elseif t <= τ2 + [t + 1.0, 1.0] + else + [t + 1.0, 3.0] + end _p(t) = t <= τ2 ? [0.0, 0.0] : [1.0, -1.0] -_u(t) = t <= τ1 ? 1.0 : t <= τ2 ? 0.0 : -1.0 +_u(t) = + if t <= τ1 + 1.0 + elseif t <= τ2 + 0.0 + else + -1.0 + end function _sw() return Solutions.SwitchingModel(; @@ -131,7 +145,9 @@ function test_switching_serialization() back = Serialization.import_ocp_solution(ocp; filename=f, format=fmt) Test.@test CTModels.switching_times(back) == [τ1] Test.@test CTModels.phase_labels(back) == ["a", "b"] - Test.@test size(Solutions.limits(Solutions.switching_model(back), :state)[1], 2) == 0 + Test.@test size( + Solutions.limits(Solutions.switching_model(back), :state)[1], 2 + ) == 0 end end diff --git a/test/suite/solutions/test_switching.jl b/test/suite/solutions/test_switching.jl index 699caccf..fc16a3ea 100644 --- a/test/suite/solutions/test_switching.jl +++ b/test/suite/solutions/test_switching.jl @@ -20,7 +20,7 @@ function _ocp_1d() 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 @@ -32,7 +32,7 @@ function _ocp_2d() Building.time!(pre; t0=0.0, tf=1.0) Building.state!(pre, 2) Building.control!(pre, 1) - Building.dynamics!(pre, (r, t, x, u, v) -> (r[1] = x[2]; r[2] = u; nothing)) + Building.dynamics!(pre, (r, t, x, u, v) -> (r[1]=x[2]; r[2]=u; nothing)) Building.objective!(pre, :min; lagrange=(t, x, u, v) -> 0.5 * u^2) return Building.build(pre) end @@ -95,9 +95,7 @@ function test_switching() Test.@test size(Solutions.limits(d, :state)[1]) == (2, 0) # conversions: Int times, SubString labels, Int matrices c = Solutions.SwitchingModel(; - times=[1], - labels=[SubString("ab", 1, 1), "b"], - state=([1;;], [2;;]), + times=[1], labels=[SubString("ab", 1, 1), "b"], state=([1;;], [2;;]) ) Test.@test Solutions.switching_times(c) == [1.0] Test.@test Solutions.phase_labels(c) == ["a", "b"] @@ -127,7 +125,8 @@ function test_switching() Test.@test occursin("Switching times : [0.5]", plain) Test.@test occursin("Phases : bang+ → bang-", plain) Test.@test occursin("Limits : state, costate, control", plain) - Test.@test sprint(show, sw) == "SwitchingModel(times=[0.5], phases=bang+ → bang-)" + Test.@test sprint(show, sw) == + "SwitchingModel(times=[0.5], phases=bang+ → bang-)" # labels only, limits not stored; an unlabelled phase shows as · m = Solutions.SwitchingModel(; times=[0.2, 0.6], labels=["a", "", "c"]) Test.@test occursin("Phases : a → · → c", sprint(show, MIME"text/plain"(), m))