diff --git a/NEWS.md b/NEWS.md index 512d4dd..f02fac7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,12 @@ # Release notes +## Version 0.10.5 (2026-06-28) + +* Increased robustness for checking whether a `TimeProfile` is of the correct type for indexing. +* Added support in checks for `PartitionProfile`: + * This support is only included for indexing. + * Support for check of length infeasible due to the problems related potentially differing partitions. + ## Version 0.10.4 (2026-06-15) ### Bug fixes @@ -26,7 +33,7 @@ ### Minor updates -* Add support for JuMP.AbstractModel to allow custom model types, e.g. for decomposition. +* Add support for JuMP.AbstractModel to allow custom model types, *e.g.*, for decomposition. ### Bug fixes diff --git a/Project.toml b/Project.toml index f216017..7a26183 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsBase" uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" authors = ["Lars Hellemo , Julian Straus "] -version = "0.10.4" +version = "0.10.5" [deps] JuMP = "4076af6c-e467-56ae-b986-b466b2749572" @@ -18,5 +18,5 @@ EMIExt = "EnergyModelsInvestments" EnergyModelsInvestments = "0.9" JuMP = "1" SparseVariables = "0.7.3" -TimeStruct = "0.9.11" +TimeStruct = "0.9.12" julia = "1.10" diff --git a/docs/src/library/internals/functions.md b/docs/src/library/internals/functions.md index 3c9d188..94c2938 100644 --- a/docs/src/library/internals/functions.md +++ b/docs/src/library/internals/functions.md @@ -67,6 +67,10 @@ check_profile check_strategic_profile check_representative_profile check_scenario_profile +check_partition_profile +check_sub_profs +check_sub_profile +check_not_profile compile_logs ``` diff --git a/src/checks.jl b/src/checks.jl index 3f89ab2..e0137a7 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -639,6 +639,126 @@ function check_profile( end check_profile(fieldname, value, ts, p_msg) = nothing +""" + check_not_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {TP<:TimeProfile} + +Check that the profile `sub_prof` is not of type `TP`. +""" +function check_not_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {TP<:TimeProfile} + bool_tp = !isa(sub_prof, TP) + @assert_or_log(bool_tp, "`$(TP)`s " * sub_msg) + return bool_tp +end + +""" + check_sub_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {T<:TimeProfile} + +Check that the time profile `sub_prof` is applicable for indexing with `TimeStructurePeriod` +or `PeriodPartition` as declared by the time profile type `TP`. The check is not performed +on potential subprofiles. This can be achieved through the function [`check_sub_profs`](@ref) + +!!! danger "Usage of this function" + User should never use this function directly. It should only be included within the core + structure in `EnergyModelsBase`. +""" +function check_sub_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {TP<:Union{StrategicProfile, StrategicStochasticProfile}} + bool_op = check_not_profile(OperationalProfile, sub_prof, sub_msg) + bool_part = check_not_profile(PartitionProfile, sub_prof, sub_msg) + bool_scp = check_not_profile(ScenarioProfile, sub_prof, sub_msg) + bool_rp = check_not_profile(RepresentativeProfile, sub_prof, sub_msg) + + return bool_op * bool_part * bool_scp * bool_rp +end +function check_sub_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {TP<:RepresentativeProfile} + bool_op = check_not_profile(OperationalProfile, sub_prof, sub_msg) + bool_part = check_not_profile(PartitionProfile, sub_prof, sub_msg) + bool_scp = check_not_profile(ScenarioProfile, sub_prof, sub_msg) + + return bool_op * bool_part * bool_scp +end +function check_sub_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {TP<:ScenarioProfile} + bool_op = check_not_profile(OperationalProfile, sub_prof, sub_msg) + bool_part = check_not_profile(PartitionProfile, sub_prof, sub_msg) + + return bool_op * bool_part +end +function check_sub_profile(::Type{TP}, sub_prof::TimeProfile, sub_msg::String) where {TP<:PartitionProfile} + bool_op = check_not_profile(OperationalProfile, sub_prof, sub_msg) + return bool_op +end +function check_sub_profile(::Type{TP}, sub_prof::Number, sub_msg::String) where {TP<:PartitionProfile} + return true +end + +""" + check_sub_profs(check_type::Type{TP_C}, prof::TP, msg::String) where {TP_C<:TimeProfile, TP<:TimeProfile} + +Check that the provided `prof` allows indexing as specified through the provided +`TP_C` profile type. Each sub profile is checked if the `prof` includes sub profiles +different than `FixedProfile`. The check is based on the provided profile type `TP_C`. + +!!! danger "Usage of these functions" + User should never use these functions directly. They should only be included within the + core structure in `EnergyModelsBase`. +""" +function check_sub_profs(::Type{TP_C}, prof::TP_C, msg::String) where {TP_C<:TimeProfile} + bool_val = true + sub_msg = "in `$(TP_C)`s " * msg + for sub_prof ∈ prof.vals + bool_val *= check_sub_profile(TP_C, sub_prof, sub_msg) + !bool_val && break + end + return bool_val +end +function check_sub_profs( + ::Type{TP_C}, + prof::TP, + msg::String, +) where {TP_C<:TimeProfile, TP<:TimeProfile} + bool_val = true + sub_msg = "in `$(TP.name.name)`s " * msg + for sub_prof ∈ prof.vals + bool_val *= check_sub_profile(TP_C, sub_prof, sub_msg) + bool_val *= check_sub_profs(TP_C, sub_prof, sub_msg) + !bool_val && break + end + return bool_val +end +function check_sub_profs( + ::Type{TP_C}, + prof::TP_C, + msg::String, +) where {TP_C<:StrategicStochasticProfile} + bool_val = true + sub_msg = "in `StrategicStochasticProfile`s " * msg + for sp_array ∈ prof.vals, sub_prof ∈ sp_array + bool_val = check_sub_profile(TP_C, sub_prof, sub_msg) + !bool_val && break + end + return bool_val +end +function check_sub_profs( + ::Type{TP_C}, + prof::StrategicStochasticProfile, + msg::String, +) where {TP_C<:TimeProfile} + bool_val = true + sub_msg = "in `StrategicStochasticProfile`s " * msg + for sp_array ∈ prof.vals, sub_prof ∈ sp_array + bool_val = check_sub_profile(TP_C, sub_prof, sub_msg) + bool_val *= check_sub_profs(TP_C, sub_prof, msg) + !bool_val && break + end + return bool_val +end +function check_sub_profs( + ::Type{TP_C}, + prof::TP, + msg::String, +) where {TP_C<:TimeProfile, TP<:Union{FixedProfile, Number}} + return true +end + """ check_strategic_profile(time_profile::TimeProfile, message::String) @@ -647,48 +767,22 @@ strategic indexing. ## Checks - `TimeProfile`s accessed in `StrategicPeriod`s cannot include `OperationalProfile`, - `ScenarioProfile`, or `RepresentativeProfile` as this is not allowed through indexing - on the `TimeProfile`. + `PartitionProfile`, `ScenarioProfile`, or `RepresentativeProfile` as this is not allowed + through indexing on the `TimeProfile`. """ function check_strategic_profile(time_profile::TimeProfile, message::String) - # Check on the highest level - bool_sp = check_strat_sub_profile(time_profile, message, true) - - if isa(time_profile, StrategicProfile) - for l1_profile ∈ time_profile.vals - sub_msg = "in strategic profiles " * message - bool_sp = check_strat_sub_profile(l1_profile, sub_msg, bool_sp) - !bool_sp && break - end - elseif isa(time_profile, StrategicStochasticProfile) - for sp_array ∈ time_profile.vals, l1_profile ∈ sp_array - sub_msg = "in strategic stochastic profiles " * message - bool_sp = check_strat_sub_profile(l1_profile, sub_msg, bool_sp) - !bool_sp && break - end - end - return bool_sp -end -function check_strat_sub_profile(sub_profile::TimeProfile, sub_msg::String, bool_sp::Bool) - @assert_or_log( - !isa(sub_profile, OperationalProfile), - "Operational profiles " * sub_msg - ) - @assert_or_log(!isa(sub_profile, ScenarioProfile), "Scenario profiles " * sub_msg) - @assert_or_log( - !isa(sub_profile, RepresentativeProfile), - "Representative profiles " * sub_msg - ) + if isa(time_profile, Union{StrategicProfile, StrategicStochasticProfile}) + # Iterate through the strategic or strategic stochstic profiles, if existing + bool_sp = check_sub_profs(typeof(time_profile), time_profile, message) + else + # Check the profiles + bool_sp = check_sub_profile(StrategicProfile, time_profile, message) + end - bool_sp *= - !isa(sub_profile, OperationalProfile) && - !isa(sub_profile, ScenarioProfile) && - !isa(sub_profile, RepresentativeProfile) return bool_sp end - """ check_representative_profile(time_profile::TimeProfile, message::String) @@ -700,44 +794,23 @@ representative periods indexing. - `message` - A message that should be printed after the type of profile. ## Checks -- `TimeProfile`s accessed in `RepresentativePeriod`s cannot include `OperationalProfile` - or `ScenarioProfile` as this is not allowed through indexing on the `TimeProfile`. +- `TimeProfile`s accessed in `RepresentativePeriod`s cannot include `OperationalProfile`, + `PartitionProfile` or `ScenarioProfile` as this is not allowed through indexing on the + `TimeProfile`. """ function check_representative_profile(time_profile::TimeProfile, message::String) - # Check on the highest level - bool_rp = check_repr_sub_profile(time_profile, message, true) - - # Iterate through the strategic profiles, if existing - if isa(time_profile, StrategicProfile) - for l1_profile ∈ time_profile.vals - sub_msg_1 = "in strategic profiles " * message - bool_rp = check_repr_sub_profile(l1_profile, sub_msg_1, bool_rp) - if isa(l1_profile, RepresentativeProfile) - for l2_profile ∈ l1_profile.vals - sub_msg_2 = "in representative profiles " * sub_msg_1 - bool_rp = check_repr_sub_profile(l2_profile, sub_msg_2, bool_rp) - end - end - end - end - # Iterate through the representative profiles, if existing - if isa(time_profile, RepresentativeProfile) - for l1_profile ∈ time_profile.vals - sub_msg = "in representative profiles " * message - bool_rp = check_repr_sub_profile(l1_profile, sub_msg, bool_rp) - end - end - return bool_rp -end -function check_repr_sub_profile(sub_profile::TimeProfile, sub_msg::String, bool_rp::Bool) - @assert_or_log( - !isa(sub_profile, OperationalProfile), - "Operational profiles " * sub_msg + if isa( + time_profile, + Union{StrategicProfile, StrategicStochasticProfile, RepresentativeProfile} ) - @assert_or_log(!isa(sub_profile, ScenarioProfile), "Scenario profiles " * sub_msg) - - bool_rp *= !isa(sub_profile, OperationalProfile) && !isa(sub_profile, ScenarioProfile) + # Iterate through the strategic, strategic stochstic, or representative profiles, + # if existing + bool_rp = check_sub_profs(RepresentativeProfile, time_profile, message) + else + # Check the profiles + bool_rp = check_sub_profile(RepresentativeProfile, time_profile, message) + end return bool_rp end @@ -748,68 +821,47 @@ Function for checking that an individual `TimeProfile` does not include the wron scenario indexing. ## Checks -- `TimeProfile`s accessed in `OperationalScenario`s cannot include `OperationalProfile` as - this is not allowed through indexing on the `TimeProfile`. +- `TimeProfile`s accessed in `OperationalScenario`s cannot include `OperationalProfile` or + `PartitionProfile` as this is not allowed through indexing on the `TimeProfile`. """ function check_scenario_profile(time_profile::TimeProfile, message::String) - # Check on the highest level - bool_scp = check_osc_sub_profile(time_profile, message, true) - - # Iterate through the strategic profiles, if existing - if isa(time_profile, StrategicProfile) - for l1_profile ∈ time_profile.vals - sub_msg_1 = "in strategic profiles " * message - bool_scp = check_osc_sub_profile(l1_profile, sub_msg_1, bool_scp) - if isa(l1_profile, RepresentativeProfile) - sub_msg_2 = "in representative profiles " * sub_msg_1 - for l2_profile ∈ l1_profile.vals - bool_scp = check_osc_sub_profile(l2_profile, sub_msg_2, bool_scp) - if isa(l2_profile, ScenarioProfile) - sub_msg_3 = "in scenario profiles in " * sub_msg_2 - for l3_profile ∈ l2_profile.vals - bool_scp = check_osc_sub_profile(l3_profile, sub_msg_3, bool_scp) - end - end - end - elseif isa(l1_profile, ScenarioProfile) - for l2_profile ∈ l1_profile.vals - sub_msg_2 = "in scenario profiles " * sub_msg_1 - bool_scp = check_osc_sub_profile(l2_profile, sub_msg_2, bool_scp) - end - end - end - end - - # Iterate through the representative profiles, if existing - if isa(time_profile, RepresentativeProfile) - for l1_profile ∈ time_profile.vals - sub_msg_1 = "in representative profiles " * message - bool_scp = check_osc_sub_profile(l1_profile, sub_msg_1, bool_scp) - if isa(l1_profile, ScenarioProfile) - for l2_profile ∈ l1_profile.vals - sub_msg_2 = "in scenario profiles " * sub_msg_1 - bool_scp = check_osc_sub_profile(l2_profile, sub_msg_2, bool_scp) - end - end - end + # Check the sub structures + if isa( + time_profile, + Union{StrategicProfile, StrategicStochasticProfile, RepresentativeProfile, ScenarioProfile} + ) + bool_scp = check_sub_profs(ScenarioProfile, time_profile, message) + else + # Check the profiles + bool_scp = check_sub_profile(ScenarioProfile, time_profile, message) end - # Iterate through the scenario profiles, if existing - if isa(time_profile, ScenarioProfile) - for l1_profile ∈ time_profile.vals - sub_msg = "in scenario profiles " * message - bool_scp = check_osc_sub_profile(l1_profile, sub_msg, bool_scp) - end - end return bool_scp end -function check_osc_sub_profile(sub_profile::TimeProfile, sub_msg::String, bool_scp::Bool) - @assert_or_log( - !isa(sub_profile, OperationalProfile), - "Operational profiles " * sub_msg + +""" + check_partition_profile(time_profile::TimeProfile, message::String) + +Function for checking that an individual `TimeProfile` does not include the wrong type for +partition indexing. + +## Checks +- `TimeProfile`s accessed in `PeriodPartion`s cannot include `OperationalProfile` as this is + not allowed through indexing on the `TimeProfile`. +""" +function check_partition_profile(time_profile::TimeProfile, message::String) + # Check the sub structures + if isa( + time_profile, + Union{StrategicProfile, StrategicStochasticProfile, RepresentativeProfile, ScenarioProfile, PartitionProfile} ) - bool_scp *= !isa(sub_profile, OperationalProfile) - return bool_scp + bool_part = check_sub_profs(PartitionProfile, time_profile, message) + else + # Check the profiles + bool_part = check_sub_profile(ScenarioProfile, time_profile, message) + end + + return bool_part end """ diff --git a/test/test_checks.jl b/test/test_checks.jl index b7ef269..bc059ff 100644 --- a/test/test_checks.jl +++ b/test/test_checks.jl @@ -363,24 +363,32 @@ end # - EMB.check_profile(fieldname, value::OperationalProfile, ts::OperationalScenarios, sp) oscs = OperationalScenarios(3, day) ts = TwoLevel(2, 1, oscs) + ts_tree = TwoLevelTree(1, [2] ,oscs) for tp ∈ profiles @test_throws AssertionError create_simple_graph(ts, tp) + @test_throws AssertionError create_simple_graph(ts_tree, tp) end # Test that there is an error with wrong `OperationalProfile`s in representative periods # - EMB.check_profile(fieldname, value::OperationalProfile, ts::RepresentativePeriods, sp) - ts = TwoLevel(2, 1, RepresentativePeriods(2, [.5, .5], day)) + rps = RepresentativePeriods(2, [.5, .5], day) + ts = TwoLevel(2, 1, rps) + ts_tree = TwoLevelTree(1, [2], rps) for tp ∈ profiles @test_throws AssertionError create_simple_graph(ts, tp) + @test_throws AssertionError create_simple_graph(ts_tree, tp) end # Test that there is an error with wrong `OperationalProfile`s in operational scenarios # in representative periods # - EMB.check_profile(fieldname, value::OperationalProfile, ts::OperationalScenarios, sp) # - EMB.check_profile(fieldname, value::OperationalProfile, ts::RepresentativePeriods, sp) - ts = TwoLevel(2, 1, RepresentativePeriods(2, [.5, .5], oscs)) + rps = RepresentativePeriods(2, [.5, .5], oscs) + ts = TwoLevel(2, 1, rps) + ts_tree = TwoLevelTree(1, [2], rps) for tp ∈ profiles @test_throws AssertionError create_simple_graph(ts, tp) + @test_throws AssertionError create_simple_graph(ts_tree, tp) end # Test that there is warning when using `ScenarioProfile` without `OperationalScenarios` @@ -457,11 +465,14 @@ end # - EMB.check_strategic_profile(time_profile::TimeProfile, message::String) profiles = [ OperationalProfile([5]), + PartitionProfile([5]), ScenarioProfile([5]), RepresentativeProfile([5]), StrategicProfile([OperationalProfile([5])]), + StrategicProfile([PartitionProfile([5])]), StrategicProfile([ScenarioProfile([5])]), StrategicProfile([RepresentativeProfile([5])]), + StrategicStochasticProfile([[PartitionProfile([5])]]), StrategicStochasticProfile([[OperationalProfile([5])]]), StrategicStochasticProfile([[ScenarioProfile([5])]]), StrategicStochasticProfile([[RepresentativeProfile([5])]]), @@ -470,32 +481,72 @@ end @test_throws AssertionError EMB.check_strategic_profile(tp, "") end + # Check that valid profiles pass check_strategic_profile without error + valid_profiles = [ + FixedProfile(5), + StrategicProfile([FixedProfile(5)]), + StrategicStochasticProfile([[FixedProfile(5)]]), + ] + for tp ∈ valid_profiles + @test EMB.check_strategic_profile(tp, "") + end + + # Check that wrong profiles for representative indexable variables are identified # - EMB.check_representative_profile(time_profile::TimeProfile, message::String) profiles = [ OperationalProfile([5]), + PartitionProfile([5]), ScenarioProfile([5]), StrategicProfile([OperationalProfile([5])]), + StrategicProfile([PartitionProfile([5])]), StrategicProfile([ScenarioProfile([5])]), + StrategicStochasticProfile([[OperationalProfile([5])]]), StrategicProfile([RepresentativeProfile([OperationalProfile([5])])]), + StrategicProfile([RepresentativeProfile([PartitionProfile([5])])]), StrategicProfile([RepresentativeProfile([ScenarioProfile([5])])]), + StrategicStochasticProfile([[RepresentativeProfile([OperationalProfile([5])])]]), RepresentativeProfile([OperationalProfile([5])]), + RepresentativeProfile([PartitionProfile([5])]), RepresentativeProfile([ScenarioProfile([5])]), ] for tp ∈ profiles @test_throws AssertionError EMB.check_representative_profile(tp, "") end + # Check that valid profiles pass check_representative_profile without error + valid_profiles = [ + FixedProfile(5), + RepresentativeProfile([5]), + StrategicProfile([FixedProfile(5)]), + StrategicProfile([RepresentativeProfile([5])]), + StrategicStochasticProfile([[FixedProfile(5)]]), + StrategicStochasticProfile([[RepresentativeProfile([5])]]), + ] + for tp ∈ valid_profiles + @test EMB.check_representative_profile(tp, "") + end + + # Check that wrong profiles for scenario indexable variables are identified # - EMB.check_scenario_profile(time_profile::TimeProfile, message::String) profiles = [ OperationalProfile([5]), + PartitionProfile([5]), StrategicProfile([OperationalProfile([5])]), + StrategicProfile([PartitionProfile([5])]), + StrategicStochasticProfile([[OperationalProfile([5])]]), StrategicProfile([RepresentativeProfile([OperationalProfile([5])])]), + StrategicProfile([RepresentativeProfile([PartitionProfile([5])])]), + StrategicStochasticProfile([[RepresentativeProfile([OperationalProfile([5])])]]), StrategicProfile([RepresentativeProfile([ScenarioProfile([OperationalProfile([5])])])]), + StrategicProfile([RepresentativeProfile([ScenarioProfile([PartitionProfile([5])])])]), RepresentativeProfile([OperationalProfile([5])]), + RepresentativeProfile([PartitionProfile([5])]), RepresentativeProfile([ScenarioProfile([OperationalProfile([5])])]), + RepresentativeProfile([ScenarioProfile([PartitionProfile([5])])]), ScenarioProfile([OperationalProfile([5])]), + ScenarioProfile([PartitionProfile([5])]), ] for tp ∈ profiles @test_throws AssertionError EMB.check_scenario_profile(tp, "") @@ -512,6 +563,41 @@ end @test EMB.check_scenario_profile(tp, "") end + # Check that wrong profiles for partition indexable variables are identified + # - EMB.check_partition_profile(time_profile::TimeProfile, message::String) + profiles = [ + OperationalProfile([5]), + StrategicProfile([OperationalProfile([5])]), + StrategicStochasticProfile([[OperationalProfile([5])]]), + StrategicProfile([RepresentativeProfile([OperationalProfile([5])])]), + StrategicStochasticProfile([[RepresentativeProfile([OperationalProfile([5])])]]), + StrategicProfile([RepresentativeProfile([ScenarioProfile([OperationalProfile([5])])])]), + RepresentativeProfile([OperationalProfile([5])]), + RepresentativeProfile([ScenarioProfile([OperationalProfile([5])])]), + ScenarioProfile([OperationalProfile([5])]), + ScenarioProfile([PartitionProfile([OperationalProfile([5])])]), + PartitionProfile([OperationalProfile([5])]), + + ] + for tp ∈ profiles + @test_throws AssertionError EMB.check_partition_profile(tp, "") + end + + # Check that valid profiles pass check_partition_profile without error + valid_profiles = [ + FixedProfile(5), + PartitionProfile([5]), + StrategicProfile([FixedProfile(5)]), + StrategicProfile([PartitionProfile([5])]), + StrategicProfile([RepresentativeProfile([PartitionProfile([5])])]), + StrategicStochasticProfile([[PartitionProfile([5])]]), + StrategicStochasticProfile([[PartitionProfile([5])]]), + RepresentativeProfile([ScenarioProfile([FixedProfile(5)])]), + ] + for tp ∈ valid_profiles + @test EMB.check_partition_profile(tp, "") + end + # Reactivate logging EMB.ASSERTS_AS_LOG = true end