From 2a800a2d88b35db8047ce24d8709f8a0e35cb995 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 10 Sep 2026 10:31:02 +0200 Subject: [PATCH 01/13] Added identification of PartitionProfile --- Project.toml | 2 +- docs/src/library/internals/reset.md | 3 ++- src/structures/reset.jl | 7 +++++++ src/utils/identification.jl | 10 +++++++++- test/test_utils.jl | 11 ++++++++--- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 273b9a8..2d0574b 100644 --- a/Project.toml +++ b/Project.toml @@ -28,5 +28,5 @@ EnergyModelsBase = "0.10" EnergyModelsGeography = "0.12" JuMP = "1.24" ParametricOptInterface = "0.9" -TimeStruct = "0.9.1" +TimeStruct = "0.9.12" julia = "1.10" diff --git a/docs/src/library/internals/reset.md b/docs/src/library/internals/reset.md index 49a44c9..6972eba 100644 --- a/docs/src/library/internals/reset.md +++ b/docs/src/library/internals/reset.md @@ -14,6 +14,7 @@ They are used internally for dispatch. ```@docs EMRH.AbstractPath EMRH.OperPath +EMRH.PartitionPath EMRH.ElementPath EMRH.TimeWeightPath EMRH.AbstractInitDataPath @@ -37,8 +38,8 @@ They are used internally for dispatch. ```@docs EMRH.AbstractReset -EMRH.ElementReset EMRH.OperReset +EMRH.ElementReset EMRH.TimeWeightReset EMRH.InitReset EMRH.ResetType diff --git a/src/structures/reset.jl b/src/structures/reset.jl index fa77dd0..d039a6d 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -21,6 +21,13 @@ Internal type for paths pointing towards operational profiles. """ struct OperPath <: AbstractPath end +""" + struct PartitionPath <: AbstractPath + +Internal type for paths pointing towards partitioned profiles. +""" +struct PartitionPath <: AbstractPath end + """ struct ElementPath <: AbstractPath diff --git a/src/utils/identification.jl b/src/utils/identification.jl index 4a8a5b1..7363e0d 100644 --- a/src/utils/identification.jl +++ b/src/utils/identification.jl @@ -131,7 +131,15 @@ function _find_update_paths( all_paths::Vector{Any}, ) new_path = vcat(current_path, [OperPath()]) - push!(all_paths, new_path) # Add current_path to all_paths + push!(all_paths, new_path) +end +function _find_update_paths( + field::PartitionProfile, + current_path::Vector{Any}, + all_paths::Vector{Any}, +) + new_path = vcat(current_path, [PartitionPath()]) + push!(all_paths, new_path) end function _find_update_paths( field::StrategicProfile, diff --git a/test/test_utils.jl b/test/test_utils.jl index 1f0bb44..c9cf6c9 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -181,8 +181,8 @@ end "demand", profile, Dict( - :surplus => FixedProfile(0), - :deficit => profile, + :surplus => profile, + :deficit => PartitionProfile([1, 2, 3]), ), Dict(heat => 1) ) @@ -255,11 +255,16 @@ end # Test of a node with operational profile and symbol dictionary # - _find_update_paths(field::OperationalProfile, current_path::Vector{Any}, all_paths::Vector{Any}) + # - _find_update_paths(field::PartitionProfile, current_path::Vector{Any}, all_paths::Vector{Any}) # - _find_update_paths(field::AbstractDict, current_path::Vector{Any}, all_paths::Vector{Any}) # - _dict_key(key::Symbol) @test issetequal( EMRH._find_update_paths(sink), - [[:cap, EMRH.OperPath()], [:penalty, "[:deficit]", EMRH.OperPath()]], + [ + [:cap, EMRH.OperPath()], + [:penalty, "[:surplus]", EMRH.OperPath()], + [:penalty, "[:deficit]", EMRH.PartitionPath()] + ], ) # Test of the new node with string dictionary From 509a1bfa7b255ec9d960594cefa802e7dd8c377a Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 10 Sep 2026 10:38:53 +0200 Subject: [PATCH 02/13] Added tests for lens generation --- test/test_utils.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/test_utils.jl b/test/test_utils.jl index c9cf6c9..5df8160 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -129,8 +129,10 @@ end # Create the profiles n_op = 15 + n_part = 5 dur_op = ones(n_op) profile = OperationalProfile(rand(n_op)) + part_profile = PartitionProfile(rand(n_part)) em_data = [EmissionsProcess(Dict(co2 => profile))] struct TestInitData <: AbstractInitData end @@ -182,7 +184,7 @@ end profile, Dict( :surplus => profile, - :deficit => PartitionProfile([1, 2, 3]), + :deficit => part_profile, ), Dict(heat => 1) ) @@ -301,6 +303,11 @@ end lens(n) == profile for (field, lens) โˆˆ lens_dict[n] if isa(typeof(field[end]), EMRH.OperPath)) for n โˆˆ ๐’ฉ) + @test all( + all( + lens(n) == part_profile + for (field, lens) โˆˆ lens_dict[n] if isa(typeof(field[end]), EMRH.PartitionPath)) + for n โˆˆ ๐’ฉ) @test all( all( lens(n) == 0.5 From 89f05a95dee9f044341b70b61f4f925491f00a73 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Wed, 16 Sep 2026 11:45:19 +0200 Subject: [PATCH 03/13] Created a `PartitionReset` and included tests: - Tested on specific node - Not tested for existing nodes with it --- docs/src/library/internals/methods-EMB.md | 6 ++ docs/src/library/internals/reset.md | 39 +++++++- src/structures/reset.jl | 71 ++++++++++++++ src/utils/case_setup.jl | 49 ++++++++-- test/test_utils.jl | 109 ++++++++++++++-------- 5 files changed, 218 insertions(+), 56 deletions(-) diff --git a/docs/src/library/internals/methods-EMB.md b/docs/src/library/internals/methods-EMB.md index d43bfa7..fa2b0ac 100644 --- a/docs/src/library/internals/methods-EMB.md +++ b/docs/src/library/internals/methods-EMB.md @@ -38,3 +38,9 @@ EnergyModelsBase.get_elements_vec EnergyModelsBase.get_nodes EnergyModelsBase.get_links ``` + +In addition, a new method is created for `get_time_struct` to extract the original time structure from the `UpdateCase`: + +```@docs +EnergyModelsBase.get_time_struct +``` diff --git a/docs/src/library/internals/reset.md b/docs/src/library/internals/reset.md index 6972eba..6b5fa68 100644 --- a/docs/src/library/internals/reset.md +++ b/docs/src/library/internals/reset.md @@ -8,7 +8,7 @@ Pages = ["reset.md"] ## [Paths](@id lib-int-reset-path) -Path are internal types that are used for the identification of a reset. +Paths are internal types that are used for the identification of a reset. They are used internally for dispatch. ```@docs @@ -36,9 +36,15 @@ EMRH.model_key(idp::EMRH.InitDataPath) Reset types are introduced to differentiate on what must be reset in a given element. They are used internally for dispatch. +!!! note "Resetting new values" + If you **must** reset a different type of value than outlined below, you must also create a new type. + + In this case, you **must** also create a new method for [`ResetType`](@ref EMRH.ResetType) as this function is used internally. + ```@docs EMRH.AbstractReset EMRH.OperReset +EMRH.PartitionReset EMRH.ElementReset EMRH.TimeWeightReset EMRH.InitReset @@ -51,11 +57,34 @@ The following function is included for identification purposes: EMRH.is_init_reset ``` -!!! note "Resetting new values" - If you **must** reset a different type of value than outlined below, you must also create a new type. - If you create a new `AbstractPath`, it is not neccesary to create a new type. +The following function is included to identify the individual partitions when creating a `PartitionReset`. - In this case, you **must** also create a new method for [`ResetType`](@ref EMRH.ResetType) as this function is used internally. +```@docs +EMRH.partition_periods +``` + +!!! warning "Reset of partition periods" + The introduction of `PartitionProfile`s requires the user to create a new method for the function `partition_periods`. + This function should extract the `PartitionProfile` in which the durations of the individual partitions are specified. + It is **not** possible to specify a `FixedProfile` due to the way the horizons are calculated. + + Using partitions is experimental. + You must be careful that the structure of the chosen partitions are in line with the horizon as outlined below: + + ```julia + # Time structure and chosen horizon + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(15, 1)) + โ„‹ = PeriodHorizons(15, 1, 4, 2) + + # Consistent profile (the last partition can only be of duration 1) + pprof = PartitionProfile([2, 2, 2, 2, 2, 2, 2, 1]) + + # Inconsistent profile + pprof = PartitionProfile([3, 3, 3, 3, 3]) + ``` + + The profile is inconsistent for the receding horizon framework as the individual partitions are not fully included in both the implementation and optimization horizon. + It would be however consistent for a standard perfect foresight model. ## [Substitution types](@id lib-int-reset-sub) diff --git a/src/structures/reset.jl b/src/structures/reset.jl index d039a6d..69f3a75 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -135,6 +135,44 @@ mutable struct OperReset <: AbstractReset end end +""" + mutable struct PartitionReset <: AbstractReset + +[`AbstractReset`](@ref) for resetting partition profiles within an element. The inner +constructor is utilized for automatically creating the lens to the field path. + +# Inner constructor arguments +- **`field_path::Vector`** is the path towards the field as identified through the function + [`_find_update_paths`](@ref). +- **`x`** is the instance of a type for which the reset type is created. + +# Fields +- **`lens::Union{PropertyLens,ComposedFunction}`** is the lens for resetting the field. +- **`var`** is the variable when using `ParametricOptInterface`. +- **`pps::PartitionProfile`** is the partition profile corresponding to the duration of the + individual partition periods. +- **`val::PartitionProfile`** is the complete partition profile from the original + element. The individual values are extracted from this profile in the receding horizon + framework. + +!!! warning "PartitionReset" + The `PartitionReset` type requires the user to declare a new method `partition_periods` + for the type `x` which utilizes the concept of partitions. An error is provided if no + method is declared. +""" +mutable struct PartitionReset <: AbstractReset + lens::Union{PropertyLens,ComposedFunction} + var + val::PartitionProfile + pps::PartitionProfile + function PartitionReset(field_path::Vector, x) + lens = _create_lens_for_field(field_path) + val = lens(x) + pps = partition_periods(x) + new(lens, nothing, val, pps) + end +end + """ mutable struct TimeWeightReset <: AbstractReset @@ -195,6 +233,7 @@ end """ ResetType(field_path::Vector, _::OperPath, x) + ResetType(field_path::Vector, _::PartitionPath, x) ResetType(field_path::Vector, _::ElementPath, x) ResetType(field_path::Vector, _::TimeWeightPath, x) ResetType(field_path::Vector, path::AbstractInitDataPath, x) @@ -208,6 +247,7 @@ Constructor for [`AbstractReset`](@ref) types depending on their specified [`Abs This is **not** necessary for a new subtype of [`AbstractInitDataPath`](@ref). """ ResetType(field_path::Vector, _::OperPath, x) = OperReset(field_path, x) +ResetType(field_path::Vector, _::PartitionPath, x) = PartitionReset(field_path, x) ResetType(field_path::Vector, _::ElementPath, x) = ElementReset(field_path, x) ResetType(field_path::Vector, _::TimeWeightPath, x) = TimeWeightReset(field_path, x) ResetType(field_path::Vector, path::AbstractInitDataPath, x) = @@ -221,6 +261,26 @@ Function fo identifying whether the AbstractReset `rt` is of type `InitReset`. is_init_reset(rt::AbstractReset) = false is_init_reset(rt::InitReset) = true +""" + partition_periods(x) + partition_periods(res_type::PartitionReset, ๐’ฏ::TS.TimeStructure) + +""" +function partition_periods(x) + x_type = typeof(x) + throw( + ErrorException( + "No method for `partition_periods` is defined for $(x_type).\n" * + "This error is caused by including a `PartitionProfile` in $(x_type) which " * + "requires a method for the `EnergyModelsRecedingHorizon` function " * + "`partition_periods(x)` to be defined." + ) + ) +end +function partition_periods(res_type::PartitionReset, ๐’ฏ::TS.TimeStructure) + return partition_duration(๐’ฏ, res_type.pps) +end + """ abstract type AbstractSub @@ -372,6 +432,7 @@ type introduced in `EnergyModelsBase` in which the individual vectors of `Resour `AbstractElement`s are replaced with the corresponding vectors of [`AbstractSub`](@ref). # Fields +- **`T::TS.TimeStructure`** is the time structure of the original problem. - **`model::ModelSub`** is the substitution type for the [`RecHorEnergyModel`](@ref). - **`map_org::Dict`** is a dictionary for mapping the types of the receding horizon problem to the types of the full problem. @@ -383,6 +444,7 @@ type introduced in `EnergyModelsBase` in which the individual vectors of `Resour the individual [`AbstractElement`](@extref EnergyModelsBase.AbstractElement)s. """ mutable struct UpdateCase <: AbstractCase + T::TS.TimeStructure model::ModelSub map_org::Dict map_updated::Dict @@ -437,6 +499,14 @@ get_sub_ele(๐’ฎแต›แต‰แถœ::Vector{Vector}, x::Type{<:AbstractElement}) = filter(๐’ฎ -> typeof(๐’ฎ) == Vector{_ele_to_sub(x)}, ๐’ฎแต›แต‰แถœ)[1] get_sub_ele(๐’ฐ::UpdateCase, x::Type{<:AbstractElement}) = get_sub_ele(get_sub_elements_vec(๐’ฐ), x) +""" + EMB.get_time_struct(๐’ฐ::UpdateCase) + +Method for the `EnergyModelsBase` function to extract the original time structure of the +update case +""" +EMB.get_time_struct(๐’ฐ::UpdateCase) = ๐’ฐ.T + """ EMB.get_products(๐’ฐ::UpdateCase) @@ -476,6 +546,7 @@ This element vector can be directly utilized for the field elements of a [`Case`](@extref EnergyModelsBase.Case). """ EMB.get_nodes(๐’ฐ::UpdateCase) = EMB.Node[๐’ฎ.new for ๐’ฎ โˆˆ get_sub_ele(๐’ฐ, EMB.Node)] + """ EMB.get_links(๐’ฐ::UpdateCase) diff --git a/src/utils/case_setup.jl b/src/utils/case_setup.jl index e22c658..8e768af 100644 --- a/src/utils/case_setup.jl +++ b/src/utils/case_setup.jl @@ -55,6 +55,7 @@ end _reset_field(x_rh, res_type::ElementReset, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) _reset_field(x_rh, res_type::Union{InitReset, TimeWeightReset}, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) _reset_field(x_rh, res_type::OperReset, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) + _reset_field(x_rh, res_type::PartitionReset, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) Resets the field expressed through `res_type` of element `x_rh` with the new value. The type of the new value is depending on the specified `res_type`: @@ -63,6 +64,8 @@ of the new value is depending on the specified `res_type`: 2. `res_type::Union{InitReset, TimeWeightReset}` uses the value in `res_type` directly, 3. `res_type::OperReset` creates a new operational profile based on the original operational profile in `res_type` and the set of operational periods `opers`. +4. `res_type::PartitionReset` creates a new partition profile based on the original + partition profile in `res_type` and the set of operational periods `opers`. """ function _reset_field( x_rh, @@ -91,10 +94,29 @@ function _reset_field( @reset res_type.lens(x_rh) = OperationalProfile(res_type.val[opers]) return x_rh end +function _reset_field( + x_rh, + res_type::PartitionReset, + ๐’ฐ::UpdateCase, + opers::Vector{<:TS.TimePeriod}, +) + # Extract the required variables from the UpdateCase + ๐’ฏ = get_time_struct(๐’ฐ) + + # Identify the partitions of the original problem that are used within the current + # receding horizon problem + ๐’ฏแต–แตˆ = partition_periods(res_type, ๐’ฏ) + parts = filter(t_pd -> isempty(setdiff(t_pd, opers)), ๐’ฏแต–แตˆ) + + # Reset the partition profile of the receding horizon problem based on the relevant + # partitions + @reset res_type.lens(x_rh) = PartitionProfile(res_type.val[parts]) + return x_rh +end """ - _create_updatetype(modeltype::RecHorEnergyModel) - _create_updatetype(modeltype::RecHorEnergyModel, case::AbstractCase) + _create_updatetype(case::AbstractCase, modeltype::RecHorEnergyModel) + _create_updatetype(๐’ฏ::TS.TimeStructure, modeltype::RecHorEnergyModel) Initialize an [`UpdateCase`](@ref) based on the provided [`RecHorEnergyModel`](@ref) `modeltype`. @@ -102,22 +124,29 @@ Initialize an [`UpdateCase`](@ref) based on the provided [`RecHorEnergyModel`](@ Initialize and populate the [`UpdateCase`](@ref) if the function has as first argument an [`AbstractCase`](@extref EnergyModelsBase.AbstractCase). """ -function _create_updatetype(modeltype::RecHorEnergyModel) - paths_model = _find_update_paths(modeltype) - reset_model = AbstractReset[ResetType(field_id, field_id[end], modeltype) for field_id โˆˆ paths_model] - ๐’ฐ = UpdateCase(Substitution(modeltype, reset_model), Dict(), Dict(), ProductSub[], Vector[]) - _init_mapping!(๐’ฐ, modeltype) - return ๐’ฐ -end function _create_updatetype(case::AbstractCase, modeltype::RecHorEnergyModel) # Create the `UpdateCase` based on the original `Case` structure - ๐’ฐ = _create_updatetype(modeltype) + ๐’ฐ = _create_updatetype(get_time_struct(case), modeltype) _add_elements!(๐’ฐ, get_products(case)) for ๐’ณ โˆˆ get_elements_vec(case) _add_elements!(๐’ฐ, ๐’ณ) end return ๐’ฐ end +function _create_updatetype(๐’ฏ::TS.TimeStructure, modeltype::RecHorEnergyModel) + paths_model = _find_update_paths(modeltype) + reset_model = AbstractReset[ResetType(field_id, field_id[end], modeltype) for field_id โˆˆ paths_model] + ๐’ฐ = UpdateCase( + ๐’ฏ, + Substitution(modeltype, reset_model), + Dict(), + Dict(), + ProductSub[], + Vector[], + ) + _init_mapping!(๐’ฐ, modeltype) + return ๐’ฐ +end """ _add_elements!(๐’ฐ::UpdateCase, ๐’ซ::Vector{T}) where {T<:Resource} diff --git a/test/test_utils.jl b/test/test_utils.jl index 5df8160..54f12ee 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -51,11 +51,7 @@ optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) hor_test = first(โ„‹) - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ซ) - for ๐’ณ โˆˆ get_elements_vec(case) - EMRH._add_elements!(๐’ฐ, ๐’ณ) - end + ๐’ฐ = EMRH._create_updatetype(case, modeltype) ๐’ฏแตฃโ‚• = TwoLevel(1, 1, SimpleTimes(durations(hor_test))) opers_opt = collect(๐’ฏ)[indices_optimization(hor_test)] EMRH._update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) @@ -125,11 +121,11 @@ end el = ResourceCarrier("el", 0.2) heat = ResourceCarrier("heat", 0.0) co2 = ResourceEmit("co2", 1.0) - resources = [el, heat, co2] + ๐’ซ = [el, heat, co2] # Create the profiles n_op = 15 - n_part = 5 + n_part = 8 dur_op = ones(n_op) profile = OperationalProfile(rand(n_op)) part_profile = PartitionProfile(rand(n_part)) @@ -138,7 +134,7 @@ end struct TestInitData <: AbstractInitData end # Create the individual nodes - av = GenAvailability("Availability", resources) + av = GenAvailability("Availability", ๐’ซ) source_initdata = RefSource( "source", FixedProfile(1e12), @@ -316,6 +312,11 @@ end end @testset "Reset functionality" begin + # Test that the error throwing functionality is working + @test_throws ErrorException EMRH.partition_periods(sink) + pps = PartitionProfile(vcat(fill(2,7), [1])) + EMRH.partition_periods(n::Sink) = pps + # Create an operational modeltype and the time structure modeltype = RecHorOperationalModel( Dict(co2 => FixedProfile(100)), @@ -323,20 +324,18 @@ end co2, ) - # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - # Create all time related parameters ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) ๐’ฝ = first(โ„‹) ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) - ind_impl = indices_implementation(๐’ฝ) - opers_opt = opers[indices_optimization(๐’ฝ)] - opers_impl = opers[ind_impl] - opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, pps) + + # Create the update type + โ„’ = Link[] + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, โ„’], [[get_nodes, get_links]], Dict(:horizons => โ„‹)) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) # Test that the individual reset functions are working for all types # All functions are located within the file src/structures/reset.jl @@ -355,6 +354,17 @@ end @test reset_src.val == opex_var(source_oper) @test isnothing(reset_src.var) + # Test the sink resets (PartitionReset) + reset_snk = EMRH.resets(๐’ฎแต›แต‰แถœ[1][5])[2] + @test isa(reset_snk, EMRH.PartitionReset) + @test !EMRH.is_init_reset(reset_snk) + @test reset_snk.lens(sink) == deficit_penalty(sink) + @test reset_snk.val == deficit_penalty(sink) + @test isnothing(reset_snk.var) + @test all( + [reset_snk.pps.vals[k] == EMRH.partition_periods(sink).vals[k] for k โˆˆ 1:n_part] + ) + # Test the storages resets (InitReset) reset_storage = EMRH.resets(๐’ฎแต›แต‰แถœ[1][4])[3] @test isa(reset_storage, EMRH.InitReset{EMRH.InitDataPath}) @@ -364,6 +374,14 @@ end @test isnothing(reset_storage.var) @test reset_storage.path == EMRH.InitDataPath(:stor_level) + # Create all time related parameters for the first horizon + ๐’ฝ = first(โ„‹) + ind_impl = indices_implementation(๐’ฝ) + opers_opt = opers[indices_optimization(๐’ฝ)] + opers_impl = opers[ind_impl] + opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + parts_opt = filter(t_pd -> isempty(setdiff(t_pd, opers_opt)), ๐’ฏแต–แตˆ) + # Test that the reset functionality is working # - _update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) # - _update_case_types! @@ -384,7 +402,11 @@ end # Test the individual resets @test all(opex_var(๐’ฉสณ[k]).vals == opex_var(๐’ฉ[k])[opers_opt] for k โˆˆ [2,3]) @test data_init(๐’ฉสณ[4]).init_val_dict[:stor_level] == 5.0 - @test deficit_penalty(๐’ฉสณ[5]).vals == deficit_penalty(๐’ฉ[5])[opers_opt] + @test surplus_penalty(๐’ฉสณ[5]).vals == surplus_penalty(๐’ฉ[5])[opers_opt] + @test deficit_penalty(๐’ฉสณ[5]).vals == deficit_penalty(๐’ฉ[5])[parts_opt] + + # Delete the created method + Base.delete_method(@which EMRH.partition_periods(sink)) end end @@ -393,6 +415,7 @@ end # Create the individual resources co2 = ResourceEmit("co2", 1.0) el = ResourceCarrier("el", 0.2) + ๐’ซ = [co2, el] # Create the profile n_op = 15 @@ -473,21 +496,14 @@ end co2, ) - # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, โ„’) - # Create all time related parameters ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) - ๐’ฝ = first(โ„‹) - ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) - ind_impl = indices_implementation(๐’ฝ) - opers_opt = opers[indices_optimization(๐’ฝ)] - opers_impl = opers[ind_impl] - opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + + # Create the update type + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, โ„’], [[get_nodes, get_links]], Dict(:horizons => โ„‹)) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) # Test that the individual reset functions are working for all types # All functions are located within the file src/structures/reset.jl @@ -505,6 +521,14 @@ end @test reset_link[2].lens(link) == sink @test reset_link[2].val == sink + # Create all time related parameters for the first horizon + ๐’ฝ = first(โ„‹) + ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) + ind_impl = indices_implementation(๐’ฝ) + opers_opt = opers[indices_optimization(๐’ฝ)] + opers_impl = opers[ind_impl] + opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + # Test that the reset functionality is working # - _update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) # - _update_case_types! @@ -579,13 +603,20 @@ end end @testset "Reset functionality" begin - # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - # Create all time related parameters ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) + + # Create the update type + ๐’ฐ = EMRH._create_updatetype(๐’ฏ, modeltype) + + # Test that the individual reset functions are working for all types + # All functions are located within the file src/structures/reset.jl + sแต = EMRH.get_sub_model(๐’ฐ) + @test isa(EMRH.resets(sแต)[1], EMRH.OperReset) + + # Create all time related parameters for the first horizon ๐’ฝ = first(โ„‹) ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) ind_impl = indices_implementation(๐’ฝ) @@ -593,11 +624,6 @@ end opers_impl = opers[ind_impl] opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] - # Test that the individual reset functions are working for all types - # All functions are located within the file src/structures/reset.jl - sแต = EMRH.get_sub_model(๐’ฐ) - @test isa(EMRH.resets(sแต)[1], EMRH.OperReset) - # Test that the reset functionality is working # - _update_case_types! # - reset_field @@ -701,15 +727,16 @@ end co2, ) - # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, ๐’ฑ) - # Create all time related parameters ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) + + # Create the update type + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, ๐’ฑ], [[get_nodes, get_future_value]], Dict(:horizons => โ„‹)) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) + + # Create all time related parameters ๐’ฝ = first(โ„‹) ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) ind_impl = indices_implementation(๐’ฝ) From 4af2a671985c2880f5632f56e06b7758a85e0e90 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Wed, 16 Sep 2026 16:05:00 +0200 Subject: [PATCH 04/13] Implemented PartitionProfile for POIExt - Required significant rework in structure - New arguments in the function calls --- ext/POIExt/POIExt.jl | 2 +- ext/POIExt/utils/case_setup.jl | 86 +++++++++++++++++++++----- src/structures/reset.jl | 14 ++++- src/utils/case_setup.jl | 11 ++++ test/test_poi.jl | 109 ++++++++++++++++++--------------- test/test_utils.jl | 62 ++++++++++++++++--- 6 files changed, 209 insertions(+), 75 deletions(-) diff --git a/ext/POIExt/POIExt.jl b/ext/POIExt/POIExt.jl index 643ad6b..dc68b8b 100644 --- a/ext/POIExt/POIExt.jl +++ b/ext/POIExt/POIExt.jl @@ -3,7 +3,7 @@ module POIExt using EnergyModelsBase using EnergyModelsRecedingHorizon using .EnergyModelsRecedingHorizon: RecHorEnergyModel, UpdateCase, AbstractSub, -ElementReset, InitReset, TimeWeightReset, OperReset, +ElementReset, InitReset, TimeWeightReset, OperReset, PartitionReset, EmptyReset, _create_updatetype, _add_elements!, update_results!, update_init_data!, _update_case_types!, update_future_value!, get_sub_model, get_sub_products, get_sub_elements_vec, get_sub_ele, updated, original, resets, _initialize_loop_variables, _add_mapping! diff --git a/ext/POIExt/utils/case_setup.jl b/ext/POIExt/utils/case_setup.jl index ec8c9f6..fb7d7de 100644 --- a/ext/POIExt/utils/case_setup.jl +++ b/ext/POIExt/utils/case_setup.jl @@ -26,10 +26,10 @@ In addition, the [`UpdateCase`](@ref) `๐’ฐ` is updated with the new mapping bet periods of the optimization (through `๐’ฏแตฃโ‚•`) and the original (through `opers`) problem. """ function update_model!(m, ๐’ฐ, opers, ๐’ฏแตฃโ‚•) - _update_parameter_values!(m, get_sub_model(๐’ฐ), opers) - _update_parameter_values!(m, get_sub_products(๐’ฐ), opers) + _update_parameter_values!(m, ๐’ฐ, get_sub_model(๐’ฐ), opers) + _update_parameter_values!(m, ๐’ฐ, get_sub_products(๐’ฐ), opers) for ๐’ฎ โˆˆ get_sub_elements_vec(๐’ฐ) - _update_parameter_values!(m, ๐’ฎ, opers) + _update_parameter_values!(m, ๐’ฐ, ๐’ฎ, opers) end ๐’ฐ.map_org[:periods] = Dict(zip(๐’ฏแตฃโ‚•, opers)) ๐’ฐ.map_updated[:periods] = Dict(zip(opers, ๐’ฏแตฃโ‚•)) @@ -75,6 +75,7 @@ end EMRH._reset_field(m, x_rh, res_type::ElementReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) EMRH._reset_field(m, x_rh, res_type::Union{InitReset,TimeWeightReset}, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) EMRH._reset_field(m, x_rh, res_type::OperReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) + EMRH._reset_field(m, x_rh, res_type::PartitionReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) Resets the field expressed through `res_type` of element `x_rh` with the new value. The type of the new value is depending on the specified `res_type`: @@ -85,6 +86,10 @@ of the new value is depending on the specified `res_type`: and resets the field with it, 3. `res_type::OperReset` creates multiple MOI parameters and a new operational profile based on the original operational profile, and resets the field with it. +4. `res_type::PartitionReset` creates multiple MOI parameters and a new partition profile + based on the original partition profile, and resets the field with it. +5. `restype::EmptyReset` does not reset any field or introduce variables. It is used to + avoid problems with partition profile resetting. """ function EMRH._reset_field( m, @@ -120,10 +125,37 @@ function EMRH._reset_field( @reset res_type.lens(x_rh) = OperationalProfile([res_type.var[t] for t โˆˆ ๐’ฏแดฟแดด]) return x_rh end +function EMRH._reset_field( + m, + x_rh, + res_type::PartitionReset, + ๐’ฐ::UpdateCase, + ๐’ฏแดฟแดด::TimeStructure, +) + # Identify the partitions of the original problem that are used within the current + # receding horizon ๐’ฏแดฟแดด + ๐’ฏแต–แตˆ = EMRH.partition_periods(res_type, ๐’ฏแดฟแดด) + + # Reset the partition profile of the receding horizon problem based on the relevant + # partitions + val_par = PartitionProfile(MOI.Parameter.(res_type.val[๐’ฏแต–แตˆ])) + res_type.var = @variable(m, [๐’ฏแต–แตˆ] โˆˆ val_par[collect(๐’ฏแต–แตˆ)]) + @reset res_type.lens(x_rh) = PartitionProfile([res_type.var[t_pd] for t_pd โˆˆ ๐’ฏแต–แตˆ]) + return x_rh +end +function EMRH._reset_field( + m, + x_rh, + res_type::EmptyReset, + ๐’ฐ::UpdateCase, + ๐’ฏแดฟแดด::TimeStructure, +) + return x_rh +end """ - _update_parameter_values!(m, ๐’ฎ::Vector{<:AbstractSub}, opers::Vector{<:TS.TimePeriod}) - _update_parameter_values!(m, s:::AbstractSub, opers::Vector{<:TS.TimePeriod}) + _update_parameter_values!(m, ๐’ฐ::UpdateCase, ๐’ฎ::Vector{<:AbstractSub}, opers::Vector{<:TS.TimePeriod}) + _update_parameter_values!(m, ๐’ฐ::UpdateCase, s::AbstractSub, opers::Vector{<:TS.TimePeriod}) Updates the parameters from `m` with the values within the `Vector{<:AbstractSub}` or `AbstractSub` indexed by `opers`. @@ -133,28 +165,32 @@ new value. """ function _update_parameter_values!( m, + ๐’ฐ::UpdateCase, ๐’ฎ::EMRH.Vector{<:AbstractSub}, opers::Vector{<:TS.TimePeriod}, ) for s โˆˆ ๐’ฎ - _update_parameter_values!(m, s, opers) + _update_parameter_values!(m, ๐’ฐ, s, opers) end end function _update_parameter_values!( m, + ๐’ฐ::UpdateCase, s::AbstractSub, opers::Vector{<:TS.TimePeriod}, ) for res_type โˆˆ s.resets - _update_parameter!(m, res_type, opers) + _update_parameter!(m, ๐’ฐ, res_type, opers) end end """ - _update_parameter!(m, res_type::ElementReset, opers::Vector) - _update_parameter!(m, res_type::OperReset, opers::Vector) - _update_parameter!(m, res_type::InitReset{EMRH.InitDataPath}, opers::Vector) - _update_parameter!(m, res_type::TimeWeightReset, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::ElementReset, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::OperReset, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::InitReset{EMRH.InitDataPath}, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::TimeWeightReset, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::EmptyReset, opers::Vector) Set the parameter value in `m` for a given `res_type`: @@ -164,15 +200,35 @@ Set the parameter value in `m` for a given `res_type`: 4. `res_type::OperReset` creates a new operational profile based on the original operational profile and the set of operational periods in `opers`, updating each parameter with it. +5. `res_type::PartitionReset` creates a new partition profile based on the original + partition profile and the set of partition periods, identified through `opers`, updating + each parameter with it. +6. `res_type::EmptyReset` results in no update, """ -_update_parameter!(m, res_type::ElementReset, opers::Vector) = nothing -_update_parameter!(m, res_type::InitReset{EMRH.InitDataPath}, opers::Vector) = +_update_parameter!(m, ๐’ฐ::UpdateCase, res_type::ElementReset, opers::Vector) = nothing +_update_parameter!(m, ๐’ฐ::UpdateCase, res_type::InitReset{EMRH.InitDataPath}, opers::Vector) = MOI.set(m, POI.ParameterValue(), res_type.var, res_type.val) -_update_parameter!(m, res_type::TimeWeightReset, opers::Vector) = +_update_parameter!(m, ๐’ฐ::UpdateCase, res_type::TimeWeightReset, opers::Vector) = MOI.set(m, POI.ParameterValue(), res_type.var, res_type.val) -function _update_parameter!(m, res_type::OperReset, opers::Vector) +function _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::OperReset, opers::Vector) val = res_type.val[opers] for (i, var) โˆˆ enumerate(res_type.var) MOI.set(m, POI.ParameterValue(), var, val[i]) end end +function _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset, opers::Vector) + # Extract the required variables from the UpdateCase + ๐’ฏ = get_time_struct(๐’ฐ) + + # Identify the partitions of the original problem that are used within the current + # receding horizon problem + ๐’ฏแต–แตˆ = EMRH.partition_periods(res_type, ๐’ฏ) + parts = filter(t_pd -> isempty(setdiff(t_pd, opers)), ๐’ฏแต–แตˆ) + + # Update the parameters + val = res_type.val[parts] + for (i, var) โˆˆ enumerate(res_type.var) + MOI.set(m, POI.ParameterValue(), var, val[i]) + end +end +_update_parameter!(m, ๐’ฐ::UpdateCase, res_type::EmptyReset, opers::Vector) = nothing diff --git a/src/structures/reset.jl b/src/structures/reset.jl index 69f3a75..73a6e0d 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -80,6 +80,14 @@ related to the chosen [`AbstractPath`](@ref) as outlined above. """ abstract type AbstractReset end + +""" + struct EmptyReset <: AbstractReset + +Empty reset type, introduced to avoid problems with partition profile resetting. +""" +struct EmptyReset <: AbstractReset end + """ mutable struct ElementReset <: AbstractReset @@ -169,7 +177,11 @@ mutable struct PartitionReset <: AbstractReset lens = _create_lens_for_field(field_path) val = lens(x) pps = partition_periods(x) - new(lens, nothing, val, pps) + if val โ‰  pps + new(lens, nothing, val, pps) + else + EmptyReset() + end end end diff --git a/src/utils/case_setup.jl b/src/utils/case_setup.jl index 8e768af..65c6ce4 100644 --- a/src/utils/case_setup.jl +++ b/src/utils/case_setup.jl @@ -56,6 +56,7 @@ end _reset_field(x_rh, res_type::Union{InitReset, TimeWeightReset}, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) _reset_field(x_rh, res_type::OperReset, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) _reset_field(x_rh, res_type::PartitionReset, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) + _reset_field(x_rh, res_type::EmptyReset, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) Resets the field expressed through `res_type` of element `x_rh` with the new value. The type of the new value is depending on the specified `res_type`: @@ -66,6 +67,8 @@ of the new value is depending on the specified `res_type`: operational profile in `res_type` and the set of operational periods `opers`. 4. `res_type::PartitionReset` creates a new partition profile based on the original partition profile in `res_type` and the set of operational periods `opers`. +5. `restype::EmptyReset` does not reset any field and is used to avoid problems with + partition profile resetting. """ function _reset_field( x_rh, @@ -113,6 +116,14 @@ function _reset_field( @reset res_type.lens(x_rh) = PartitionProfile(res_type.val[parts]) return x_rh end +function _reset_field( + x_rh, + res_type::EmptyReset, + ๐’ฐ::UpdateCase, + opers::Vector{<:TS.TimePeriod}, +) + return x_rh +end """ _create_updatetype(case::AbstractCase, modeltype::RecHorEnergyModel) diff --git a/test/test_poi.jl b/test/test_poi.jl index c9dde32..540ac85 100644 --- a/test/test_poi.jl +++ b/test/test_poi.jl @@ -4,27 +4,38 @@ struct CapDirect <: Link from::EMB.Node to::EMB.Node capacity::TimeProfile + part_dur::PartitionProfile + part_mult::PartitionProfile end + +# Add methods to required functions +EMB.capacity(l::CapDirect) = l.capacity +EMB.capacity(l::CapDirect, t) = l.capacity[t] +EMB.has_capacity(l::CapDirect) = true +EMRH.partition_periods(l::CapDirect) = l.part_dur + function EMB.create_link(m, l::CapDirect, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) + # Declaration of the required subsets + ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, EMRH.partition_periods(l)) + # Generic link in which each output corresponds to the input @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ EMB.link_res(l)], m[:link_out][l, t, p] == m[:link_in][l, t, p] ) # Capacity constraint - @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ EMB.link_res(l)], - m[:link_out][l, t, p] โ‰ค m[:link_cap_inst][l, t] + @constraint(m, [t_pd โˆˆ ๐’ฏแต–แตˆ, t โˆˆ t_pd, p โˆˆ EMB.link_res(l)], + m[:link_out][l, t, p] โ‰ค m[:link_cap_inst][l, t] * l.part_mult[t_pd] ) constraints_capacity_installed(m, l, ๐’ฏ, modeltype) end -EMB.capacity(l::CapDirect) = l.capacity -EMB.capacity(l::CapDirect, t) = l.capacity[t] -EMB.has_capacity(l::CapDirect) = true # Introduction of different profiles price_profile = [10, 10, 10, 10, 1000, 1000, 1000, 1000] cap_profile = [20, 30, 40, 30, 10, 50, 35, 20] +part_profile = [2, 2, 2, 2] +mult_profile = [2, 1, 1.5, 1] demand_profile = [20, 15, 20, 15, 10, 10, 20, 20] em_co2 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8] @@ -61,7 +72,7 @@ function create_poi_case(; Data[EmissionsProcess(Dict(co2 => OperationalProfile(em_co2)))]), RefStorage{RecedingAccumulating}( "electricity storage", - StorCapOpexVar(FixedProfile(20), FixedProfile(10)), + StorCapOpexVar(FixedProfile(30), FixedProfile(10)), StorCapOpexFixed(FixedProfile(150), FixedProfile(0)), power, Dict(power => 1), @@ -78,7 +89,14 @@ function create_poi_case(; #connect the nodes with links โ„’ = [ - CapDirect("source-storage", ๐’ฉ[1], ๐’ฉ[2], OperationalProfile(cap_profile)), + CapDirect( + "source-storage", + ๐’ฉ[1], + ๐’ฉ[2], + OperationalProfile(cap_profile), + PartitionProfile(part_profile), + PartitionProfile(mult_profile), + ), Direct("source-demand", ๐’ฉ[1], ๐’ฉ[3], Linear()), Direct("storage-demand", ๐’ฉ[2], ๐’ฉ[3], Linear()), ] @@ -102,11 +120,7 @@ end ๐’ฝโ‚€ = first(โ„‹) # Create the lenses - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ซ) - for ๐’ณ โˆˆ ๐’ณแต›แต‰แถœ - EMRH._add_elements!(๐’ฐ, ๐’ณ) - end + ๐’ฐ = EMRH._create_updatetype(case, modeltype) ๐’ฎแต›แต‰แถœ = EMRH.get_sub_elements_vec(๐’ฐ) # Test that the UpdateCase is correctly created with all types @@ -129,29 +143,29 @@ end modelแตฃโ‚• = EMRH.updated(EMRH.get_sub_model(๐’ฐ)) # Test that no variables are created for models - # 4*4 for operational profiles and 1 for initial data - @test length(all_variables(m)) == 17 + # 4*4 for operational profiles, 2 for partition profiles, and 1 for initial data + @test length(all_variables(m)) == 19 # Extract the data from the receding horizon model - ๐’ฉแตฃโ‚• = get_nodes(caseแตฃโ‚•) - โ„’แตฃโ‚• = get_links(caseแตฃโ‚•) - source = ๐’ฉแตฃโ‚•[1] - stor = ๐’ฉแตฃโ‚•[2] - sink = ๐’ฉแตฃโ‚•[3] + src, stor, snk = get_nodes(caseแตฃโ‚•) + cap_link = get_links(caseแตฃโ‚•)[1] co2 = get_products(caseแตฃโ‚•)[2] - cap_link = โ„’แตฃโ‚•[1] # Test that all references are replaced correctly with the variables - @test isa(opex_var(source), OperationalProfile{VariableRef}) - @test length(opex_var(source).vals) == length(๐’ฝโ‚€) - @test isa(process_emissions(node_data(source)[1], co2), OperationalProfile{VariableRef}) - @test length(process_emissions(node_data(source)[1], co2).vals) == length(๐’ฝโ‚€) + @test isa(opex_var(src), OperationalProfile{VariableRef}) + @test length(opex_var(src).vals) == length(๐’ฝโ‚€) + @test isa(process_emissions(node_data(src)[1], co2), OperationalProfile{VariableRef}) + @test length(process_emissions(node_data(src)[1], co2).vals) == length(๐’ฝโ‚€) @test isa(node_data(stor)[1].init_val_dict[:stor_level], AffExpr) @test length(node_data(stor)[1].init_val_dict) == 1 - @test isa(capacity(sink), OperationalProfile{VariableRef}) - @test length(capacity(sink).vals) == length(๐’ฝโ‚€) + @test isa(capacity(snk), OperationalProfile{VariableRef}) + @test length(capacity(snk).vals) == length(๐’ฝโ‚€) @test isa(capacity(cap_link), OperationalProfile{VariableRef}) @test length(capacity(cap_link).vals) == length(๐’ฝโ‚€) + @test isa(cap_link.part_mult, PartitionProfile{VariableRef}) + @test isa(cap_link.part_dur, PartitionProfile{Int64}) + @test length(cap_link.part_dur.vals) == 4 + @test length(cap_link.part_mult.vals) == 2 end @testset "Full model run" begin @@ -171,12 +185,8 @@ end results = run_model_rh(case, modeltype, optimizer) # Extract data - ๐’ฉ = get_nodes(case) - โ„’ = get_links(case) - source = ๐’ฉ[1] - stor = ๐’ฉ[2] - sink = ๐’ฉ[3] - cap_link = โ„’[1] + src, stor, snk = get_nodes(case) + cap_link = get_links(case)[1] co2 = get_products(case)[2] ops = collect(get_time_struct(case)) @@ -196,33 +206,36 @@ end filter(r -> r.x1 == stor && r.x2 == last_ops[k], results[:stor_level])[!, :y] โ‰ˆ filter(r -> r.x1 == stor && r.x2 == first_ops[k], results[:stor_level])[!, :y] - filter(r -> r.x1 == stor && r.x2 == first_ops[k], results[:stor_level_ฮ”_op])[!, :y] - for k โˆˆ 1:3) + for k โˆˆ 1:3) # Test that the demand is equal to the profile and satisfied in all periods @test all( - filter(r -> r.x1 == sink && r.x2 == ops[k], results[:cap_use])[1, :y] โ‰ˆ - demand_profile[k] for k โˆˆ 1:8 - ) + filter(r -> r.x1 == snk && r.x2 == ops[k], results[:cap_use])[1, :y] โ‰ˆ + demand_profile[k] + for k โˆˆ 1:8) @test all( - filter(r -> r.x1 == sink && r.x2 == ops[k], results[:sink_deficit])[1, :y] โ‰ˆ 0 for - k โˆˆ 1:8 - ) + filter(r -> r.x1 == snk && r.x2 == ops[k], results[:sink_deficit])[1, :y] โ‰ˆ 0 + for k โˆˆ 1:8) # Test that the link capacity is equal to the profile @test all( filter(r -> r.x1 == cap_link && r.x2 == ops[k], results[:link_cap_inst])[1, :y] โ‰ˆ - cap_profile[k] for k โˆˆ 1:8 - ) + cap_profile[k] + for k โˆˆ 1:8) + + # Test that the link capacity is equal to the profile + @test all( + filter(r -> r.x1 == cap_link && r.x2 == ops[k], results[:link_out])[1, :y] โ‰ค + cap_profile[k] * mult_profile[pd] + for (k, pd) โˆˆ zip(1:8, [1, 1, 2, 2, 3, 3, 4, 4])) + # Test that the co2 process emissions are correctly updated @test all( filter( - r -> r.x1 == source && r.x2 == ops[k] && r.x3 == co2, + r -> r.x1 == src && r.x2 == ops[k] && r.x3 == co2, results[:emissions_node], - )[ - 1, - :y, - ] โ‰ˆ - filter(r -> r.x1 == source && r.x2 == ops[k], results[:cap_use])[1, :y] * em_co2[k] - for k โˆˆ 1:8) + )[1, :y] โ‰ˆ + filter(r -> r.x1 == src && r.x2 == ops[k], results[:cap_use])[1, :y] * em_co2[k] + for k โˆˆ 1:8) end diff --git a/test/test_utils.jl b/test/test_utils.jl index 54f12ee..7365fe6 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -328,8 +328,6 @@ end ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) - ๐’ฝ = first(โ„‹) - ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, pps) # Create the update type @@ -376,6 +374,7 @@ end # Create all time related parameters for the first horizon ๐’ฝ = first(โ„‹) + ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) ind_impl = indices_implementation(๐’ฝ) opers_opt = opers[indices_optimization(๐’ฝ)] opers_impl = opers[ind_impl] @@ -404,9 +403,6 @@ end @test data_init(๐’ฉสณ[4]).init_val_dict[:stor_level] == 5.0 @test surplus_penalty(๐’ฉสณ[5]).vals == surplus_penalty(๐’ฉ[5])[opers_opt] @test deficit_penalty(๐’ฉสณ[5]).vals == deficit_penalty(๐’ฉ[5])[parts_opt] - - # Delete the created method - Base.delete_method(@which EMRH.partition_periods(sink)) end end @@ -417,10 +413,13 @@ end el = ResourceCarrier("el", 0.2) ๐’ซ = [co2, el] - # Create the profile + # Create the profiles n_op = 15 + n_part = 8 dur_op = ones(n_op) profile = OperationalProfile(rand(n_op)) + part_profile = PartitionProfile(rand(n_part)) + part_dur = PartitionProfile(vcat(fill(2,7), [1])) # Create the individual nodes src = RefSource( @@ -444,15 +443,43 @@ end from::EMB.Node to::EMB.Node formulation::EMB.Formulation - profile::TimeProfile + capacity::TimeProfile + part_dur::PartitionProfile + part_mult::PartitionProfile + end + + # Add methods to required functions + EMB.capacity(l::ProfDirect) = l.capacity + EMB.capacity(l::ProfDirect, t) = l.capacity[t] + EMB.has_capacity(l::ProfDirect) = true + EMRH.partition_periods(l::ProfDirect) = l.part_dur + + function EMB.create_link(m, l::ProfDirect, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) + + # Declaration of the required subsets + ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, EMRH.partition_periods(l)) + + # Generic link in which each output corresponds to the input + @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ EMB.link_res(l)], + m[:link_out][l, t, p] == m[:link_in][l, t, p] + ) + + # Capacity constraint + @constraint(m, [t_pd โˆˆ ๐’ฏแต–แตˆ, t โˆˆ t_pd, p โˆˆ EMB.link_res(l)], + m[:link_out][l, t, p] โ‰ค m[:link_cap_inst][l, t] * l.part_mult[t_pd] + ) + constraints_capacity_installed(m, l, ๐’ฏ, modeltype) end + link = ProfDirect( "prof_link", src, sink, Linear(), profile, + part_dur, + part_profile, ) โ„’ = Link[link] @@ -465,9 +492,16 @@ end # Test of a link with operational profile # - _find_update_paths(field::AbstractElement, current_path::Vector{Any}, all_paths::Vector{Any}) # - _find_update_paths(field::OperationalProfile, current_path::Vector{Any}, all_paths::Vector{Any}) + # - _find_update_paths(field::PartitionProfile, current_path::Vector{Any}, all_paths::Vector{Any}) @test issetequal( EMRH._find_update_paths(link), - [[:from, EMRH.ElementPath()], [:to, EMRH.ElementPath()], [:profile, EMRH.OperPath()]], + [ + [:from, EMRH.ElementPath()], + [:to, EMRH.ElementPath()], + [:capacity, EMRH.OperPath()], + [:part_dur, EMRH.PartitionPath()], + [:part_mult, EMRH.PartitionPath()], + ], ) end @@ -485,7 +519,9 @@ end l = link @test lens_dict[l][[:from, EMRH.ElementPath()]](l) == src @test lens_dict[l][[:to, EMRH.ElementPath()]](l) == sink - @test lens_dict[l][[:profile, EMRH.OperPath()]](l) == profile + @test lens_dict[l][[:capacity, EMRH.OperPath()]](l) == profile + @test lens_dict[l][[:part_dur, EMRH.PartitionPath()]](l) == part_dur + @test lens_dict[l][[:part_mult, EMRH.PartitionPath()]](l) == part_profile end @testset "Reset functionality" begin @@ -512,6 +548,8 @@ end @test isa(๐’ฎแต›แต‰แถœ[2], Vector{EMRH.LinkSub}) @test EMRH.get_sub_ele(๐’ฐ, EMB.Link) == ๐’ฐ.elements[2] @test EMRH.get_sub_ele(๐’ฎแต›แต‰แถœ, EMB.Link) == ๐’ฐ.elements[2] + @test isa(EMRH.resets(๐’ฎแต›แต‰แถœ[2][1])[4], EMRH.EmptyReset) + @test isa(EMRH.resets(๐’ฎแต›แต‰แถœ[2][1])[5], EMRH.PartitionReset) # Test the resets (ElementReset) reset_link = EMRH.resets(๐’ฎแต›แต‰แถœ[2][1]) @@ -550,7 +588,9 @@ end # Test the individual resets of the link @test โ„’สณ[1].from == ๐’ฉสณ[1] @test โ„’สณ[1].to == ๐’ฉสณ[2] - @test โ„’สณ[1].profile.vals == โ„’[1].profile[opers_opt] + @test capacity(โ„’สณ[1]).vals == capacity(โ„’[1])[opers_opt] + @test EMRH.partition_periods(โ„’สณ[1]) == EMRH.partition_periods(โ„’[1]) + @test โ„’สณ[1].part_mult.vals == โ„’[1].part_mult.vals[1:2] end end @@ -558,6 +598,7 @@ end # Create the individual resources el = ResourceCarrier("el", 0.2) co2 = ResourceEmit("co2", 1.0) + ๐’ซ = [el, co2] # Create the profile n_op = 15 @@ -641,6 +682,7 @@ end # Create the individual resources el = ResourceCarrier("el", 0.2) co2 = ResourceEmit("co2", 1.0) + ๐’ซ = [el, co2] # Create the profile n_op = 15 From cb74123b8a5a05ba99ee51d79838e8b10b04507a Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 17 Sep 2026 09:36:51 +0200 Subject: [PATCH 05/13] Rewrote EMG extensions to work with new structure --- ext/EMGPOIExt/EMGPOIExt.jl | 2 +- ext/EMGPOIExt/utils/case_setup.jl | 4 +- test/test_emg.jl | 133 ++++++++++++++++-------------- 3 files changed, 72 insertions(+), 67 deletions(-) diff --git a/ext/EMGPOIExt/EMGPOIExt.jl b/ext/EMGPOIExt/EMGPOIExt.jl index c221d75..403d9c9 100644 --- a/ext/EMGPOIExt/EMGPOIExt.jl +++ b/ext/EMGPOIExt/EMGPOIExt.jl @@ -3,7 +3,7 @@ module EMGPOIExt using EnergyModelsBase using EnergyModelsGeography using EnergyModelsRecedingHorizon -using .EnergyModelsRecedingHorizon: RecHorEnergyModel, InitReset +using .EnergyModelsRecedingHorizon: RecHorEnergyModel, UpdateCase, InitReset using JuMP using ParametricOptInterface diff --git a/ext/EMGPOIExt/utils/case_setup.jl b/ext/EMGPOIExt/utils/case_setup.jl index c3f400c..089d2f7 100644 --- a/ext/EMGPOIExt/utils/case_setup.jl +++ b/ext/EMGPOIExt/utils/case_setup.jl @@ -1,9 +1,9 @@ """ - POIExt._update_parameter!(m, res_type::InitReset{EMGExt.TransInitDataPath}, opers::Vector) + POIExt._update_parameter!(m, ๐’ฐ::UpdateCase, res_type::InitReset{EMGExt.TransInitDataPath}, opers::Vector) Update the parameter value for a given `InitReset{EMGExt.TransInitDataPath}` to the provided value of the [`InitReset`](@ref EnergyModelsRecedingHorizon.InitReset) type. """ -POIExt._update_parameter!(m, res_type::InitReset{EMGExt.TransInitDataPath}, opers::Vector) = +POIExt._update_parameter!(m, ๐’ฐ::UpdateCase, res_type::InitReset{EMGExt.TransInitDataPath}, opers::Vector) = MOI.set(m, POI.ParameterValue(), res_type.var, res_type.val) diff --git a/test/test_emg.jl b/test/test_emg.jl index 50523ed..24512a1 100644 --- a/test/test_emg.jl +++ b/test/test_emg.jl @@ -4,7 +4,7 @@ H2_lp = ResourceCarrier("H2_lp", 0.0) el = ResourceCarrier("Power", 0.0) co2 = ResourceEmit("co2", 1.0) - resources = [H2_hp, H2_lp, el, co2] + ๐’ซ = [H2_hp, H2_lp, el, co2] # Create the profiles n_op = 15 @@ -13,7 +13,7 @@ prof_2 = OperationalProfile(rand(n_op)) # Create the Transmission corridors - av = GeoAvailability(1, resources) + av = GeoAvailability(1, ๐’ซ) ๐’ฉ = EMB.Node[av] a_1 = RefArea(2, "RefArea", 1, 1, av) a_2 = LimitedExchangeArea(1, "LEArea", 1, 1, av, Dict(H2_hp => prof_1, el => prof_2)) @@ -28,7 +28,6 @@ @test isnothing(data_init(a_1)) end - @testset "Path creation" begin # Test of all potential node input from EMRH as called through the function # - _find_update_paths(x::Union{AbstractElement, Resource, RecHorEnergyModel}) @@ -85,21 +84,17 @@ co2, ) - # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, ๐’œ) - # Create all time related parameters ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) ๐’ฝ = first(โ„‹) ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) - ind_impl = indices_implementation(๐’ฝ) - opers_opt = opers[indices_optimization(๐’ฝ)] - opers_impl = opers[ind_impl] - opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + + # Create the update type + โ„’ = Link[] + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, ๐’œ], [[get_nodes, get_areas]], Dict(:horizons => โ„‹)) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) # Test that the individual reset types functions are working # All functions are located within the file src/structures/reset.jl @@ -117,6 +112,13 @@ @test reset_area[1].lens(a_2) == av @test reset_area[1].val == av + # Create all time related parameters for the first horizon + ๐’ฝ = first(โ„‹) + ind_impl = indices_implementation(๐’ฝ) + opers_opt = opers[indices_optimization(๐’ฝ)] + opers_impl = opers[ind_impl] + opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + # Test that the reset are working # - _update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) # - _update_case_types! @@ -145,7 +147,7 @@ end H2_lp = ResourceCarrier("H2_lp", 0.0) el = ResourceCarrier("Power", 0.0) co2 = ResourceEmit("co2", 1.0) - resources = [H2_hp, H2_lp, el, co2] + ๐’ซ = [H2_hp, H2_lp, el, co2] # Create the profiles n_op = 15 @@ -199,7 +201,7 @@ end ) # Create the Transmission corridors - av = GeoAvailability(1, resources) + av = GeoAvailability(1, ๐’ซ) ๐’ฉ = EMB.Node[av] a_1 = RefArea(2, "RefArea", 1, 1, av) a_2 = LimitedExchangeArea(1, "LEArea", 1, 1, av, Dict(H2_hp => prof_1, el => prof_2)) @@ -302,22 +304,17 @@ end co2, ) - # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, ๐’œ) - EMRH._add_elements!(๐’ฐ, โ„’แต—สณแตƒโฟหข) - # Create all time related parameters ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) opers = collect(๐’ฏ) โ„‹ = PeriodHorizons(dur_op, 4, 2) ๐’ฝ = first(โ„‹) ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) - ind_impl = indices_implementation(๐’ฝ) - opers_opt = opers[indices_optimization(๐’ฝ)] - opers_impl = opers[ind_impl] - opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + + # Create the update type + โ„’ = Link[] + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, ๐’œ, โ„’แต—สณแตƒโฟหข], [[get_nodes, get_areas]], Dict(:horizons => โ„‹)) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) # Test that the individual reset types functions are working # All functions are located within the file src/structures/reset.jl and @@ -343,6 +340,13 @@ end l = l_pipe @test isa(reset_trans[5], EMRH.InitReset) + # Create all time related parameters for the first horizon + ๐’ฝ = first(โ„‹) + ind_impl = indices_implementation(๐’ฝ) + opers_opt = opers[indices_optimization(๐’ฝ)] + opers_impl = opers[ind_impl] + opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + # Test that the reset are working # - _update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) # - _update_case_types! @@ -382,15 +386,15 @@ end @testset "Full model run" begin # Introduction of profiles - demand_profile = [20, 10, 5, 25, 20, 10, 5, 25] - pipe_profile = [10, 10, 10, 10, 0, 10, 10, 10] + demand_profile = OperationalProfile([20, 10, 5, 25, 20, 10, 5, 25]) + pipe_profile = OperationalProfile([10, 10, 10, 10, 0, 10, 10, 10]) # Create the individual resources H2_hp = ResourceCarrier("H2_hp", 0.0) H2_lp = ResourceCarrier("H2_lp", 0.0) el = ResourceCarrier("Power", 0.0) co2 = ResourceEmit("co2", 1.0) - resources = [H2_hp, H2_lp, el, co2] + ๐’ซ = [H2_hp, H2_lp, el, co2] # Creation of the source and sink module as well as the arrays used for nodes and links h2_src = RefSource( @@ -409,19 +413,19 @@ end ) sink = RefSink( "snk", - OperationalProfile(demand_profile), + demand_profile, Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(100)), Dict(H2_lp => 1), ) - nodes = [ - GeoAvailability(1, resources), h2_src, el_src, - GeoAvailability(2, resources), sink + ๐’ฉ = [ + GeoAvailability(1, ๐’ซ), h2_src, el_src, + GeoAvailability(2, ๐’ซ), sink ] - links = [ - Direct(31, nodes[2], nodes[1], Linear()) - Direct(31, nodes[3], nodes[1], Linear()) - Direct(24, nodes[4], nodes[5], Linear()) + โ„’ = [ + Direct(31, ๐’ฉ[2], ๐’ฉ[1], Linear()) + Direct(31, ๐’ฉ[3], ๐’ฉ[1], Linear()) + Direct(24, ๐’ฉ[4], ๐’ฉ[5], Linear()) ] init_lp = TransInitData(Dict(:linepack_stor_level => 1.0)) @@ -432,7 +436,7 @@ end H2_lp, el, FixedProfile(0.01), - OperationalProfile(pipe_profile), + pipe_profile, FixedProfile(0.0), FixedProfile(0), FixedProfile(0), @@ -450,20 +454,22 @@ end 0.2, [init_lp], ) - modes = [pipe, pipe_lp] + โ„ณ = [pipe, pipe_lp] # Creation of the two areas and potential transmission lines - areas = [ - RefArea(1, "Oslo", 10.751, 59.921, nodes[1]), - RefArea(2, "Trondheim", 10.398, 63.4366, nodes[4]) + ๐’œ = [ + RefArea(1, "Oslo", 10.751, 59.921, ๐’ฉ[1]), + RefArea(2, "Trondheim", 10.398, 63.4366, ๐’ฉ[4]) ] - transmissions = [Transmission(areas[1], areas[2], modes)] + โ„’แต—สณแตƒโฟหข = [Transmission(๐’œ[1], ๐’œ[2], โ„ณ)] # Creation of the time structure and the used global data - T = TwoLevel(1, 1, SimpleTimes(8, 1);) - ops = collect(T) - โ„‹ = PeriodHorizons([duration(t) for t โˆˆ T], 4, 2) + n_op = 8 + dur_op = ones(n_op) + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op);) + ops = collect(๐’ฏ) + โ„‹ = PeriodHorizons(dur_op, 4, 2) modeltype = RecHorOperationalModel( Dict(co2 => FixedProfile(100)), Dict(co2 => FixedProfile(0)), @@ -472,9 +478,9 @@ end # Input data structure case = Case( - T, - resources, - [nodes, links, areas, transmissions], + ๐’ฏ, + ๐’ซ, + [๐’ฉ, โ„’, ๐’œ, โ„’แต—สณแตƒโฟหข], [[get_nodes, get_links], [get_areas, get_transmissions]], Dict(:horizons => โ„‹) ) @@ -484,14 +490,14 @@ end results = run_model_rh(case, modeltype, optimizer) # Test that all results were saved (* 2 as we have two TransmissionModes) - @test length(results[:trans_in][!, :y]) == length(ops) * 2 + @test length(results[:trans_in][!, :y]) == n_op * 2 # Test that the first period in the first horizon is correctly used @test mode_data(pipe_lp)[1].init_val_dict[:linepack_stor_level] โ‰ˆ - filter(r -> r.x1 == pipe_lp && r.x2 == ops[1], results[:linepack_stor_level])[1, :y] - - filter(r -> r.x1 == pipe_lp && r.x2 == ops[1], results[:trans_in])[1, :y] + - filter(r -> r.x1 == pipe_lp && r.x2 == ops[1], results[:trans_out])[1, :y] + - filter(r -> r.x1 == pipe_lp && r.x2 == ops[1], results[:trans_loss])[1, :y] + filter(r -> r.x1 == pipe_lp && r.x2 == first(๐’ฏ), results[:linepack_stor_level])[1, :y] - + filter(r -> r.x1 == pipe_lp && r.x2 == first(๐’ฏ), results[:trans_in])[1, :y] + + filter(r -> r.x1 == pipe_lp && r.x2 == first(๐’ฏ), results[:trans_out])[1, :y] + + filter(r -> r.x1 == pipe_lp && r.x2 == first(๐’ฏ), results[:trans_loss])[1, :y] # Test that the subsequent first periods are used correctly first_ops = [ops[3], ops[5], ops[7]] @@ -506,22 +512,21 @@ end # Test that the demand is equal to the profile and satisfied in all periods @test all( - filter(r -> r.x1 == sink && r.x2 == ops[k], results[:cap_use])[1, :y] โ‰ˆ - demand_profile[k] for k โˆˆ 1:8 - ) + filter(r -> r.x1 == sink && r.x2 == t, results[:cap_use])[1, :y] โ‰ˆ + demand_profile[t] + for t โˆˆ ๐’ฏ) @test all( - filter(r -> r.x1 == sink && r.x2 == ops[k], results[:sink_deficit])[1, :y] โ‰ˆ 0 for - k โˆˆ 1:8 - ) + filter(r -> r.x1 == sink && r.x2 == t, results[:sink_deficit])[1, :y] โ‰ˆ 0 + for t โˆˆ ๐’ฏ) # Test that the mode capacities are equal to the values @test all( - filter(r -> r.x1 == pipe && r.x2 == ops[k], results[:trans_cap])[1, :y] == - pipe_profile[k] for k โˆˆ 1:8 - ) + filter(r -> r.x1 == pipe && r.x2 == t, results[:trans_cap])[1, :y] == + pipe_profile[t] + for t โˆˆ ๐’ฏ) @test all( - filter(r -> r.x1 == pipe_lp && r.x2 == ops[k], results[:trans_cap])[1, :y] == - 50 for k โˆˆ 1:8 - ) + filter(r -> r.x1 == pipe_lp && r.x2 == t, results[:trans_cap])[1, :y] == + 50 + for t โˆˆ ๐’ฏ) end From cbb6bebac9a8678769dfcbe20403d96366700abb Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Thu, 17 Sep 2026 09:37:08 +0200 Subject: [PATCH 06/13] Updated remaining tests to work with changes --- test/test_future_value.jl | 35 +++++++++++++++++++++-------------- test/test_init_data.jl | 18 ++++++++++-------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/test/test_future_value.jl b/test/test_future_value.jl index 03de971..d47e040 100644 --- a/test/test_future_value.jl +++ b/test/test_future_value.jl @@ -24,6 +24,7 @@ end @testset "Access functions" begin # Create the StorageValueCut type Power = ResourceCarrier("Power", 0.0) + ๐’ซ = [Power] storage = RefStorage{RecedingAccumulating}( "storage", # Node ID StorCap(FixedProfile(10)), # Charge @@ -53,6 +54,7 @@ end # Create the StorageValueCuts type Power = ResourceCarrier("Power", 0.0) CO2 = ResourceEmit("CO2", 1.0) + ๐’ซ = [Power, CO2] stor_a = RefStorage{RecedingAccumulating}( "stor_a", StorCap(FixedProfile(10)), @@ -87,13 +89,13 @@ end ] ) for (j, k) โˆˆ enumerate(time_profile)] - return ๐’ฑ, ๐’ฉ, modeltype + return ๐’ซ, ๐’ฑ, ๐’ฉ, modeltype end @testset "Resetting of values" begin # Create the cuts time_vec = [0, 10, 10, 40, 70] - ๐’ฑ, ๐’ฉ, modeltype = stor_val_cuts(time_vec) + ๐’ซ, ๐’ฑ, ๐’ฉ, modeltype = stor_val_cuts(time_vec) # Test that the path is correctly created # - _find_update_paths(x::StorageValueCuts) @@ -108,10 +110,13 @@ end ], ) for v โˆˆ ๐’ฑ) - # Create the Update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, ๐’ฑ) + # Create all time related parameters + ๐’ฏ = TwoLevel(1, 12, SimpleTimes(12, 1)) + opers = collect(๐’ฏ) + + # Create the update type + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, ๐’ฑ], [[get_nodes, get_future_value]]) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) ๐’ฎโฟ = EMRH.get_sub_elements_vec(๐’ฐ)[1] ๐’ฎแต› = EMRH.get_sub_elements_vec(๐’ฐ)[2] @@ -141,7 +146,6 @@ end # Test that the `StorageValueCut`s are correctly reset # - _update_case_types!(๐’ฎแต›::Vector{<:AbstractSub}, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) - opers = collect(TwoLevel(1, 12, SimpleTimes(12,1))) EMRH._update_case_types!(๐’ฎโฟ, ๐’ฐ, opers) EMRH._update_case_types!(๐’ฎแต›, ๐’ฐ, opers) ๐’ฑแตฃโ‚• = EMRH.get_future_value(๐’ฐ) @@ -161,19 +165,22 @@ end @testset "Resetting of values - POI" begin # Create the cuts time_vec = [0, 10, 10, 40, 70] - ๐’ฑ, ๐’ฉ, modeltype = stor_val_cuts(time_vec) + ๐’ซ, ๐’ฑ, ๐’ฉ, modeltype = stor_val_cuts(time_vec) + + # Create all time related parameters + ๐’ฏ = TwoLevel(1, 12, SimpleTimes(12,1)) + opers = collect(๐’ฏ) - # Create the Update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, ๐’ฑ) + # Create the update type + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, ๐’ฑ], [[get_nodes, get_future_value]]) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) ๐’ฎโฟ = EMRH.get_sub_elements_vec(๐’ฐ)[1] ๐’ฎแต› = EMRH.get_sub_elements_vec(๐’ฐ)[2] ๐’ฎแต› = convert(Vector{EMRH.FutureValueSub{EMRH.StorageValueCuts}}, ๐’ฎแต›) # Test that the `StorageValueCut`s are correctly reset # - _update_case_types!(m, ๐’ฎแต›::Vector{<:AbstractSub}, ๐’ฐ::UpdateCase, opers::Vector{<:TS.TimePeriod}) - ๐’ฏแตฃโ‚• = TwoLevel(1,12, SimpleTimes(12,1)) + ๐’ฏแตฃโ‚• = TwoLevel(1,12, SimpleTimes(12, 1)) m = Model(() -> POI.Optimizer(HiGHS.Optimizer())) set_optimizer_attribute(m, MOI.Silent(), true) EMRH._update_case_types!(m, EMRH.get_sub_model(๐’ฐ), ๐’ฐ, ๐’ฏแตฃโ‚•) @@ -185,7 +192,7 @@ end # - _update_parameter!(m, res_type::TimeWeightReset, opers::Vector) EMRH.update_future_value!(๐’ฎแต›, 3) - POIExt._update_parameter_values!(m, ๐’ฎแต›, collect(๐’ฏแตฃโ‚•)) + POIExt._update_parameter_values!(m, ๐’ฐ, ๐’ฎแต›, collect(๐’ฏแตฃโ‚•)) @test all(iszero(parameter_value(EMRH.time_weight(v))) for v โˆˆ ๐’ฑแตฃโ‚• if v.id โˆ‰ [1,2,3]) @test parameter_value(EMRH.time_weight(๐’ฑแตฃโ‚•[1])) โ‰ˆ 0.7 @test parameter_value(EMRH.time_weight(๐’ฑแตฃโ‚•[2])) โ‰ˆ 0.3 diff --git a/test/test_init_data.jl b/test/test_init_data.jl index 9bb026a..25a20fa 100644 --- a/test/test_init_data.jl +++ b/test/test_init_data.jl @@ -3,6 +3,7 @@ co2 = ResourceEmit("co2", 1.0) el = ResourceCarrier("el", 0.2) ๐’ซ = [el, co2] + init_data = StorageInitData(10) src = RefSource( "electricity source", @@ -55,11 +56,15 @@ co2, ) + # Create all time related parameters + dur_op = ones(8) + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) + opers = collect(๐’ฏ) + โ„‹ = PeriodHorizons(dur_op, 4, 2) + # Create the update type - ๐’ฐ = EMRH._create_updatetype(modeltype) - EMRH._add_elements!(๐’ฐ, ๐’ซ) - EMRH._add_elements!(๐’ฐ, ๐’ฉ) - EMRH._add_elements!(๐’ฐ, โ„’) + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, โ„’], [[get_nodes, get_links]], Dict(:horizons => โ„‹)) + ๐’ฐ = EMRH._create_updatetype(case, modeltype) ๐’ฎแต›แต‰แถœ = EMRH.get_sub_elements_vec(๐’ฐ) ๐’ฎแต›แต‰แถœแตขโ‚™ = [filter(has_init, ๐’ฎ) for ๐’ฎ โˆˆ ๐’ฎแต›แต‰แถœ] @@ -70,10 +75,7 @@ @test ๐’ฎแต›แต‰แถœแตขโ‚™ == [[๐’ฎโฟ[2]], EMRH.LinkSub[]] @test all(!has_init(s_n) for s_n โˆˆ ๐’ฎโฟ[[1, 3]]) - # Create all time related parameters - ๐’ฏ = TwoLevel(1, 1, SimpleTimes(8, 1)) - opers = collect(๐’ฏ) - โ„‹ = PeriodHorizons(ones(8), 4, 2) + # Create all time related parameters for the first horizon ๐’ฝ = first(โ„‹) ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) ind_impl = indices_implementation(๐’ฝ) From bfa4f863932d4f0fc9baddb57bee292a3140bfd6 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 18 Sep 2026 11:24:17 +0200 Subject: [PATCH 07/13] Upated NEWS.md and docs, renamed `partition_periods` --- NEWS.md | 7 +++- docs/Project.toml | 1 + docs/make.jl | 2 + docs/src/how-to/adapt-emx-elem.md | 40 ++++++++++++++++++- .../src/library/internals/reference-EMGExt.md | 2 +- .../src/library/internals/reference-POIExt.md | 2 +- docs/src/library/internals/reset.md | 26 +----------- ext/POIExt/utils/case_setup.jl | 4 +- src/structures/reset.jl | 20 +++++----- src/utils/case_setup.jl | 2 +- test/test_poi.jl | 4 +- test/test_utils.jl | 12 +++--- 12 files changed, 74 insertions(+), 48 deletions(-) diff --git a/NEWS.md b/NEWS.md index f91be05..252e9ee 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,11 @@ # Release notes -## Version 0.2.3 (2026-07-xx) +## Version 0.3.0 (2026-07-xx) (Not yet registered) + +### Included support for `PartitionProfile` + +* Support for `PartitionProfile` required significant structural rework. +* It requires the user to create a new method to an existing function to extract the durations of the individual partitions. ### Improved results saving diff --git a/docs/Project.toml b/docs/Project.toml index 67d8f42..d2802c0 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -3,6 +3,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" DocumenterInterLinks = "d12716ef-a0f6-4df4-a9f1-a5a34e75c656" EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" +EnergyModelsFlex = "a81b9388-333d-4b63-81f2-910b060b544c" EnergyModelsGeography = "3f775d88-a4da-46c4-a2cc-aa9f16db6708" EnergyModelsRecedingHorizon = "37ec17de-8561-4b6b-83ee-0529988fea82" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" diff --git a/docs/make.jl b/docs/make.jl index bcfc584..d2045c9 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -8,6 +8,7 @@ using DocumenterCitations using TimeStruct using EnergyModelsBase +using EnergyModelsFlex using EnergyModelsGeography using EnergyModelsRecedingHorizon @@ -31,6 +32,7 @@ links = InterLinks( "TimeStruct" => "https://sintefore.github.io/TimeStruct.jl/stable/", "EnergyModelsBase" => "https://energymodelsx.github.io/EnergyModelsBase.jl/stable/", "EnergyModelsGeography" => "https://energymodelsx.github.io/EnergyModelsGeography.jl/stable/", + "EnergyModelsFlex" => "https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/", ) bib = CitationBibliography(joinpath(@__DIR__, "src", "references.bib")) diff --git a/docs/src/how-to/adapt-emx-elem.md b/docs/src/how-to/adapt-emx-elem.md index cf86bfb..505fca6 100644 --- a/docs/src/how-to/adapt-emx-elem.md +++ b/docs/src/how-to/adapt-emx-elem.md @@ -1,7 +1,7 @@ # [Adapt an EMX element](@id how_to-adapt_elem) This package is based on [`EnergyModelsBase`](https://energymodelsx.github.io/EnergyModelsBase.jl/), and therefore several aspects of creating a new element are covered in *[its documentation](@extref EnergyModelsBase how_to-create_node)*. -In this page, we will focus on the specific requirements for the introduction of new [`AbstractElement`](@extref EnergyModelsBase.AbstractElement)s in the receding horizon framework. +On this page, we focus on the specific requirements for the introduction of new [`AbstractElement`](@extref EnergyModelsBase.AbstractElement)s in the receding horizon framework. ## [Initialization settings](@id how_to-adapt_elem-init) @@ -26,3 +26,41 @@ This is not needed for elements that can use [`InitData`](@ref) and its default For elements with specific constructor needs, it is necessary to dispatch on `Accessors.ConstructionBase.constructorof`. This includes elements with parametric types for which the parametric input cannot be deduced from the fields (*e.g.*, `Storage` nodes), as well as elements with inner constructors. An example of how this can be done is shown in the package for the `Storage` node (see the [`constructorof` dispatch](@ref EMRH.Accessors.ConstructionBase.constructorof)). + +## [Elements with partitions](@id how_to-adapt_elem-part) + +If you have an [`AbstractElement`](@extref EnergyModelsBase.AbstractElement) that utilizes the concept of partitions *via* [`PartitionProfile`](@extref TimeStruct.PartitionProfile), you must create a new method for the function [`period_duration`](@ref EMRH.period_duration) for your type, which identifies the durations of the partitions. + +As an example, consider the node [`PeriodDemandSink`](@extref EnergyModelsFlex.PeriodDemandSink), in which we have demand periods based on partitions. +This node extracts the durations of the partitions using the `EnergyModelsFlex` internal function [`period_duration`](@extref EnergyModelsFlex.period_duration-Tuple{EnergyModelsFlex.AbstractPeriodDemandSink}). + +In this situation, you must declare a new method for [`period_duration`](@ref EMRH.period_duration) (the `EnergyModelsRecedingHorizon` function) as + +```julia +const EMRH = EnergyModelsRecedingHorizon +const EMF = EnergyModelsFlex + +EMRH.period_duration(n::EMF.AbstractPeriodDemandSink) = EMF.period_duration(n) +``` + +!!! warning "Reset of partition periods" + It is **not** possible to specify a `FixedProfile` for the duration of the partition periods due to the way the horizons are calculated. + + Using partitions is experimental. + You must be careful that the structure of the chosen partitions are in line with both the optimization and implementation horizon as outlined below: + + ```julia + # Time structure and chosen horizon + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(15, 1)) + โ„‹ = PeriodHorizons(15, 1, 8, 4) + + # Consistent profile (the last partition can only be of duration 1) + pprof_1 = PartitionProfile([2, 2, 2, 2, 2, 2, 2, 1]) + pprof_2 = PartitionProfile([4, 4, 4, 3]) + + # Inconsistent profile + pprof = PartitionProfile([3, 3, 3, 3, 3]) + ``` + + The profile is inconsistent for the receding horizon framework as the individual partitions are not fully included in both the implementation and optimization horizon. + It would be however consistent for a standard perfect foresight model. diff --git a/docs/src/library/internals/reference-EMGExt.md b/docs/src/library/internals/reference-EMGExt.md index a5b167f..08851b7 100644 --- a/docs/src/library/internals/reference-EMGExt.md +++ b/docs/src/library/internals/reference-EMGExt.md @@ -69,5 +69,5 @@ EMG.get_transmissions ## [ParametricOptInterface](@id lib-int-EMGExt-POI) ```@docs -POIExt._update_parameter!(m, res_type::EMRH.InitReset{EMGExt.TransInitDataPath}, opers::Vector) +POIExt._update_parameter!(m, ๐’ฐ::EMRH.UpdateCase, res_type::EMRH.InitReset{EMGExt.TransInitDataPath}, opers::Vector) ``` diff --git a/docs/src/library/internals/reference-POIExt.md b/docs/src/library/internals/reference-POIExt.md index 49b5bab..6c67321 100644 --- a/docs/src/library/internals/reference-POIExt.md +++ b/docs/src/library/internals/reference-POIExt.md @@ -22,7 +22,7 @@ update_model! ```@docs _update_parameter_values! -_update_parameter!(m, res_type::EMRH.ElementReset, opers::Vector) +_update_parameter!(m, ๐’ฐ::EMRH.UpdateCase, res_type::EMRH.ElementReset, opers::Vector) ``` ## [EnergyModelsRecedingHorizon](@id lib-int-POIExt-EMRH) diff --git a/docs/src/library/internals/reset.md b/docs/src/library/internals/reset.md index 6b5fa68..d70d4af 100644 --- a/docs/src/library/internals/reset.md +++ b/docs/src/library/internals/reset.md @@ -43,6 +43,7 @@ They are used internally for dispatch. ```@docs EMRH.AbstractReset +EMRH.EmptyReset EMRH.OperReset EMRH.PartitionReset EMRH.ElementReset @@ -60,32 +61,9 @@ EMRH.is_init_reset The following function is included to identify the individual partitions when creating a `PartitionReset`. ```@docs -EMRH.partition_periods +EMRH.period_duration ``` -!!! warning "Reset of partition periods" - The introduction of `PartitionProfile`s requires the user to create a new method for the function `partition_periods`. - This function should extract the `PartitionProfile` in which the durations of the individual partitions are specified. - It is **not** possible to specify a `FixedProfile` due to the way the horizons are calculated. - - Using partitions is experimental. - You must be careful that the structure of the chosen partitions are in line with the horizon as outlined below: - - ```julia - # Time structure and chosen horizon - ๐’ฏ = TwoLevel(1, 1, SimpleTimes(15, 1)) - โ„‹ = PeriodHorizons(15, 1, 4, 2) - - # Consistent profile (the last partition can only be of duration 1) - pprof = PartitionProfile([2, 2, 2, 2, 2, 2, 2, 1]) - - # Inconsistent profile - pprof = PartitionProfile([3, 3, 3, 3, 3]) - ``` - - The profile is inconsistent for the receding horizon framework as the individual partitions are not fully included in both the implementation and optimization horizon. - It would be however consistent for a standard perfect foresight model. - ## [Substitution types](@id lib-int-reset-sub) `Substitution` types correspond to types that store information of the individual original instance, the updated instance, and all required resets of an object: diff --git a/ext/POIExt/utils/case_setup.jl b/ext/POIExt/utils/case_setup.jl index fb7d7de..2e3c3d0 100644 --- a/ext/POIExt/utils/case_setup.jl +++ b/ext/POIExt/utils/case_setup.jl @@ -134,7 +134,7 @@ function EMRH._reset_field( ) # Identify the partitions of the original problem that are used within the current # receding horizon ๐’ฏแดฟแดด - ๐’ฏแต–แตˆ = EMRH.partition_periods(res_type, ๐’ฏแดฟแดด) + ๐’ฏแต–แตˆ = EMRH.period_duration(res_type, ๐’ฏแดฟแดด) # Reset the partition profile of the receding horizon problem based on the relevant # partitions @@ -222,7 +222,7 @@ function _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset, opers # Identify the partitions of the original problem that are used within the current # receding horizon problem - ๐’ฏแต–แตˆ = EMRH.partition_periods(res_type, ๐’ฏ) + ๐’ฏแต–แตˆ = EMRH.period_duration(res_type, ๐’ฏ) parts = filter(t_pd -> isempty(setdiff(t_pd, opers)), ๐’ฏแต–แตˆ) # Update the parameters diff --git a/src/structures/reset.jl b/src/structures/reset.jl index 73a6e0d..00560fb 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -147,7 +147,9 @@ end mutable struct PartitionReset <: AbstractReset [`AbstractReset`](@ref) for resetting partition profiles within an element. The inner -constructor is utilized for automatically creating the lens to the field path. +constructor is utilized for automatically creating the lens to the field path and to decide +whether an `EmptyReset` should be returned if it is called for the profile representing the +`period_duration` of the node.. # Inner constructor arguments - **`field_path::Vector`** is the path towards the field as identified through the function @@ -164,7 +166,7 @@ constructor is utilized for automatically creating the lens to the field path. framework. !!! warning "PartitionReset" - The `PartitionReset` type requires the user to declare a new method `partition_periods` + The `PartitionReset` type requires the user to declare a new method `period_duration` for the type `x` which utilizes the concept of partitions. An error is provided if no method is declared. """ @@ -176,7 +178,7 @@ mutable struct PartitionReset <: AbstractReset function PartitionReset(field_path::Vector, x) lens = _create_lens_for_field(field_path) val = lens(x) - pps = partition_periods(x) + pps = period_duration(x) if val โ‰  pps new(lens, nothing, val, pps) else @@ -274,22 +276,22 @@ is_init_reset(rt::AbstractReset) = false is_init_reset(rt::InitReset) = true """ - partition_periods(x) - partition_periods(res_type::PartitionReset, ๐’ฏ::TS.TimeStructure) + period_duration(x) + period_duration(res_type::PartitionReset, ๐’ฏ::TS.TimeStructure) """ -function partition_periods(x) +function period_duration(x) x_type = typeof(x) throw( ErrorException( - "No method for `partition_periods` is defined for $(x_type).\n" * + "No method for `period_duration` is defined for $(x_type).\n" * "This error is caused by including a `PartitionProfile` in $(x_type) which " * "requires a method for the `EnergyModelsRecedingHorizon` function " * - "`partition_periods(x)` to be defined." + "`period_duration(x)` to be defined." ) ) end -function partition_periods(res_type::PartitionReset, ๐’ฏ::TS.TimeStructure) +function period_duration(res_type::PartitionReset, ๐’ฏ::TS.TimeStructure) return partition_duration(๐’ฏ, res_type.pps) end diff --git a/src/utils/case_setup.jl b/src/utils/case_setup.jl index 65c6ce4..b80885a 100644 --- a/src/utils/case_setup.jl +++ b/src/utils/case_setup.jl @@ -108,7 +108,7 @@ function _reset_field( # Identify the partitions of the original problem that are used within the current # receding horizon problem - ๐’ฏแต–แตˆ = partition_periods(res_type, ๐’ฏ) + ๐’ฏแต–แตˆ = period_duration(res_type, ๐’ฏ) parts = filter(t_pd -> isempty(setdiff(t_pd, opers)), ๐’ฏแต–แตˆ) # Reset the partition profile of the receding horizon problem based on the relevant diff --git a/test/test_poi.jl b/test/test_poi.jl index 540ac85..7e0e57f 100644 --- a/test/test_poi.jl +++ b/test/test_poi.jl @@ -12,12 +12,12 @@ end EMB.capacity(l::CapDirect) = l.capacity EMB.capacity(l::CapDirect, t) = l.capacity[t] EMB.has_capacity(l::CapDirect) = true -EMRH.partition_periods(l::CapDirect) = l.part_dur +EMRH.period_duration(l::CapDirect) = l.part_dur function EMB.create_link(m, l::CapDirect, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) # Declaration of the required subsets - ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, EMRH.partition_periods(l)) + ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, EMRH.period_duration(l)) # Generic link in which each output corresponds to the input @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ EMB.link_res(l)], diff --git a/test/test_utils.jl b/test/test_utils.jl index 7365fe6..12ae971 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -313,9 +313,9 @@ end @testset "Reset functionality" begin # Test that the error throwing functionality is working - @test_throws ErrorException EMRH.partition_periods(sink) + @test_throws ErrorException EMRH.period_duration(sink) pps = PartitionProfile(vcat(fill(2,7), [1])) - EMRH.partition_periods(n::Sink) = pps + EMRH.period_duration(n::Sink) = pps # Create an operational modeltype and the time structure modeltype = RecHorOperationalModel( @@ -360,7 +360,7 @@ end @test reset_snk.val == deficit_penalty(sink) @test isnothing(reset_snk.var) @test all( - [reset_snk.pps.vals[k] == EMRH.partition_periods(sink).vals[k] for k โˆˆ 1:n_part] + [reset_snk.pps.vals[k] == EMRH.period_duration(sink).vals[k] for k โˆˆ 1:n_part] ) # Test the storages resets (InitReset) @@ -452,12 +452,12 @@ end EMB.capacity(l::ProfDirect) = l.capacity EMB.capacity(l::ProfDirect, t) = l.capacity[t] EMB.has_capacity(l::ProfDirect) = true - EMRH.partition_periods(l::ProfDirect) = l.part_dur + EMRH.period_duration(l::ProfDirect) = l.part_dur function EMB.create_link(m, l::ProfDirect, ๐’ฏ, ๐’ซ, modeltype::EnergyModel) # Declaration of the required subsets - ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, EMRH.partition_periods(l)) + ๐’ฏแต–แตˆ = partition_duration(๐’ฏ, EMRH.period_duration(l)) # Generic link in which each output corresponds to the input @constraint(m, [t โˆˆ ๐’ฏ, p โˆˆ EMB.link_res(l)], @@ -589,7 +589,7 @@ end @test โ„’สณ[1].from == ๐’ฉสณ[1] @test โ„’สณ[1].to == ๐’ฉสณ[2] @test capacity(โ„’สณ[1]).vals == capacity(โ„’[1])[opers_opt] - @test EMRH.partition_periods(โ„’สณ[1]) == EMRH.partition_periods(โ„’[1]) + @test EMRH.period_duration(โ„’สณ[1]) == EMRH.period_duration(โ„’[1]) @test โ„’สณ[1].part_mult.vals == โ„’[1].part_mult.vals[1:2] end end From 190cd65f261ac4ba552f9fed8cb370f2d445cf5f Mon Sep 17 00:00:00 2001 From: Julian Straus <104911227+JulStraus@users.noreply.github.com> Date: Thu, 24 Sep 2026 13:46:37 +0200 Subject: [PATCH 08/13] Apply batched suggestions from code review Co-authored-by: Lucas F. Bernardino --- docs/src/how-to/adapt-emx-elem.md | 18 ++---------------- src/structures/reset.jl | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/docs/src/how-to/adapt-emx-elem.md b/docs/src/how-to/adapt-emx-elem.md index 505fca6..10c0797 100644 --- a/docs/src/how-to/adapt-emx-elem.md +++ b/docs/src/how-to/adapt-emx-elem.md @@ -47,20 +47,6 @@ EMRH.period_duration(n::EMF.AbstractPeriodDemandSink) = EMF.period_duration(n) It is **not** possible to specify a `FixedProfile` for the duration of the partition periods due to the way the horizons are calculated. Using partitions is experimental. - You must be careful that the structure of the chosen partitions are in line with both the optimization and implementation horizon as outlined below: + You must be careful that the structure of the chosen partitions are in line with both the optimization and implementation horizon such that partitions are not split in the receding horizon optimization problems. See the examples below: - ```julia - # Time structure and chosen horizon - ๐’ฏ = TwoLevel(1, 1, SimpleTimes(15, 1)) - โ„‹ = PeriodHorizons(15, 1, 8, 4) - - # Consistent profile (the last partition can only be of duration 1) - pprof_1 = PartitionProfile([2, 2, 2, 2, 2, 2, 2, 1]) - pprof_2 = PartitionProfile([4, 4, 4, 3]) - - # Inconsistent profile - pprof = PartitionProfile([3, 3, 3, 3, 3]) - ``` - - The profile is inconsistent for the receding horizon framework as the individual partitions are not fully included in both the implementation and optimization horizon. - It would be however consistent for a standard perfect foresight model. + diff --git a/src/structures/reset.jl b/src/structures/reset.jl index 00560fb..2413129 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -149,7 +149,7 @@ end [`AbstractReset`](@ref) for resetting partition profiles within an element. The inner constructor is utilized for automatically creating the lens to the field path and to decide whether an `EmptyReset` should be returned if it is called for the profile representing the -`period_duration` of the node.. +`period_duration` of the node. # Inner constructor arguments - **`field_path::Vector`** is the path towards the field as identified through the function From 858bb5cffd1df6672a394ded36a0c57c707ecc3d Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 25 Sep 2026 08:39:40 +0200 Subject: [PATCH 09/13] Fixed problems with resets --- ext/POIExt/POIExt.jl | 1 + ext/POIExt/utils/case_setup.jl | 47 ++++++++++++++++++++++++---------- src/structures/reset.jl | 46 ++++++++++++++++++++++++++++----- test/test_utils.jl | 4 +-- 4 files changed, 76 insertions(+), 22 deletions(-) diff --git a/ext/POIExt/POIExt.jl b/ext/POIExt/POIExt.jl index dc68b8b..c15c631 100644 --- a/ext/POIExt/POIExt.jl +++ b/ext/POIExt/POIExt.jl @@ -4,6 +4,7 @@ using EnergyModelsBase using EnergyModelsRecedingHorizon using .EnergyModelsRecedingHorizon: RecHorEnergyModel, UpdateCase, AbstractSub, ElementReset, InitReset, TimeWeightReset, OperReset, PartitionReset, EmptyReset, +StandResBehav, NoResBehav, _create_updatetype, _add_elements!, update_results!, update_init_data!, _update_case_types!, update_future_value!, get_sub_model, get_sub_products, get_sub_elements_vec, get_sub_ele, updated, original, resets, _initialize_loop_variables, _add_mapping! diff --git a/ext/POIExt/utils/case_setup.jl b/ext/POIExt/utils/case_setup.jl index 2e3c3d0..7619c39 100644 --- a/ext/POIExt/utils/case_setup.jl +++ b/ext/POIExt/utils/case_setup.jl @@ -75,7 +75,9 @@ end EMRH._reset_field(m, x_rh, res_type::ElementReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) EMRH._reset_field(m, x_rh, res_type::Union{InitReset,TimeWeightReset}, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) EMRH._reset_field(m, x_rh, res_type::OperReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) - EMRH._reset_field(m, x_rh, res_type::PartitionReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) + EMRH._reset_field(m, x_rh, res_type::PartitionReset{StandResBehav}, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) + EMRH._reset_field(m, x_rh, res_type::PartitionReset{NoResBehav}, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) + EMRH._reset_field(m, x_rh, res_type::EmptyReset, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure) Resets the field expressed through `res_type` of element `x_rh` with the new value. The type of the new value is depending on the specified `res_type`: @@ -86,10 +88,12 @@ of the new value is depending on the specified `res_type`: and resets the field with it, 3. `res_type::OperReset` creates multiple MOI parameters and a new operational profile based on the original operational profile, and resets the field with it. -4. `res_type::PartitionReset` creates multiple MOI parameters and a new partition profile - based on the original partition profile, and resets the field with it. -5. `restype::EmptyReset` does not reset any field or introduce variables. It is used to - avoid problems with partition profile resetting. +4. `res_type::PartitionReset{StandResBehav}` creates multiple MOI parameters and a new + partition profile based on the original partition profile, and resets the field with it. +5. `restype::PartitionReset{NoResBehav}` does not reset any field or introduce variables. + It is used to avoid problems with partition profile resetting. It will however check, + that the specified `period_duration` is consistent with the time structure. +6. `restype::EmptyReset` does not reset any field or introduce variables. """ function EMRH._reset_field( m, @@ -128,7 +132,7 @@ end function EMRH._reset_field( m, x_rh, - res_type::PartitionReset, + res_type::PartitionReset{StandResBehav}, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure, ) @@ -146,7 +150,16 @@ end function EMRH._reset_field( m, x_rh, - res_type::EmptyReset, + res_type::PartitionReset{NoResBehav}, + ๐’ฐ::UpdateCase, + ๐’ฏแดฟแดด::TimeStructure, +) + return x_rh +end +function EMRH._reset_field( + m, + x_rh, + res_type::Union{EmptyReset, PartitionReset{NoResBehav}}, ๐’ฐ::UpdateCase, ๐’ฏแดฟแดด::TimeStructure, ) @@ -189,7 +202,8 @@ end _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::OperReset, opers::Vector) _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::InitReset{EMRH.InitDataPath}, opers::Vector) _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::TimeWeightReset, opers::Vector) - _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset{StandResBehav}, opers::Vector) + _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset{NoResBehav}, opers::Vector) _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::EmptyReset, opers::Vector) Set the parameter value in `m` for a given `res_type`: @@ -200,10 +214,11 @@ Set the parameter value in `m` for a given `res_type`: 4. `res_type::OperReset` creates a new operational profile based on the original operational profile and the set of operational periods in `opers`, updating each parameter with it. -5. `res_type::PartitionReset` creates a new partition profile based on the original - partition profile and the set of partition periods, identified through `opers`, updating - each parameter with it. -6. `res_type::EmptyReset` results in no update, +5. `res_type::PartitionReset{StandResBehav}` creates a new partition profile based on + the original partition profile and the set of partition periods, identified through + `opers`, updating each parameter with it. +6. `res_type::PartitionReset{NoResBehav}` results in no update, +7. `res_type::EmptyReset` results in no update, """ _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::ElementReset, opers::Vector) = nothing _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::InitReset{EMRH.InitDataPath}, opers::Vector) = @@ -216,7 +231,12 @@ function _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::OperReset, opers::Vec MOI.set(m, POI.ParameterValue(), var, val[i]) end end -function _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset, opers::Vector) +function _update_parameter!( + m, + ๐’ฐ::UpdateCase, + res_type::PartitionReset{StandResBehav}, + opers::Vector +) # Extract the required variables from the UpdateCase ๐’ฏ = get_time_struct(๐’ฐ) @@ -231,4 +251,5 @@ function _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset, opers MOI.set(m, POI.ParameterValue(), var, val[i]) end end +_update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset{NoResBehav}, opers::Vector) = nothing _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::EmptyReset, opers::Vector) = nothing diff --git a/src/structures/reset.jl b/src/structures/reset.jl index 2413129..206e80f 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -62,6 +62,31 @@ Returns the model key (field `key`) of InitDataPath `idp`. """ model_key(idp::InitDataPath) = idp.key +""" + abstract type AbstractResetBehavior + +Supertype for behaviors of resets, that is how the reset or if the reset should take place. +The behavior is only utilized for the `ParametricOptInterface` extension as some profiles +should never be substituted with a variable. +""" +abstract type AbstractResetBehavior end + +""" + struct NormResBehav <: AbstractResetBehavior + +`AbstractResetBehavior` in which the reset should operate as standardized, *i.e.*, the +`AbstractReset` subtype will allow for the standard reset. +""" +struct StandResBehav <: AbstractResetBehavior end + +""" + struct NormResBehav <: AbstractResetBehavior + +`AbstractResetBehavior` in which the reset should not occur in the `ParametricOptInterface` +extension. +""" +struct NoResBehav <: AbstractResetBehavior end + """ abstract type AbstractReset @@ -144,12 +169,19 @@ mutable struct OperReset <: AbstractReset end """ - mutable struct PartitionReset <: AbstractReset + mutable struct PartitionReset{AbstractResetBehavior} <: AbstractReset [`AbstractReset`](@ref) for resetting partition profiles within an element. The inner -constructor is utilized for automatically creating the lens to the field path and to decide -whether an `EmptyReset` should be returned if it is called for the profile representing the -`period_duration` of the node. +constructor is utilized for + +- automatically creating the lens to the field path and +- to decide whether the reset behaviour is + - [`StandResBehav`](@ref) for the standard behavior with reset or + - [`NoResBehav`](@ref) if it is called for the profile representing the `period_duration` + of the node. + +This differentiation only affects the `ParametricOptInterface` extension. The core structure +will always apply the reset. # Inner constructor arguments - **`field_path::Vector`** is the path towards the field as identified through the function @@ -170,7 +202,7 @@ whether an `EmptyReset` should be returned if it is called for the profile repre for the type `x` which utilizes the concept of partitions. An error is provided if no method is declared. """ -mutable struct PartitionReset <: AbstractReset +mutable struct PartitionReset{AbstractResetBehavior} <: AbstractReset lens::Union{PropertyLens,ComposedFunction} var val::PartitionProfile @@ -180,9 +212,9 @@ mutable struct PartitionReset <: AbstractReset val = lens(x) pps = period_duration(x) if val โ‰  pps - new(lens, nothing, val, pps) + new{StandResBehav}(lens, nothing, val, pps) else - EmptyReset() + new{NoResBehav}(lens, nothing, val, pps) end end end diff --git a/test/test_utils.jl b/test/test_utils.jl index 12ae971..e1fb0d2 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -548,7 +548,7 @@ end @test isa(๐’ฎแต›แต‰แถœ[2], Vector{EMRH.LinkSub}) @test EMRH.get_sub_ele(๐’ฐ, EMB.Link) == ๐’ฐ.elements[2] @test EMRH.get_sub_ele(๐’ฎแต›แต‰แถœ, EMB.Link) == ๐’ฐ.elements[2] - @test isa(EMRH.resets(๐’ฎแต›แต‰แถœ[2][1])[4], EMRH.EmptyReset) + @test isa(EMRH.resets(๐’ฎแต›แต‰แถœ[2][1])[4], EMRH.PartitionReset) @test isa(EMRH.resets(๐’ฎแต›แต‰แถœ[2][1])[5], EMRH.PartitionReset) # Test the resets (ElementReset) @@ -589,7 +589,7 @@ end @test โ„’สณ[1].from == ๐’ฉสณ[1] @test โ„’สณ[1].to == ๐’ฉสณ[2] @test capacity(โ„’สณ[1]).vals == capacity(โ„’[1])[opers_opt] - @test EMRH.period_duration(โ„’สณ[1]) == EMRH.period_duration(โ„’[1]) + @test EMRH.period_duration(โ„’สณ[1]).vals == EMRH.period_duration(โ„’[1]).vals[1:2] @test โ„’สณ[1].part_mult.vals == โ„’[1].part_mult.vals[1:2] end end From 2f80226c1a8952763e1e6cf0c31001661b3879ae Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 25 Sep 2026 13:13:52 +0200 Subject: [PATCH 10/13] Added checks for period partitions --- NEWS.md | 2 +- docs/src/how-to/adapt-emx-elem.md | 33 +++++- docs/src/library/internals/methods-EMRH.md | 7 ++ docs/src/library/internals/reset.md | 8 ++ ext/POIExt/model.jl | 4 +- ext/POIExt/utils/case_setup.jl | 8 ++ src/model.jl | 3 + src/utils/case_setup.jl | 126 +++++++++++++++++++++ test/runtests.jl | 11 +- test/test_poi.jl | 11 +- test/test_utils.jl | 96 +++++++++++----- 11 files changed, 269 insertions(+), 40 deletions(-) diff --git a/NEWS.md b/NEWS.md index 252e9ee..8bae547 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # Release notes -## Version 0.3.0 (2026-07-xx) (Not yet registered) +## Version 0.2.3 (2026-09-xx) (Not yet registered) ### Included support for `PartitionProfile` diff --git a/docs/src/how-to/adapt-emx-elem.md b/docs/src/how-to/adapt-emx-elem.md index 10c0797..2264d7c 100644 --- a/docs/src/how-to/adapt-emx-elem.md +++ b/docs/src/how-to/adapt-emx-elem.md @@ -43,10 +43,33 @@ const EMF = EnergyModelsFlex EMRH.period_duration(n::EMF.AbstractPeriodDemandSink) = EMF.period_duration(n) ``` -!!! warning "Reset of partition periods" - It is **not** possible to specify a `FixedProfile` for the duration of the partition periods due to the way the horizons are calculated. +!!! warning "Caveats of period partitions" + It is **not** possible to specify a `FixedProfile` for the duration of the period partitions due to the way the horizons are calculated. - Using partitions is experimental. - You must be careful that the structure of the chosen partitions are in line with both the optimization and implementation horizon such that partitions are not split in the receding horizon optimization problems. See the examples below: + Using partitions is experimental: - + 1. You must be careful that the structure of the chosen partitions are in line with both the optimization and implementation horizon such that partitions are not split in the receding horizon optimization problems. + 2. Partitions must be of the same length in all horizons if you utilize the `ParametricOptInterface` extension. + This is checked in the + + See the examples below: + + ```julia + # Time structure and chosen horizon + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(15, 1)) + โ„‹ = PeriodHorizons(15, 1, 8, 4) + + # Consistent profiles + pprof_1 = PartitionProfile([2, 2, 2, 2, 2, 2, 2, 1]) + pprof_2 = PartitionProfile([4, 4, 4, 3]) + + # Inconsistent profile + pprof_wrong_1 = PartitionProfile([3, 3, 3, 3, 3]) + + # Inconsistent profile for the ParametricOptInterface extension + pprof_wrong_2 = PartitionProfile([4, 2, 2, 4, 2, 1]) + ``` + + * The profile `pprof_wrong_1` is inconsistent for the receding horizon framework as the individual partitions are not fully included in both the implementation and optimization horizons. + * The profile `pprof_wrong_2` is inconsistent for the `ParametricOptInterface` extension as the partitions are different in the different horizons while it would work for the base design. + The profiles would, however, be consistent for a standard perfect foresight model. diff --git a/docs/src/library/internals/methods-EMRH.md b/docs/src/library/internals/methods-EMRH.md index 68a19b0..35d3c91 100644 --- a/docs/src/library/internals/methods-EMRH.md +++ b/docs/src/library/internals/methods-EMRH.md @@ -39,6 +39,13 @@ EMRH._create_lens_for_field EMRH._path_type ``` +### [Horizon consistency check functions](@id lib-int-check) + +```@docs +EMRH._check_period_partitions +EMRH._check_horizon +``` + ### [Functions for resetting values](@id lib-int-reset) ```@docs diff --git a/docs/src/library/internals/reset.md b/docs/src/library/internals/reset.md index d70d4af..84a4c2f 100644 --- a/docs/src/library/internals/reset.md +++ b/docs/src/library/internals/reset.md @@ -64,6 +64,14 @@ The following function is included to identify the individual partitions when cr EMRH.period_duration ``` +The following types were included to create a parametric type for `PartitionReset` to describe the behavior of the `AbstractReset`: + +```@docs +EMRH.AbstractResetBehavior +EMRH.StandResBehav +EMRH.NoResBehav +``` + ## [Substitution types](@id lib-int-reset-sub) `Substitution` types correspond to types that store information of the individual original instance, the updated instance, and all required resets of an object: diff --git a/ext/POIExt/model.jl b/ext/POIExt/model.jl index 7f66527..f327842 100644 --- a/ext/POIExt/model.jl +++ b/ext/POIExt/model.jl @@ -25,7 +25,6 @@ function EMRH.run_model_rh( n_๐’ฝ = length(โ„‹) op_per_strat = ๐’ฏ.op_per_strat - # Assert that the horizon is functioning with the POI implementation. horizon_duration = all( durations(๐’ฝ) == durations(๐’ฝโ‚€) for @@ -44,6 +43,9 @@ function EMRH.run_model_rh( # Create the `UpdateCase` based on the original `Case` structure ๐’ฐ = _create_updatetype(case, modeltype) + # Identify problems with potential `period_duration`s in the time structure + EMRH._check_period_partitions(๐’ฐ, โ„‹, optimizer) + # Extract the time structure from the case to identify the used operational periods # and the receding horizon time structure if use_op_per_strat diff --git a/ext/POIExt/utils/case_setup.jl b/ext/POIExt/utils/case_setup.jl index 7619c39..1393b0d 100644 --- a/ext/POIExt/utils/case_setup.jl +++ b/ext/POIExt/utils/case_setup.jl @@ -253,3 +253,11 @@ function _update_parameter!( end _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::PartitionReset{NoResBehav}, opers::Vector) = nothing _update_parameter!(m, ๐’ฐ::UpdateCase, res_type::EmptyReset, opers::Vector) = nothing + +function EMRH._check_number_op(n::Vector{Int}, n_ref::Vector{Int}, msg::String, _::POI.Optimizer) + if n โ‰  n_ref + return " The $(msg) horizon differs from the first horizon.\n" + else + return "" + end +end diff --git a/src/model.jl b/src/model.jl index 18aca47..58474c9 100644 --- a/src/model.jl +++ b/src/model.jl @@ -65,6 +65,9 @@ function run_model_rh( # Initialize loop variables ๐’ฎแต›โปแต›แต‰แถœ, ๐’ฎแต›แต‰แถœแตขโ‚™, results, vars = _initialize_loop_variables(๐’ฐ) + # Identify problems with potential `period_duration`s in the time structure + _check_period_partitions(๐’ฐ, โ„‹, optimizer) + # Iterate through the different horizons and solve the problem for ๐’ฝ โˆˆ โ„‹ @info "Solving for horizon $(๐’ฝ.id) of $n_๐’ฝ" diff --git a/src/utils/case_setup.jl b/src/utils/case_setup.jl index b80885a..7476a8e 100644 --- a/src/utils/case_setup.jl +++ b/src/utils/case_setup.jl @@ -259,3 +259,129 @@ _type_to_key(::Type{T}) where {T<:Union{EMB.Node, NodeSub}} = :nodes _type_to_key(::Type{T}) where {T<:Union{Link, LinkSub}} = :links _type_to_key(::Type{T}) where {T<:Union{FutureValue, FutureValueSub}} = :future_values _type_to_key(::Type{T}) where {T<:Union{EnergyModel, ModelSub}} = :modeltype + +""" + _check_period_partitions(๐’ฐ::UpdateCase, โ„‹::AbstractHorizons, optimizer) + _check_period_partitions(log::Dict{String, Vector{String}}, ๐’ฎ::Vector{<:AbstractSub}, ๐’ฏ::TS.TimeStructure, โ„‹::AbstractHorizons, optimizer) + _check_period_partitions(log::Dict{String, Vector{String}}, s::AbstractSub, ๐’ฏ::TS.TimeStructure, โ„‹::AbstractHorizons, optimizer) + +Function for checking that the individual period partitions are in line with the horizon +structure `โ„‹`. + +Returns an `AssertionError` if any period partition is not consistent with `โ„‹`. It furthermore +creates a log which points towards any inconsistent partitions and horizons. +""" +function _check_period_partitions(๐’ฐ::UpdateCase, โ„‹::AbstractHorizons, optimizer) + # Extract the information from the `UpdateCase` + ๐’ฏ = get_time_struct(๐’ฐ) + + # Collect all potential problems + log = Dict{String, Vector{String}}() + _check_period_partitions(log, get_sub_model(๐’ฐ), ๐’ฏ, โ„‹, optimizer) + _check_period_partitions(log, get_sub_products(๐’ฐ), ๐’ฏ, โ„‹, optimizer) + for ๐’ฎ โˆˆ get_sub_elements_vec(๐’ฐ) + _check_period_partitions(log, ๐’ฎ, ๐’ฏ, โ„‹, optimizer) + end + + # Throw an `ErrorException` if there are inconsistent partitions + !isempty(log) && EMB.compile_logs(๐’ฐ, log) +end + +function _check_period_partitions( + log::Dict{String, Vector{String}}, + ๐’ฎ::Vector{<:AbstractSub}, + ๐’ฏ::TS.TimeStructure, + โ„‹::AbstractHorizons, + optimizer, +) + for s โˆˆ ๐’ฎ + _check_period_partitions(log, s, ๐’ฏ, โ„‹, optimizer) + end +end +function _check_period_partitions( + log::Dict{String, Vector{String}}, + s::AbstractSub, + ๐’ฏ::TS.TimeStructure, + โ„‹::AbstractHorizons, + optimizer, +) + opers = collect(๐’ฏ) + + # Identify whether the `Substitution` has `PartitionReset`s + part_res = filter(res_type -> isa(res_type, PartitionReset), resets(s)) + + if !isempty(part_res) + # Extract the period duration for the reset. It must be the same for all resets + ๐’ฏแต–แตˆ = period_duration(first(part_res), ๐’ฏ) + opers = collect(๐’ฏ) + + # Iterate through all horizons + x_org = original(s) + opers_opt_ref = nothing + n_opt_ref = nothing + n_impl_ref = nothing + log["$(x_org)"] = String[] + for ๐’ฝ โˆˆ โ„‹ + # Extract variables and reassign values + opers_opt = opers[indices_optimization(๐’ฝ)] + opers_impl = opers[indices_implementation(๐’ฝ)] + if isnothing(opers_opt_ref) + opers_opt_ref = deepcopy(opers_opt) + end + if length(opers_opt) < length(opers_opt_ref) + n_opt_ref = nothing + n_impl_ref = nothing + end + + # Check the two horizons + msg_opt, bool_opt, n_opt_ref = + _check_horizon(n_opt_ref, ๐’ฏแต–แตˆ, opers_opt, "optimization", optimizer) + msg_impl, bool_impl, n_impl_ref = + _check_horizon(n_impl_ref, ๐’ฏแต–แตˆ, opers_impl, "implementation", optimizer) + + # Add to the logs + (bool_opt || bool_impl) && push!(log["$(x_org)"], "Horizon $(๐’ฝ): \n") + if bool_opt + log["$(x_org)"][end] *= msg_opt + end + if bool_impl + log["$(x_org)"][end] *= msg_impl + end + end + end +end + +""" + _check_horizon(n_ref::Union{Nothing, Vector{Int}}, ๐’ฏแต–แตˆ, opersแตฃโ‚•, name::String, optimizer) + +Function for identifying any potential inconsistenceies between the operational periods of a +horizon `opersแตฃโ‚•` and the period partitions `๐’ฏแต–แตˆ` for a given horizon `name`. + +Returns a log, a boolean indicating whether there are inconsistencies and the reference +number of partitions within the horizon. +""" +function _check_horizon(n_ref::Union{Nothing, Vector{Int}}, ๐’ฏแต–แตˆ, opersแตฃโ‚•, name::String, optimizer) + parts = filter(t_pd -> isempty(setdiff(t_pd, opersแตฃโ‚•)), ๐’ฏแต–แตˆ) + n_parts = [length(part) for part โˆˆ parts] + log = "" + bool_op = nothing + bool_poi = false + try + opersโ‚šโ‚แตฃโ‚œ = reduce(vcat, [collect(part) for part โˆˆ parts]) + bool_op = any(t โˆ‰ opersโ‚šโ‚แตฃโ‚œ for t โˆˆ opersแตฃโ‚•) + if isnothing(n_ref) + n_ref = n_parts + else + log = _check_number_op(n_parts, n_ref, name, optimizer) + bool_poi = !isempty(log) + end + catch e + bool_op = true + end + if bool_op + log *= " The partitions are inconsistent with the $(name) horizon.\n" + end + return log, bool_op || bool_poi, n_ref +end + +_check_number_op(_::Vector{Int}, _::Vector{Int}, _::String, _) = "" diff --git a/test/runtests.jl b/test/runtests.jl index 36518a7..4ea7684 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -29,15 +29,13 @@ logger_org = global_logger() logger_new = ConsoleLogger(Error) global_logger(logger_new) +ENV["EMB_TEST"] = true # Set flag for example scripts to check if they are run as part of the testsx + @testset "RecedingHorizon" begin @testset "RecedingHorizon | General" begin include("test_general.jl") end - @testset "RecedingHorizon | Examples" begin - include("test_examples.jl") - end - @testset "RecedingHorizon | FutureValue" begin include("test_future_value.jl") end @@ -61,6 +59,11 @@ global_logger(logger_new) @testset "RecedingHorizon | Geography" begin include("test_emg.jl") end + + @testset "RecedingHorizon | Examples" begin + include("test_examples.jl") + end end + # Reset the loglevel global_logger(logger_org) diff --git a/test/test_poi.jl b/test/test_poi.jl index 7e0e57f..0f8656f 100644 --- a/test/test_poi.jl +++ b/test/test_poi.jl @@ -34,7 +34,6 @@ end # Introduction of different profiles price_profile = [10, 10, 10, 10, 1000, 1000, 1000, 1000] cap_profile = [20, 30, 40, 30, 10, 50, 35, 20] -part_profile = [2, 2, 2, 2] mult_profile = [2, 1, 1.5, 1] demand_profile = [20, 15, 20, 15, 10, 10, 20, 20] em_co2 = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8] @@ -44,6 +43,7 @@ function create_poi_case(; dur_op = [1, 1, 1, 1, 1, 1, 1, 1], init_state = 10, HorizonType = PeriodHorizons, + part_profile = [2, 2, 2, 2] ) #Define resources with their emission intensities power = ResourceCarrier("power", 0.0) @@ -170,6 +170,7 @@ end @testset "Full model run" begin optimizer = POI.Optimizer(HiGHS.Optimizer()) + # Test that the wrong horizon type is caught case, modeltype = create_poi_case(; HorizonType = DurationHorizons) @test_throws AssertionError run_model_rh(case, modeltype, optimizer) @@ -179,6 +180,14 @@ end case, modeltype = create_poi_case(; dur_op) @test_throws AssertionError run_model_rh(case, modeltype, optimizer) + # Test that a wrong partition profile is caught + EMB.TEST_ENV = true + optimizer = POI.Optimizer(HiGHS.Optimizer()) + part_profile = [2, 1, 1, 2, 1, 1] + case, modeltype = create_poi_case(; part_profile) + @test_throws AssertionError run_model_rh(case, modeltype, optimizer) + EMB.TEST_ENV = false + # Run a working model case, modeltype = create_poi_case() optimizer = POI.Optimizer(HiGHS.Optimizer()) diff --git a/test/test_utils.jl b/test/test_utils.jl index e1fb0d2..bc8c396 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -559,38 +559,78 @@ end @test reset_link[2].lens(link) == sink @test reset_link[2].val == sink - # Create all time related parameters for the first horizon - ๐’ฝ = first(โ„‹) - ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) - ind_impl = indices_implementation(๐’ฝ) - opers_opt = opers[indices_optimization(๐’ฝ)] - opers_impl = opers[ind_impl] - opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + # Create all time related parameters for the first and second last horizon + # The latter is required as `partition_duration` should return a different value + part_vec = [[1, 2], [n_part-1, n_part]] + for (k, ๐’ฝ) โˆˆ enumerate(collect(โ„‹)[[1, end-1]]) + ๐’ฏแตฃโ‚• = TwoLevel(1, sum(durations(๐’ฝ)), SimpleTimes(durations(๐’ฝ))) + ind_impl = indices_implementation(๐’ฝ) + opers_opt = opers[indices_optimization(๐’ฝ)] + opers_impl = opers[ind_impl] + opers_implแตฃโ‚• = collect(๐’ฏแตฃโ‚•)[1:length(ind_impl)] + + # Test that the reset functionality is working + # - _update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) + # - _update_case_types! + # - reset_field + EMRH._update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) + + # Extract the resetted nodes and links + ๐’ฉสณ = [s.new for s โˆˆ ๐’ฎแต›แต‰แถœ[1]] + โ„’สณ = [s.new for s โˆˆ ๐’ฎแต›แต‰แถœ[2]] + @test get_elements_vec(๐’ฐ) == Vector[๐’ฉสณ, โ„’สณ] + @test get_links(๐’ฐ) == โ„’สณ + @test ๐’ฉสณ โ‰  ๐’ฉ + @test โ„’สณ โ‰  โ„’ + + # Test that the nodes are reset + @test opex_var(๐’ฉสณ[1]).vals == opex_var(๐’ฉ[1])[opers_opt] + @test capacity(๐’ฉสณ[2]).vals == capacity(๐’ฉ[2])[opers_opt] + + # Test the individual resets of the link + @test โ„’สณ[1].from == ๐’ฉสณ[1] + @test โ„’สณ[1].to == ๐’ฉสณ[2] + @test capacity(โ„’สณ[1]).vals == capacity(โ„’[1])[opers_opt] + @test EMRH.period_duration(โ„’สณ[1]).vals == EMRH.period_duration(โ„’[1]).vals[part_vec[k]] + @test โ„’สณ[1].part_mult.vals == โ„’[1].part_mult.vals[part_vec[k]] + end + end - # Test that the reset functionality is working - # - _update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) - # - _update_case_types! - # - reset_field - EMRH._update_update_case!(๐’ฐ, opers_opt, ๐’ฏแตฃโ‚•) + @testset "Check of period partitions" begin + # Set the global to true to suppress the error message + EMB.TEST_ENV = true + + # Create a node that has an inconsistent partition duration + โ„’ = [ + ProfDirect( + "prof_link", + src, + sink, + Linear(), + profile, + PartitionProfile(fill(3, 5)), + part_profile, + ) + ] - # Extract the resetted nodes and links - ๐’ฉสณ = [s.new for s โˆˆ ๐’ฎแต›แต‰แถœ[1]] - โ„’สณ = [s.new for s โˆˆ ๐’ฎแต›แต‰แถœ[2]] - @test get_elements_vec(๐’ฐ) == Vector[๐’ฉสณ, โ„’สณ] - @test get_links(๐’ฐ) == โ„’สณ - @test ๐’ฉสณ โ‰  ๐’ฉ - @test โ„’สณ โ‰  โ„’ + # Create an operational modeltype and the time structure + modeltype = RecHorOperationalModel( + Dict(co2 => FixedProfile(100)), + Dict(co2 => FixedProfile(60)), + co2, + ) - # Test that the nodes are reset - @test opex_var(๐’ฉสณ[1]).vals == opex_var(๐’ฉ[1])[opers_opt] - @test capacity(๐’ฉสณ[2]).vals == capacity(๐’ฉ[2])[opers_opt] + # Create all time related parameters + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(dur_op)) + opers = collect(๐’ฏ) + โ„‹ = PeriodHorizons(dur_op, 4, 2) - # Test the individual resets of the link - @test โ„’สณ[1].from == ๐’ฉสณ[1] - @test โ„’สณ[1].to == ๐’ฉสณ[2] - @test capacity(โ„’สณ[1]).vals == capacity(โ„’[1])[opers_opt] - @test EMRH.period_duration(โ„’สณ[1]).vals == EMRH.period_duration(โ„’[1]).vals[1:2] - @test โ„’สณ[1].part_mult.vals == โ„’[1].part_mult.vals[1:2] + # Create the case + case = Case(๐’ฏ, ๐’ซ, [๐’ฉ, โ„’], [[get_nodes, get_links]], Dict(:horizons => โ„‹)) + @test_throws AssertionError run_model_rh(case, modeltype, HiGHS.Optimizer()) + + # Set the global to true to suppress the error message + EMB.TEST_ENV = true end end From 5f5d52ab46f120b57315a49163221d3a64e4cebe Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Fri, 25 Sep 2026 14:46:25 +0200 Subject: [PATCH 11/13] Fixed typos in the description --- docs/src/how-to/adapt-emx-elem.md | 2 +- ext/EMGExt/structures/node.jl | 2 +- ext/POIExt/utils/case_setup.jl | 6 +++--- src/structures/future_value.jl | 2 +- src/structures/reset.jl | 6 +++--- src/utils/case_setup.jl | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/how-to/adapt-emx-elem.md b/docs/src/how-to/adapt-emx-elem.md index 2264d7c..f672718 100644 --- a/docs/src/how-to/adapt-emx-elem.md +++ b/docs/src/how-to/adapt-emx-elem.md @@ -50,7 +50,7 @@ EMRH.period_duration(n::EMF.AbstractPeriodDemandSink) = EMF.period_duration(n) 1. You must be careful that the structure of the chosen partitions are in line with both the optimization and implementation horizon such that partitions are not split in the receding horizon optimization problems. 2. Partitions must be of the same length in all horizons if you utilize the `ParametricOptInterface` extension. - This is checked in the + This is checked in the receding horizon validation routines. See the examples below: diff --git a/ext/EMGExt/structures/node.jl b/ext/EMGExt/structures/node.jl index 7fa721b..d251d89 100644 --- a/ext/EMGExt/structures/node.jl +++ b/ext/EMGExt/structures/node.jl @@ -5,7 +5,7 @@ When an abstract reset `ri` for a Transmission corridor`l` and a data path `idp` instead of the vector of `AbstractSub` vectors `๐’ฎแต›แต‰แถœแตขโ‚™`, it updates the value in AbstractReset `ri` of AbstractElement `x` with the value of the variable specified by the `key` of the `idp` in the last period of `opers_implแตฃโ‚•` for the `TransmissionMode` specified -by the the `idx` of `idp`. +by the `idx` of `idp`. """ function EMRH.update_init_data!( m, diff --git a/ext/POIExt/utils/case_setup.jl b/ext/POIExt/utils/case_setup.jl index 1393b0d..1e0b63d 100644 --- a/ext/POIExt/utils/case_setup.jl +++ b/ext/POIExt/utils/case_setup.jl @@ -2,7 +2,7 @@ _init_update_case!(m, ๐’ฐ, opers, ๐’ฏแตฃโ‚•) Initialize the JuMP model `m` and the [`UpdateCase`](@ref) `๐’ฐ` with the anonymous variables -corresponding to the inidividual fields of all types whose value(s) change(s). +corresponding to the individual fields of all types whose value(s) change(s). In addition, the [`UpdateCase`](@ref) `๐’ฐ` is updated with the mapping between the operational periods of the optimization (through `๐’ฏแตฃโ‚•`) and the original (through `opers`) problem. @@ -90,10 +90,10 @@ of the new value is depending on the specified `res_type`: on the original operational profile, and resets the field with it. 4. `res_type::PartitionReset{StandResBehav}` creates multiple MOI parameters and a new partition profile based on the original partition profile, and resets the field with it. -5. `restype::PartitionReset{NoResBehav}` does not reset any field or introduce variables. +5. `res_type::PartitionReset{NoResBehav}` does not reset any field or introduce variables. It is used to avoid problems with partition profile resetting. It will however check, that the specified `period_duration` is consistent with the time structure. -6. `restype::EmptyReset` does not reset any field or introduce variables. +6. `res_type::EmptyReset` does not reset any field or introduce variables. """ function EMRH._reset_field( m, diff --git a/src/structures/future_value.jl b/src/structures/future_value.jl index 278d939..209422e 100644 --- a/src/structures/future_value.jl +++ b/src/structures/future_value.jl @@ -167,7 +167,7 @@ element_type(v::TypeFutureValue) = v.element_type """ coefficients(v::TypeFutureValue) -Returns the the cofficients dictionary of of the future value `v`. +Returns the coefficients dictionary of the future value `v`. """ coefficients(v::TypeFutureValue) = v.val_dict diff --git a/src/structures/reset.jl b/src/structures/reset.jl index 206e80f..269ed4f 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -72,7 +72,7 @@ should never be substituted with a variable. abstract type AbstractResetBehavior end """ - struct NormResBehav <: AbstractResetBehavior + struct StandResBehav <: AbstractResetBehavior `AbstractResetBehavior` in which the reset should operate as standardized, *i.e.*, the `AbstractReset` subtype will allow for the standard reset. @@ -80,7 +80,7 @@ abstract type AbstractResetBehavior end struct StandResBehav <: AbstractResetBehavior end """ - struct NormResBehav <: AbstractResetBehavior + struct NoResBehav <: AbstractResetBehavior `AbstractResetBehavior` in which the reset should not occur in the `ParametricOptInterface` extension. @@ -101,7 +101,7 @@ related to the chosen [`AbstractPath`](@ref) as outlined above. If you require resetting different fields than the provided, you must include a new [`AbstractPath`](@ref) subtype, a new mutable composite type as subtype of `AbstractReset`, - and a new method for the the constructor [`ResetType`](@ref). + and a new method for the constructor [`ResetType`](@ref). """ abstract type AbstractReset end diff --git a/src/utils/case_setup.jl b/src/utils/case_setup.jl index 7476a8e..f1e1ffa 100644 --- a/src/utils/case_setup.jl +++ b/src/utils/case_setup.jl @@ -67,7 +67,7 @@ of the new value is depending on the specified `res_type`: operational profile in `res_type` and the set of operational periods `opers`. 4. `res_type::PartitionReset` creates a new partition profile based on the original partition profile in `res_type` and the set of operational periods `opers`. -5. `restype::EmptyReset` does not reset any field and is used to avoid problems with +5. `res_type::EmptyReset` does not reset any field and is used to avoid problems with partition profile resetting. """ function _reset_field( @@ -354,7 +354,7 @@ end """ _check_horizon(n_ref::Union{Nothing, Vector{Int}}, ๐’ฏแต–แตˆ, opersแตฃโ‚•, name::String, optimizer) -Function for identifying any potential inconsistenceies between the operational periods of a +Function for identifying any potential inconsistencies between the operational periods of a horizon `opersแตฃโ‚•` and the period partitions `๐’ฏแต–แตˆ` for a given horizon `name`. Returns a log, a boolean indicating whether there are inconsistencies and the reference From 2226871df0aa0560c94c2322fe4609ed8443d6a1 Mon Sep 17 00:00:00 2001 From: Julian Straus <104911227+JulStraus@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:14:13 +0200 Subject: [PATCH 12/13] Apply batched suggestions from code review Co-authored-by: Lucas F. Bernardino --- src/structures/reset.jl | 2 +- src/utils/case_setup.jl | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/structures/reset.jl b/src/structures/reset.jl index 269ed4f..6373b45 100644 --- a/src/structures/reset.jl +++ b/src/structures/reset.jl @@ -109,7 +109,7 @@ abstract type AbstractReset end """ struct EmptyReset <: AbstractReset -Empty reset type, introduced to avoid problems with partition profile resetting. +Empty reset type, introduced for fields not subject to resetting. """ struct EmptyReset <: AbstractReset end diff --git a/src/utils/case_setup.jl b/src/utils/case_setup.jl index f1e1ffa..587eafb 100644 --- a/src/utils/case_setup.jl +++ b/src/utils/case_setup.jl @@ -67,8 +67,7 @@ of the new value is depending on the specified `res_type`: operational profile in `res_type` and the set of operational periods `opers`. 4. `res_type::PartitionReset` creates a new partition profile based on the original partition profile in `res_type` and the set of operational periods `opers`. -5. `res_type::EmptyReset` does not reset any field and is used to avoid problems with - partition profile resetting. +5. `res_type::EmptyReset` does not reset any field. """ function _reset_field( x_rh, From 481d2809a0bb610150bd5057b90f90f0be1707b6 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 29 Sep 2026 08:46:16 +0200 Subject: [PATCH 13/13] Updated NEWS.md --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 8bae547..2024226 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,6 @@ # Release notes -## Version 0.2.3 (2026-09-xx) (Not yet registered) +## Version 0.2.3 (2026-09-29) ### Included support for `PartitionProfile`