From fcc0404937249374b955f631c1d95ce56e34a9f7 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Sun, 28 Jun 2026 16:24:56 +0200 Subject: [PATCH 1/4] Rewrote CapacityCostLink based on PeriodPartition * Constraints implementation adjusted * Test adjusted based on requirements * Checks not yet adjusted * Documentation not yet adjusted --- src/link/checks.jl | 2 +- src/link/datastructures.jl | 80 ++++++++++++++++----- src/link/model.jl | 84 +++++----------------- test/link/test_CapacityCostLink.jl | 110 +++++++++++++++++------------ 4 files changed, 144 insertions(+), 132 deletions(-) diff --git a/src/link/checks.jl b/src/link/checks.jl index 462ca7a..7870356 100644 --- a/src/link/checks.jl +++ b/src/link/checks.jl @@ -19,7 +19,7 @@ function EMB.check_link(l::CapacityCostLink, ๐’ฏ, ::EnergyModel, ::Bool) all(cap_price(l)[t] โ‰ฅ 0 for t โˆˆ ๐’ฏ), "The capacity price must be non-negative." ) - check_cap_price_periods(l, ๐’ฏ, cap_price_periods(l)) + # check_cap_price_periods(l, ๐’ฏ, cap_price_periods(l)) end """ diff --git a/src/link/datastructures.jl b/src/link/datastructures.jl index 65125ec..bf80ca3 100644 --- a/src/link/datastructures.jl +++ b/src/link/datastructures.jl @@ -1,9 +1,9 @@ """ struct CapacityCostLink <: Link -A link between two nodes with costs on the link usage for the resource `cap_resource`. All -other resources have no costs associated with their usage (follows the -[`Direct`](@extref EnergyModelsBase.Direct)). +A link between two nodes with costs on the maximum link usage for the resource `cap_resource` +within specified price periods. All other resources have no costs associated with their usage +(as they follow the [`Direct`](@extref EnergyModelsBase.Direct) approach). # Fields - **`id`** is the name/identifier of the link. @@ -11,9 +11,11 @@ other resources have no costs associated with their usage (follows the - **`to::Node`** is the node to which there is flow out of the link. - **`cap::TimeProfile`** is the capacity of the link for the `cap_resource`. - **`cap_price::TimeProfile`** is the price of capacity usage for the `cap_resource`. -- **`cap_price_periods::Union{Int64, Vector{<:Number}}`** is either the number of sub periods - within a strategic period (if specified as `Int64`) or the minimum durations of the - individual sub periods within a strategic period (if specified as `Vector{<:Number}`). +- **`cap_period_duration::TimeProfile`** is a time profile describing the durations of the + individual capacity price periods. Due to a constructor, it can either be specified as + number (the same duration in all price periods), as a vector (varying duration of each + price period), or as a time profile (*e.g.*, varying period durations due to varying + operational time structures). It cannot be specified as `OperationalProfile`. - **`cap_resource::Resource`** is the resource used by `CapacityCostLink` - **`formulation::Formulation`** is the used formulation of links. The field `formulation` is conditional through usage of a constructor. @@ -32,7 +34,7 @@ struct CapacityCostLink <: EMB.Link to::EMB.Node cap::TimeProfile cap_price::TimeProfile - cap_price_periods::Union{Int64, Vector{<:Number}} + cap_period_duration::TimeProfile cap_resource::Resource formulation::EMB.Formulation data::Vector{<:ExtensionData} @@ -44,17 +46,45 @@ function CapacityCostLink( to::EMB.Node, cap::TimeProfile, cap_price::TimeProfile, - cap_price_periods::Union{Int64, Vector{<:Number}}, + cap_period_duration::Union{Number, Vector{<:Number}}, cap_resource::Resource, formulation::EMB.Formulation, + data::Vector{<:ExtensionData} ) + if isa(cap_period_duration, Number) + price_per = FixedProfile(cap_period_duration) + elseif isa(cap_period_duration, Vector{<:Number}) + price_per = PartitionProfile(cap_period_duration) + end return CapacityCostLink( id, from, to, cap, cap_price, - cap_price_periods, + price_per, + cap_resource, + formulation, + data, + ) +end +function CapacityCostLink( + id::Any, + from::EMB.Node, + to::EMB.Node, + cap::TimeProfile, + cap_price::TimeProfile, + cap_period_duration::Union{Number, Vector{<:Number}, TimeProfile}, + cap_resource::Resource, + formulation::EMB.Formulation, +) + return CapacityCostLink( + id, + from, + to, + cap, + cap_price, + cap_period_duration, cap_resource, formulation, ExtensionData[], @@ -66,7 +96,7 @@ function CapacityCostLink( to::EMB.Node, cap::TimeProfile, cap_price::TimeProfile, - cap_price_periods::Union{Int64, Vector{<:Number}}, + cap_period_duration::Union{Number, Vector{<:Number}, TimeProfile}, cap_resource::Resource, data::Vector{<:ExtensionData}, ) @@ -76,7 +106,7 @@ function CapacityCostLink( to, cap, cap_price, - cap_price_periods, + cap_period_duration, cap_resource, Linear(), data, @@ -88,7 +118,7 @@ function CapacityCostLink( to::EMB.Node, cap::TimeProfile, cap_price::TimeProfile, - cap_price_periods::Union{Int64, Vector{<:Number}}, + cap_period_duration::Union{Number, Vector{<:Number}, TimeProfile}, cap_resource::Resource, ) return CapacityCostLink( @@ -97,7 +127,7 @@ function CapacityCostLink( to, cap, cap_price, - cap_price_periods, + cap_period_duration, cap_resource, Linear(), ExtensionData[], @@ -146,19 +176,31 @@ EMB.outputs(l::CapacityCostLink) = [cap_resource(l)] """ cap_price(l::CapacityCostLink) + cap_price(l::CapacityCostLink, t::TS.TimePeriod) -Returns the price per unit of maximum capacity usage of a capacity cost link `l`. +Returns the price per unit of maximum capacity usage of of `CapacityCostLink` `l` as +`TimeProfile` or in time period `t`. """ cap_price(l::CapacityCostLink) = l.cap_price -cap_price(l::CapacityCostLink, t) = l.cap_price[t] +cap_price(l::CapacityCostLink, t::TS.TimePeriod) = l.cap_price[t] + +""" + period_duration(n::CapacityCostLink) + +Returns the prices periods of `CapacityCostLink` `n` as `TimeProfile` or in price +period `t_pd`. +""" +period_duration(l::CapacityCostLink) = l.cap_period_duration +period_duration(l::CapacityCostLink, t_pd::TS.PeriodPartition) = + l.cap_period_duration[t_pd] """ - cap_price_periods(l::CapacityCostLink) + periods(l::CapacityCostLink, ts::TS.TimeStructure) -Returns either the number of sub-periods within a strategic period for which a price is -calculated or the vector of the durations of the sub-periods of a capacity cost link `l`. +Returns the price periods of capacity cost link `l` for the given time structure. """ -cap_price_periods(l::CapacityCostLink) = l.cap_price_periods +periods(l::CapacityCostLink, ts::TS.TimeStructure) = + partition_duration(ts, period_duration(l)) """ cap_resource(l::CapacityCostLink) diff --git a/src/link/model.jl b/src/link/model.jl index 9476ece..cb6811d 100644 --- a/src/link/model.jl +++ b/src/link/model.jl @@ -2,14 +2,14 @@ EMB.variables_element(m, โ„’หขแต˜แต‡::Vector{<:CapacityCostLink}, ๐’ฏ, modeltype::EnergyModel) Creates the following additional variable for **ALL** capacity cost links: -- `ccl_cap_use_max[l, t]` is a continuous variable describing the maximum capacity - usage over sub periods for a [`CapacityCostLink`](@ref) `l` in operational period `t`. -- `ccl_cap_use_cost[l, t]` is a continuous variable describing the cost over sub periods - for a [`CapacityCostLink`](@ref) `l` in operational period `t`. +- `ccl_cap_use_max[l, t_pd]` is a continuous variable describing the maximum capacity + usage of [`CapacityCostLink`](@ref) `l` in cost period `t_pd`. +- `ccl_cap_use_cost[l, t_pd]` is a continuous variable describing the cost + of a [`CapacityCostLink`](@ref) `l` in cost period `t_pd`. """ function EMB.variables_element(m, โ„’หขแต˜แต‡::Vector{<:CapacityCostLink}, ๐’ฏ, ::EnergyModel) - @variable(m, ccl_cap_use_max[โ„’หขแต˜แต‡, ๐’ฏ] โ‰ฅ 0) - @variable(m, ccl_cap_use_cost[โ„’หขแต˜แต‡, ๐’ฏ] โ‰ฅ 0) + @variable(m, ccl_cap_use_max[l โˆˆ โ„’หขแต˜แต‡, periods(l, ๐’ฏ)] โ‰ฅ 0) + @variable(m, ccl_cap_use_cost[l โˆˆ โ„’หขแต˜แต‡, periods(l, ๐’ฏ)] โ‰ฅ 0) end """ @@ -33,7 +33,7 @@ function EMB.create_link( p_cap = cap_resource(l) # Create sub-periods based on the user-defined number of sub periods of a year - ๐’ฏหขแต˜แต‡ = get_sub_periods(l, ๐’ฏ) + ๐’ฏแต–แตˆ = periods(l, ๐’ฏ) # Capacity cost link where output equals input (no losses) @constraint(m, [t โˆˆ ๐’ฏ], @@ -45,80 +45,34 @@ function EMB.create_link( constraints_capacity_installed(m, l, ๐’ฏ, modeltype) # Max capacity use constraints - @constraint(m, [t_sub โˆˆ ๐’ฏหขแต˜แต‡, t โˆˆ t_sub], - m[:link_in][l, t, p_cap] .โ‰ค m[:ccl_cap_use_max][l, t_sub] + @constraint(m, [t_pd โˆˆ ๐’ฏแต–แตˆ, t โˆˆ t_pd], + m[:link_in][l, t, p_cap] .โ‰ค m[:ccl_cap_use_max][l, t_pd] ) # Capacity cost constraint - @constraint(m, [t_sub โˆˆ ๐’ฏหขแต˜แต‡], - m[:ccl_cap_use_cost][l, t_sub[end]] == - m[:ccl_cap_use_max][l, t_sub[end]] * get_avg_cap_price(l, t_sub) + @constraint(m, [t_pd โˆˆ ๐’ฏแต–แตˆ], + m[:ccl_cap_use_cost][l, t_pd] == + m[:ccl_cap_use_max][l, t_pd] * get_avg_cap_price(l, t_pd) ) # Sum up costs for each sub_period into the strategic period cost @constraint(m, [t_inv โˆˆ ๐’ฏแดตโฟแต›], m[:link_opex_var][l, t_inv] == - sum(m[:ccl_cap_use_cost][l, t] for t โˆˆ t_inv) + sum(m[:ccl_cap_use_cost][l, t_pd] for t_pd โˆˆ periods(l, t_inv)) ) - # Fix the fixed OPEX to avoid unconstrainted variables + # Fix the fixed OPEX to avoid unconstrained variables for t_inv โˆˆ ๐’ฏแดตโฟแต› fix(m[:link_opex_fixed][l, t_inv], 0; force = true) end end """ - get_avg_cap_price(l::CapacityCostLink, t_sub::Vector{TS.TimePeriod}) + get_avg_cap_price(l::CapacityCostLink, t_pd::TS.PeriodPartition) -Return the average capacity price over the sub period `t_sub` for the [`CapacityCostLink`](@ref) `l`. +Return the average capacity price over the sub period `t_pd` for the +[`CapacityCostLink`](@ref) `l`. """ -function get_avg_cap_price(l::CapacityCostLink, t_sub::Vector{<:TS.TimePeriod}) - return sum(cap_price(l, t) * duration(t) for t โˆˆ t_sub) / sum(duration(t) for t โˆˆ t_sub) +function get_avg_cap_price(l::CapacityCostLink, t_pd::TS.PeriodPartition) + return sum(cap_price(l, t) * duration(t) for t โˆˆ t_pd) / sum(duration(t) for t โˆˆ t_pd) end - -""" - get_sub_periods(l::CapacityCostLink, ๐’ฏ) - -Return the vector of sub periods of the [`CapacityCostLink`](@ref) `l`. -""" -function get_sub_periods(l::CapacityCostLink, ๐’ฏ) - # Calculate the duration of each sub period - sub_pers_durations = get_sub_pers_durations(l, ๐’ฏ) - - # Create a vector collecting all `TimePeriod`s of each sub period into a vector - sub_periods = Vector{TS.TimePeriod}[] - for t_inv โˆˆ strategic_periods(๐’ฏ) - idx = 1 - accumulated_duration = 0.0 - sub_period = TS.TimePeriod[] - for t โˆˆ t_inv - push!(sub_period, t) - accumulated_duration += duration(t) * multiple_strat(t_inv, t) - - # Check if the accumulated time of the periods in `sub_period` fills up a sub - # period duration - if accumulated_duration โ‰ˆ sub_pers_durations[idx] - push!(sub_periods, sub_period) - sub_period = TS.TimePeriod[] - accumulated_duration = 0.0 - idx += 1 - end - end - end - - return sub_periods -end - -""" - get_sub_pers_durations(l::CapacityCostLink, ๐’ฏ) - get_sub_pers_durations(l::CapacityCostLink, ๐’ฏ, price_pers::Int64) - get_sub_pers_durations(l::CapacityCostLink, ๐’ฏ, price_pers::Vector{<:Number}) - -Returns the individual durations of the different sub periods within a time structure `๐’ฏ` -and [`CapacityCostLink`](@ref) `l`. -""" -get_sub_pers_durations(l::CapacityCostLink, ๐’ฏ) = - get_sub_pers_durations(l, ๐’ฏ, cap_price_periods(l)) -get_sub_pers_durations(l::CapacityCostLink, ๐’ฏ, price_pers::Int64) = - ones(price_pers) * ๐’ฏ.op_per_strat / price_pers -get_sub_pers_durations(l::CapacityCostLink, ๐’ฏ, price_pers::Vector{<:Number}) = price_pers diff --git a/test/link/test_CapacityCostLink.jl b/test/link/test_CapacityCostLink.jl index 7eadd54..9d3243b 100644 --- a/test/link/test_CapacityCostLink.jl +++ b/test/link/test_CapacityCostLink.jl @@ -5,7 +5,7 @@ co2 = ResourceEmit("COโ‚‚", 0.0) function capacity_cost_link_case(; cap = FixedProfile(10), capacity_price = StrategicProfile([5e5, 1e6, 2e6]), - capacity_price_periods = 2, + cap_per_dur = 24, ) # Define the different resources ๐’ซ = [power, co2] @@ -47,7 +47,7 @@ function capacity_cost_link_case(; ๐’ฉ[3], cap, capacity_price, - capacity_price_periods, + cap_per_dur, power, ), ] @@ -78,21 +78,21 @@ end @test_throws AssertionError capacity_cost_link_case(; capacity_price) # Test that the number of sub periods is positive - @test_throws AssertionError capacity_cost_link_case(; capacity_price_periods = 0) - @test_throws AssertionError capacity_cost_link_case(; capacity_price_periods = -1) + @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = 0) + @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = -1) # Test that operational time structure can can be represented by `cap_price_periods` # sub periods when specified as `Int64` (8760 is not divisible by 7 sub periods) - @test_throws AssertionError capacity_cost_link_case(; capacity_price_periods = 7) + @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = 7) # Test that the sum of sub periods is equal to the operational time structure duration - capacity_price_periods = ones(24) - @test_throws AssertionError capacity_cost_link_case(; capacity_price_periods) + cap_per_dur = ones(24) + @test_throws AssertionError capacity_cost_link_case(; cap_per_dur) - # Test that that none of the durations of the sub periods is not positive - capacity_price_periods = + # Test that none of the durations of the sub periods is not positive + cap_per_dur = [0.0, 4.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0] * 365 - @test_throws AssertionError capacity_cost_link_case(; capacity_price_periods) + @test_throws AssertionError capacity_cost_link_case(; cap_per_dur) # Set the global to true to suppress the error message EMB.TEST_ENV = false @@ -103,7 +103,7 @@ end m, case, modeltype = capacity_cost_link_case() cc_link = get_links(case)[2] ๐’ฏ = get_time_struct(case) - sub_periods = EMF.get_sub_periods(cc_link, ๐’ฏ) + ๐’ฏแต–แตˆ = EMF.periods(cc_link, ๐’ฏ) ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) @testset "EMX functions" begin @@ -123,36 +123,37 @@ end capacity_prices = StrategicProfile([5e5, 1e6, 2e6]) @test all(EMF.cap_price(cc_link)[t_inv] == capacity_prices[t_inv] for t_inv โˆˆ ๐’ฏแดตโฟแต›) @test EMF.cap_price(cc_link).vals == capacity_prices.vals - @test EMF.cap_price_periods(cc_link) == 2 + @test EMF.period_duration(cc_link) == FixedProfile(24) @test EMF.cap_resource(cc_link) == power - @test EMF.get_sub_pers_durations(cc_link, ๐’ฏ) == [4380.0, 4380.0] - @test all(sub_periods[k] == + @test all(collect(๐’ฏแต–แตˆ[k]) == collect(๐’ฏ)[1+12*(k-1):12*k] for k โˆˆ 1:6) - @test all(EMF.get_avg_cap_price(cc_link, sub_periods[k]) โ‰ˆ 5e5 for k โˆˆ 1:2) - @test all(EMF.get_avg_cap_price(cc_link, sub_periods[k]) โ‰ˆ 1e6 for k โˆˆ 3:4) - @test all(EMF.get_avg_cap_price(cc_link, sub_periods[k]) โ‰ˆ 2e6 for k โˆˆ 5:6) + @test all(EMF.get_avg_cap_price(cc_link, ๐’ฏแต–แตˆ[k]) โ‰ˆ 5e5 for k โˆˆ 1:2) + @test all(EMF.get_avg_cap_price(cc_link, ๐’ฏแต–แตˆ[k]) โ‰ˆ 1e6 for k โˆˆ 3:4) + @test all(EMF.get_avg_cap_price(cc_link, ๐’ฏแต–แตˆ[k]) โ‰ˆ 2e6 for k โˆˆ 5:6) # Test that the new functions are also working based on specified durations - capacity_price_periods = [3650.0, 5110.0] + cap_per_dur = PartitionProfile([20, 28]) capacity_price = StrategicProfile([ OperationalProfile(vcat(ones(12) * 5e5, ones(12) * 2e5)), OperationalProfile(vcat(ones(12) * 1e6, ones(12) * 5e5)), OperationalProfile(vcat(ones(12) * 2e6, ones(12) * 1e6)), ]) - m, case, modeltype = capacity_cost_link_case(; capacity_price_periods, capacity_price) + m, case, modeltype = capacity_cost_link_case(; cap_per_dur, capacity_price) cc_link = get_links(case)[2] ๐’ฏ = get_time_struct(case) - sub_periods = EMF.get_sub_periods(cc_link, ๐’ฏ) + ๐’ฏแต–แตˆ = EMF.periods(cc_link, ๐’ฏ) - @test EMF.get_sub_pers_durations(cc_link, ๐’ฏ) == capacity_price_periods - @test all(sub_periods[1+2*(k-1)] == + @test all( + EMF.period_duration(cc_link, t_pd) == cap_per_dur[t_pd] + for t_pd โˆˆ ๐’ฏแต–แตˆ) + @test all(collect(๐’ฏแต–แตˆ[1+2*(k-1)]) == collect(๐’ฏ)[1+24*(k-1):10*k+14*(k-1)] for k โˆˆ 1:3) - @test all(sub_periods[2*k] == + @test all(collect(๐’ฏแต–แตˆ[2*k]) == collect(๐’ฏ)[11+24*(k-1):24*k] for k โˆˆ 1:3) - @test EMF.get_avg_cap_price(cc_link, sub_periods[1]) โ‰ˆ 5e5 - @test EMF.get_avg_cap_price(cc_link, sub_periods[2]) โ‰ˆ (5e5*2 + 2e5*12)/14 + @test EMF.get_avg_cap_price(cc_link, ๐’ฏแต–แตˆ[1]) โ‰ˆ 5e5 + @test EMF.get_avg_cap_price(cc_link, ๐’ฏแต–แตˆ[2]) โ‰ˆ (5e5*2 + 2e5*12)/14 end end @@ -170,7 +171,7 @@ end sink, FixedProfile(10), FixedProfile(1e6), - 2, + 24, power, ) l_data = CapacityCostLink( @@ -179,7 +180,7 @@ end sink, FixedProfile(10), FixedProfile(1e6), - 2, + 24, power, ExtensionData[], ) @@ -189,7 +190,7 @@ end sink, FixedProfile(10), FixedProfile(1e6), - 2, + 24, power, Linear(), ) @@ -199,7 +200,18 @@ end sink, FixedProfile(10), FixedProfile(1e6), - 2, + 24, + power, + Linear(), + ExtensionData[], + ) + l_new = CapacityCostLink( + "Capacity cost link", + src_cheap, + sink, + FixedProfile(10), + FixedProfile(1e6), + FixedProfile(24), power, Linear(), ExtensionData[], @@ -213,7 +225,7 @@ end @testset "Constraint implementation" begin - function ccl_standard_test(cc_link::CapacityCostLink, ๐’ฏ) + function ccl_standard_test(cc_link::CapacityCostLink, ๐’ฏ, ๐’ฏแต–แตˆ) # No losses: link_out == link_in @test all( value(m[:link_out][cc_link, t, p]) โ‰ˆ value(m[:link_in][cc_link, t, p]) @@ -235,23 +247,23 @@ end @test all( all( value(m[:link_in][cc_link, t, power]) โ‰ฒ - value(m[:ccl_cap_use_max][cc_link, t_sub[end]]) - for t โˆˆ t_sub + value(m[:ccl_cap_use_max][cc_link, t_pd]) + for t โˆˆ t_pd ) - for t_sub โˆˆ ๐’ฏหขแต˜แต‡ + for t_pd โˆˆ ๐’ฏแต–แตˆ ) # Capacity cost at end of sub-period: cap_cost == max_cap_use * get_avg_cap_price @test all( - value(m[:ccl_cap_use_cost][cc_link, t_sub[end]]) โ‰ˆ - value(m[:ccl_cap_use_max][cc_link, t_sub[end]]) * EMF.get_avg_cap_price(cc_link, t_sub) - for t_sub โˆˆ ๐’ฏหขแต˜แต‡ + value(m[:ccl_cap_use_cost][cc_link, t_pd]) โ‰ˆ + value(m[:ccl_cap_use_max][cc_link, t_pd]) * EMF.get_avg_cap_price(cc_link, t_pd) + for t_pd โˆˆ ๐’ฏแต–แตˆ ) # Strategic-period sum: link_opex_var == sum(ccl_cap_use_cost over t_inv) @test all( value(m[:link_opex_var][cc_link, t_inv]) โ‰ˆ - sum(value(m[:ccl_cap_use_cost][cc_link, t]) for t โˆˆ t_inv) + sum(value(m[:ccl_cap_use_cost][cc_link, t_pd]) for t_pd โˆˆ EMF.periods(cc_link, t_inv)) for t_inv โˆˆ ๐’ฏแดตโฟแต› ) @@ -272,16 +284,20 @@ end src_cheap, src_exp, sink = get_nodes(case) direct_link, cc_link = get_links(case) ๐’ฏ = get_time_struct(case) - ๐’ฏหขแต˜แต‡ = EMF.get_sub_periods(cc_link, ๐’ฏ) + ๐’ฏแต–แตˆ = EMF.periods(cc_link, ๐’ฏ) ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) # Perform the standard tests - ccl_standard_test(cc_link, ๐’ฏ) + ccl_standard_test(cc_link, ๐’ฏ, ๐’ฏแต–แตˆ) + + # Test that the variables are created correctly + @test length(m[:ccl_cap_use_max][cc_link, :]) == 6 + @test length(m[:ccl_cap_use_cost][cc_link, :]) == 6 # Check that the `CapacityCostLink` is only used up to a capacity of 1.0 to limit # the opex on the line (the remaining demand is covered by the `Direct` link) - @test all(value.(m[:link_out][direct_link, t, power]) โ‰ˆ 0.0 for t โˆˆ ๐’ฏหขแต˜แต‡[2]) - @test all(value.(m[:link_out][cc_link, t, power]) โ‰ˆ 1.0 for t โˆˆ ๐’ฏหขแต˜แต‡[1]) + @test all(value.(m[:link_out][direct_link, t, power]) โ‰ˆ 0.0 for t โˆˆ ๐’ฏแต–แตˆ[2]) + @test all(value.(m[:link_out][cc_link, t, power]) โ‰ˆ 1.0 for t โˆˆ ๐’ฏแต–แตˆ[1]) # Check that the opex of the CapacityCostLink is correct # Sp1: A capacity of 1.0 is used over both sub periods (having an opex of 5e5 each) @@ -301,14 +317,14 @@ end @test all(value.(m[:opex_var][src_exp, t_inv]) โ‰ˆ exp_val[t_inv] for t_inv โˆˆ ๐’ฏแดตโฟแต›) # Create the case and modeltype when specifiying the duration of the periods - capacity_price_periods = [3650.0, 5110.0] + cap_per_dur = PartitionProfile([20, 28]) capacity_price = StrategicProfile([ OperationalProfile(vcat(ones(12) * 1e5, ones(12) * 2e5)), OperationalProfile(vcat(ones(12) * 1e6, ones(12) * 5e5)), OperationalProfile(vcat(ones(12) * 2e6, ones(12) * 1e6)), ]) - m, case, modeltype = capacity_cost_link_case(; capacity_price_periods, capacity_price) + m, case, modeltype = capacity_cost_link_case(; cap_per_dur, capacity_price) # Optimize the model and conduct the general tests set_optimizer(m, OPTIMIZER) @@ -319,17 +335,17 @@ end src_cheap, src_exp, sink = get_nodes(case) direct_link, cc_link = get_links(case) ๐’ฏ = get_time_struct(case) - ๐’ฏหขแต˜แต‡ = EMF.get_sub_periods(cc_link, ๐’ฏ) + ๐’ฏแต–แตˆ = EMF.periods(cc_link, ๐’ฏ) ๐’ฏแดตโฟแต› = strategic_periods(๐’ฏ) # Perform the standard tests - ccl_standard_test(cc_link, ๐’ฏ) + ccl_standard_test(cc_link, ๐’ฏ, ๐’ฏแต–แตˆ) # Check that the `CapacityCostLink` is only used up to a capacity of 1.0 in sp3, sub # periods 1 to limit the opex on the line (the remaining demand is covered by the `Direct` link) # The direct link is not used in sp1 due to the reduced costs - @test all(value.(m[:link_out][direct_link, t, power]) โ‰ˆ 0.0 for k โˆˆ [1,2,4] for t โˆˆ ๐’ฏหขแต˜แต‡[k]) - @test all(value.(m[:link_out][cc_link, t, power]) โ‰ˆ 1.0 for t โˆˆ ๐’ฏหขแต˜แต‡[3]) + @test all(value.(m[:link_out][direct_link, t, power]) โ‰ˆ 0.0 for k โˆˆ [1,2,4] for t โˆˆ ๐’ฏแต–แตˆ[k]) + @test all(value.(m[:link_out][cc_link, t, power]) โ‰ˆ 1.0 for t โˆˆ ๐’ฏแต–แตˆ[3]) # Check that the opex of the expensive source is correct # Sp1: A capacity of 9.0 is used in the first sub period (having an opex of 1e5) and A From c12b983d5baabae0d3c3cb44beb5868d4e78c339 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Sun, 28 Jun 2026 16:55:23 +0200 Subject: [PATCH 2/4] Updated the checks for the new format --- src/link/checks.jl | 64 +++++++++--------------------- test/link/test_CapacityCostLink.jl | 16 ++------ 2 files changed, 22 insertions(+), 58 deletions(-) diff --git a/src/link/checks.jl b/src/link/checks.jl index 7870356..7d0c112 100644 --- a/src/link/checks.jl +++ b/src/link/checks.jl @@ -6,59 +6,33 @@ This method checks that the *[`CapacityCostLink`](@ref)* link is valid. ## Checks - The field `cap` is required to be non-negative. - The field `cap_price` is required to be non-negative. -- The field `check_cap_price_periods` must follow the rules oulined in the subfunction - [`check_cap_price_periods`](@ref), that is it must either be a positive number or the - durations must sum up to the value `op_per_strat` of the time structure. +- The individual `period_duration`s must all satisfy the specified duration(s) and be + positive. """ function EMB.check_link(l::CapacityCostLink, ๐’ฏ, ::EnergyModel, ::Bool) + ๐’ฏแต–แตˆ = periods(l, ๐’ฏ) + @assert_or_log( all(capacity(l, t) โ‰ฅ 0 for t โˆˆ ๐’ฏ), "The capacity must be non-negative." ) @assert_or_log( all(cap_price(l)[t] โ‰ฅ 0 for t โˆˆ ๐’ฏ), - "The capacity price must be non-negative." + "The capacity prices must be non-negative." ) - # check_cap_price_periods(l, ๐’ฏ, cap_price_periods(l)) -end - -""" - check_cap_price_periods(l::CapacityCostLink, ๐’ฏ, price_pers::Int64) - check_cap_price_periods(l::CapacityCostLink, ๐’ฏ, price_pers::Vector{<:Number}) - -This method checks that the field `check_cap_price_periods` is correct. -## Checks -- If the field is an `Int64`, the field `check_cap_price_periods` is required to be positive. -- If the field is an `Vector{<:Number}`, the field `check_cap_price_periods` is required to sum - up to the value `op_per_strat` and each value in the `Vector` must be positive. -- The individual capacity price periods must be able to represent the operational time - structure -""" -function check_cap_price_periods(l::CapacityCostLink, ๐’ฏ, price_pers::Int64) - @assert_or_log( - price_pers > 0, - "The number of sub periods of a strategic period must be positive." - ) - price_pers > 0 && @assert_or_log( - vcat(get_sub_periods(l, ๐’ฏ)...) == collect(๐’ฏ), - "The operational period durations could not accumulate into `cap_price_periods = - $(price_pers)` sub periods of each strategic period." - ) -end -function check_cap_price_periods(l::CapacityCostLink, ๐’ฏ, price_pers::Vector{<:Number}) - @assert_or_log( - sum(price_pers) โ‰ˆ ๐’ฏ.op_per_strat, - "The duration of all sub periods must be equal to the value `op_per_strat` of the " * - "time structure." - ) - @assert_or_log( - all(price_pers .> 0), - "Each sub period must have a positive duration." - ) - @assert_or_log( - vcat(get_sub_periods(l, ๐’ฏ)...) == collect(๐’ฏ), - "The operational period durations could not accumulate into `cap_price_periods = - $(length(price_pers))` sub periods of each strategic period." - ) + message = "are not allowed for the field `:period_duration`." + bool = EMB.check_partition_profile(period_duration(l), message) + if bool + @assert_or_log( + all(sum(duration(t) for t โˆˆ t_pd) โ‰ฅ period_duration(l, t_pd) for t_pd โˆˆ ๐’ฏแต–แตˆ), + "The duration of the last period on the `SimpleTimes` level is shorter than " * + "specified. This is caused by inconsistently specified `period_duration` and" * + "time structure." + ) + @assert_or_log( + all(period_duration(l, t_pd) > 0 for t_pd โˆˆ ๐’ฏแต–แตˆ), + "Each value in the field `period_duration` must be positive." + ) + end end diff --git a/test/link/test_CapacityCostLink.jl b/test/link/test_CapacityCostLink.jl index 9d3243b..e30583e 100644 --- a/test/link/test_CapacityCostLink.jl +++ b/test/link/test_CapacityCostLink.jl @@ -77,21 +77,11 @@ end capacity_price = StrategicProfile([-1e5, 1e6, 2e6]) @test_throws AssertionError capacity_cost_link_case(; capacity_price) - # Test that the number of sub periods is positive - @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = 0) - @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = -1) - - # Test that operational time structure can can be represented by `cap_price_periods` - # sub periods when specified as `Int64` (8760 is not divisible by 7 sub periods) - @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = 7) - - # Test that the sum of sub periods is equal to the operational time structure duration - cap_per_dur = ones(24) - @test_throws AssertionError capacity_cost_link_case(; cap_per_dur) + # Test that operational time structure can can be represented by `period_duration` + @test_throws AssertionError capacity_cost_link_case(; cap_per_dur = 9) # Test that none of the durations of the sub periods is not positive - cap_per_dur = - [0.0, 4.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0] * 365 + cap_per_dur = [0.0, 8.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, 8.0, 4.0, 4.0] @test_throws AssertionError capacity_cost_link_case(; cap_per_dur) # Set the global to true to suppress the error message From e405618a0bef52102359c9b252da3c25f0fee6f3 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Sun, 28 Jun 2026 17:21:45 +0200 Subject: [PATCH 3/4] Updated the documentation and README.md --- NEWS.md | 7 +- README.md | 3 +- docs/make.jl | 1 + docs/src/how-to/update-models.md | 37 +++++++++ docs/src/library/internals/methods-EMF.md | 3 - docs/src/library/internals/methods-fields.md | 21 ++--- docs/src/links/capacitycostlink.md | 85 +++++++++++++++----- src/link/datastructures.jl | 4 +- 8 files changed, 125 insertions(+), 36 deletions(-) diff --git a/NEWS.md b/NEWS.md index ecc4ab4..56f5187 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,9 +6,14 @@ #### Rework of `PeriodDemandSink` -* Rewrote `PeriodDemandSink` with `PeriodPartition` (introduced in `TimeStruct` 0.9.12) to increase flexibility of node with respect to the time structure. +* Rewrote `PeriodDemandSink` with `PeriodPartition` (introduced in *[`TimeStruct` 0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)*) to increase flexibility of the node with respect to the time structure. * Rewriting changed input arguments as well as behavior of the node. +#### Rework of `CapacityCostLink` + +* Rewrote `CapacityCostLink` with `PeriodPartition` (introduced in *[`TimeStruct` 0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)*) to increase flexibility of the link with respect to the time structure. +* Rewriting requires adjustment of the parameters due to changed meaning of some of the values. + ## Version 0.3.0 (2026-04-16) ### Breaking changes diff --git a/README.md b/README.md index c176a85..2f439a6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ > As a consequence, it is advised to read the documentation for each node to identify their usefulness. > Is is planned to remove some nodes and rewrite the behaviour of other nodes to improve their flexibility. > -> Among others, using `PeriodDemandSink` in combination with `EnergyModelsGUI` results in errors when trying to access field values with `PartitionProfile`. +> Among others, using `PeriodDemandSink` and `CapacityCostLink` in combination with `EnergyModelsGUI` results in errors when trying to access fields that have as values `PartitionProfile`. +> The same holds for variables that are defined over `PeriodPartition`s where you cannot see the results. ## Usage diff --git a/docs/make.jl b/docs/make.jl index a86e54d..00b8a72 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -7,6 +7,7 @@ using Literate const EMB = EnergyModelsBase const EMF = EnergyModelsFlex +const TS = TimeStruct DocMeta.setdocmeta!( EnergyModelsFlex, diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md index 36228a3..62535aa 100644 --- a/docs/src/how-to/update-models.md +++ b/docs/src/how-to/update-models.md @@ -6,6 +6,8 @@ This document is designed to provide users with information regarding how they h ## [Adjustments from 0.3.0](@id how_to-update-03) +### [Changed `PeriodDemandSink`](@id how_to-update-03-PeriodDemandSink) + The introduction of `PartitionProfile` in *[`TimeStruct` v0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)* allowed a rewrite of `PeriodDemandSink`: ```julia @@ -31,3 +33,38 @@ PeriodDemandSink( data ) ``` + +### [Changed `CapacityCostLink`](@id how_to-update-03-CapacityCostLink) + +The introduction of `PartitionProfile` in *[`TimeStruct` v0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)* allowed a rewrite of `CapacityCostLink` which changed the model behavior: + +The meaning of the field `cap_period_duration` was changed when moving from 0.3 to 0.4: + +1. When specifying a number, the previous meaning of the number of operational periods was changed to the sum of the durations of the operational periods. + The reason for this change is to make the link behavior less dependent on the operational resolution. + The following change is hence required if you have operational durations differing from 1: + + ```julia + # time structure + ts = SimpleTimes(10, 2) + + # old behavior, corresponding to 2 periods + cap_period_duration = 2 + + # new behavior, corresponding to periods whocse duration sums to at least 4 + cap_period_duration = 4 + ``` + +2. When specifying a vector, the previous scaling based on the chosen value of `op_per_strat` was removed as it is in our opinion more straightforward to base it on the actual operational time structure. + The following change is hence required: + + ```julia + # time structure + ts = Twolevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0) + + # old behavior, corresponding to 5 periods a 1752 duration based on `op_per_strat` + cap_period_duration = [1752, 1752, 1752, 1752, 1752] + + # new behavior, corresponding to 5 periods a 4 duration based on `SimpleTimes` + cap_period_duration = [4, 4, 4, 4, 4] + ``` diff --git a/docs/src/library/internals/methods-EMF.md b/docs/src/library/internals/methods-EMF.md index 5be9842..23ea6a8 100644 --- a/docs/src/library/internals/methods-EMF.md +++ b/docs/src/library/internals/methods-EMF.md @@ -11,13 +11,10 @@ Pages = ["methods-EMF.md"] ```@docs EMF.check_limits_default EMF.check_input -EMF.check_cap_price_periods ``` ## [Utility functions](@id lib-int-links-fun_utils) ```@docs EMF.get_avg_cap_price -EMF.get_sub_periods -EMF.get_sub_pers_durations ``` diff --git a/docs/src/library/internals/methods-fields.md b/docs/src/library/internals/methods-fields.md index 5b6bf68..9d8a17e 100644 --- a/docs/src/library/internals/methods-fields.md +++ b/docs/src/library/internals/methods-fields.md @@ -10,29 +10,30 @@ Pages = ["methods-fields.md"] ## [`PeriodDemandSink` types](@id lib-int-met_field-PeriodDemandSink) ```@docs -EnergyModelsFlex.period_demand -EnergyModelsFlex.period_duration -EnergyModelsFlex.periods -EnergyModelsFlex.number_of_periods +EMF.period_demand +EMF.period_duration(n::EMF.AbstractPeriodDemandSink) +EMF.periods(n::EMF.AbstractPeriodDemandSink, ts::TS.TimeStructure) +EMF.number_of_periods ``` ## [`ActivationCostNode` types](@id lib-int-met_field-ActivationCostNode) ```@docs -EnergyModelsFlex.activation_consumption +EMF.activation_consumption ``` ## [`CapacityCostLink` types](@id lib-int-met_field-CapacityCostLink) ```@docs -EnergyModelsFlex.cap_price_periods -EnergyModelsFlex.cap_resource -EnergyModelsFlex.cap_price +EMF.cap_price +EMF.period_duration(l::CapacityCostLink) +EMF.periods(l::CapacityCostLink, ts::TS.TimeStructure) +EMF.cap_resource ``` ## [`Combustion` types](@id lib-int-met_field-Combustion) ```@docs -EnergyModelsFlex.limits -EnergyModelsFlex.heat_resource +EMF.limits +EMF.heat_resource ``` diff --git a/docs/src/links/capacitycostlink.md b/docs/src/links/capacitycostlink.md index a4f25c2..55fded8 100644 --- a/docs/src/links/capacitycostlink.md +++ b/docs/src/links/capacitycostlink.md @@ -6,15 +6,48 @@ This is useful for applications such as transmission networks, pipelines, or int In addition, they only allow the transport of a single, specified [`Resource`](@extref EnergyModelsBase.Resource). +!!! warning "`CapacityCostLink` and `EnergyModelsGUI`" + Some of the fields of this link cannot be represented in `EnergyModelsGUI`. + The reason for that limitation is that `EnergyModelsGUI` does not yet support partitions of `TimePeriod`s. + `EnergyModelsGUI` can still be utilized for all other fields. + +!!! warning "Changed behavior between 0.3 and 0.4" + The meaning of the field `cap_period_duration` was changed when moving from 0.3 to 0.4: + + 1. When specifying a number, the previous meaning of the number of operational periods was changed to the sum of the durations of the operational periods. + The reason for this change is to make the link behavior less dependent on the operational resolution. + The following change is hence required if you have operational durations differing from 1: + + ```julia + # time structure + ts = SimpleTimes(10, 2) + + # old behavior, corresponding to 2 periods + cap_period_duration = 2 + + # new behavior, corresponding to periods whocse duration sums to at least 4 + cap_period_duration = 4 + ``` + + 2. When specifying a vector, the previous scaling based on the chosen value of `op_per_strat` was removed as it is in our opinion more straightforward to base it on the actual operational time structure. + The following change is hence required: + + ```julia + # time structure + ts = Twolevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0) + + # old behavior, corresponding to 5 periods a 1752 duration based on `op_per_strat` + cap_period_duration = [1752, 1752, 1752, 1752, 1752] + + # new behavior, corresponding to 5 periods a 4 duration based on `SimpleTimes` + cap_period_duration = [4, 4, 4, 4, 4] + ``` + ## [Introduced type and its fields](@id links-CapacityCostLink-fields) [`CapacityCostLink`](@ref) is implemented as equivalent to an abstract type [`Link`](@extref EnergyModelsBase.Link). Hence, it utilizes the same functions declared in `EnergyModelsBase`. -!!! warning "Application of the link" - The current implementation is not very flexible with respect to the chosen time structure. - Specifically, if you use [`OperationalScenarios`](@extref TimeStruct.OperationalScenarios), [`RepresentativePeriods`](@extref TimeStruct.RepresentativePeriods), or differing operational structures within your [`TwoLevel`](@extref TimeStruct.TwoLevel), you must be careful when choosing the parameter `cap_price_periods`. - ### [Standard fields](@id links-CapacityCostLink-fields-stand) [`CapacityCostLink`](@ref) has the following standard fields, equivalent to a [`Direct`](@extref EnergyModelsBase.Direct) link: @@ -50,19 +83,33 @@ The following additional fields are included for [`CapacityCostLink`](@ref) link Capacity costs are calculated per sub-period and then summed over the strategic period. This means a constant value (*e.g.,* โ‚ฌ/GW/year) is effectively applied once for each sub-period (based on the peak within that sub-period), and is not automatically scaled by sub-period duration. - Example: With `cap_price = 100 โ‚ฌ/GW/year`, `cap_price_periods = 12`, and a peak usage of 1 GW in each month, the model computes a total cost of 12 ร— 100 = 1200 โ‚ฌ/year. + **Example:** + + ```julia + # Modelling a full year with hourly resolution + ts = TwoLevel(1, 1, SimpleTimes(8760, 1); op_per_strat=8760.0) + + # 12 price periods corresponding to months + cap_price_periods = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] .* 24 + + cap_price = 100 # โ‚ฌ/GW/year + ``` + + If the peak usage is 1 GW in each month, the model computes a total cost of 12 ร— 100 = 1200 โ‚ฌ/year. To achieve seasonal/monthly peak charges, define multiple `cap_price_periods` and provide `cap_price` values that represent the intended charge per sub-period (or scale the values accordingly). It is planned to change this behavior in the future. The change corresponds to a breaking change as we change the behavior of the model. -- **`cap_price_periods::Union{Int64, Vector{<:Number}}`** :\ - The number of sub-periods within a year for which the capacity cost is calculated (if specifying an `Int64`) or the duration of the individual sub periods (if specifying a `Vector{<:Number}`). - This allows modeling of varying peak demands across seasons. - The value must be positive if your are using an `Int64` (and hence specifiy the number of periods) or all values of be positive and summing up to the specified scaling factor between operational and strategic period durations (the parameter `op_per_strat`) if you are using a `Vector{<:Number}`. - - !!! tip "Number of sub-periods" - For investment periods with many operational periods, consider increasing the number of `cap_price_periods`. +- **`cap_period_duration::TimeProfile`** :\ + Defines the total duration of a capacity price period.\ + For instance, if the duration of 1 of the operational time structure is 1 hour and `period_duration = FixedProfile(24)`, then each capacity price period spans one day. + The capacity price of this node (for a given day, see below) is the given by the maximum capacity usage within a day.\ + Due to a constructor, it can either be specified as number (the same duration in all capacity price periods), as a vector (varying duration of each capacity price period), or as a time profile (*e.g.*, varying period durations due to varying operational time structures). + It cannot be specified as `OperationalProfile` and must be positive for each individual value. + + !!! tip "Duration of capacity price periods" + For investment periods with many operational periods, consider decreasing `cap_period_duration`. The [`CapacityCostLink`](@ref) capacity constraints couple operational periods and can significantly increase solve time. Splitting the horizon into multiple sub-periods reduces this coupling and often makes the problem much easier to solve. In some cases, this also means using more than one capacity price period even if capacity costs occur only annually in reality, depending on model size and complexity. @@ -103,8 +150,8 @@ with parantheses. Two additional variables track capacity utilization and associated costs over sub-periods: -- ``\texttt{ccl\_cap\_use\_max}[l, t_{sub}]``: Maximum capacity usage in sub-period ``t_{sub}`` for link ``l``. -- ``\texttt{ccl\_cap\_use\_cost}[l, t_{sub}]``: Operational cost in sub-period ``t_{sub}`` for link ``l``. +- ``\texttt{ccl\_cap\_use\_max}[l, t_{pd}]``: Maximum capacity usage in sub-period ``t_{pd}`` for link ``l``. +- ``\texttt{ccl\_cap\_use\_cost}[l, t_{pd}]``: Operational cost in sub-period ``t_{pd}`` for link ``l``. ### [Constraints](@id links-CapacityCostLink-math-con) @@ -126,28 +173,28 @@ and the no-loss constraint All additional constraints are created within a new method for the function [`create_link`](@extref EnergyModelsBase.create_link). -The capacity utilization constraint tracks the maximum usage within each sub-period: +The capacity utilization constraint tracks the maximum usage within each sub-period ``t_{sub}``: ```math -\texttt{link\_in}[l, t, cap\_resource(l)] \leq \texttt{ccl\_cap\_use\_max}[l, t_{sub}] +\texttt{link\_in}[l, t, cap\_resource(l)] \leq \texttt{ccl\_cap\_use\_max}[l, t_{pd}] ``` The capacity cost is calculated as: ```math -\texttt{ccl\_cap\_use\_cost}[l, t_{sub}] = \texttt{ccl\_cap\_use\_max}[l, t_{sub}] \times \overline{cap\_price}(l, t_{sub}) +\texttt{ccl\_cap\_use\_cost}[l, t_{pd}] = \texttt{ccl\_cap\_use\_max}[l, t_{pd}] \times \overline{cap\_price}(l, t_{pd}) ``` where ``\overline{cap\_price}`` is the average capacity price over the sub-period calculated as: ```math -\overline{cap\_price}(l, t_{sub}) = \frac{\sum_{t \in t_{sub}} cap\_price(l, t) \times duration(t)}{\sum_{t \in t_{sub}} duration(t)} +\overline{cap\_price}(l, t_{pd}) = \frac{\sum_{t \in t_{pd}} cap\_price(l, t) \times duration(t)}{\sum_{t \in t_{pd}} duration(t)} ``` Finally, costs are aggregated to each strategic period: ```math -\texttt{link\_opex\_var}[l, t_{inv}] = \sum_{t_{sub} \in t_{inv}} \texttt{ccl\_cap\_use\_cost}[l, t_{sub}] +\texttt{link\_opex\_var}[l, t_{inv}] = \sum_{t_{pd} \in t_{inv}} \texttt{ccl\_cap\_use\_cost}[l, t_{pd}] ``` In addition, the energy flow of the constrained resource should not exceed the maximum capacity, which is included through the following constraint: diff --git a/src/link/datastructures.jl b/src/link/datastructures.jl index bf80ca3..c36375f 100644 --- a/src/link/datastructures.jl +++ b/src/link/datastructures.jl @@ -185,9 +185,9 @@ cap_price(l::CapacityCostLink) = l.cap_price cap_price(l::CapacityCostLink, t::TS.TimePeriod) = l.cap_price[t] """ - period_duration(n::CapacityCostLink) + period_duration(l::CapacityCostLink) -Returns the prices periods of `CapacityCostLink` `n` as `TimeProfile` or in price +Returns the prices periods of `CapacityCostLink` `l` as `TimeProfile` or in price period `t_pd`. """ period_duration(l::CapacityCostLink) = l.cap_period_duration From 3f26d00241488eeaf8e293f9a92dac59545e831c Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Wed, 1 Jul 2026 08:33:04 +0200 Subject: [PATCH 4/4] Included changed from review --- docs/src/how-to/update-models.md | 49 +++++++++++++++++++++++++------- src/legacy_constructors.jl | 2 +- src/link/datastructures.jl | 9 +++--- src/sink/datastructures.jl | 5 ++++ 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/docs/src/how-to/update-models.md b/docs/src/how-to/update-models.md index 62535aa..6bfaeb9 100644 --- a/docs/src/how-to/update-models.md +++ b/docs/src/how-to/update-models.md @@ -34,37 +34,66 @@ PeriodDemandSink( ) ``` +!!! note "`period_length` meaning" + The meaning of `period_length` has changed in addition to its name. + While it previously defined the number of periods, it defines in the new implementation the total duration of the operational periods that are within a single demand period. + Consider the following example in which we want three demand periods consisting of two operational periods each: + + ```julia + # Time structure with 1 strategic period of duration 1 and 6 operational periods of duration 2 + ๐’ฏ = TwoLevel(1, 1, SimpleTimes(6, 2)) + + # OLD: Previously, we had to specify + period_length = 2 + # implying 2 operational periods + + # NEW: Now, we have to specify + period_length = 4 + # implying a total duration of 4 for the 2 periods + ``` + + THis is reflected by the renaming from `period_length` to `period_duration`. + ### [Changed `CapacityCostLink`](@id how_to-update-03-CapacityCostLink) The introduction of `PartitionProfile` in *[`TimeStruct` v0.9.12](https://github.com/sintefore/TimeStruct.jl/releases/tag/v0.9.12)* allowed a rewrite of `CapacityCostLink` which changed the model behavior: -The meaning of the field `cap_period_duration` was changed when moving from 0.3 to 0.4: +The field `cap_price_periods` was renamed to `cap_period_duration` and its meaning was changed when moving from 0.3 to 0.4. +The reason for this change is to make the link behavior less dependent on the operational resolution. +The following updated must hence be performed as it is not possible to create respective constructor methods: 1. When specifying a number, the previous meaning of the number of operational periods was changed to the sum of the durations of the operational periods. - The reason for this change is to make the link behavior less dependent on the operational resolution. - The following change is hence required if you have operational durations differing from 1: + The following change is hence required if you have operational durations differing from `1`: ```julia # time structure - ts = SimpleTimes(10, 2) + ts = TwoLevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0) - # old behavior, corresponding to 2 periods - cap_period_duration = 2 + # old behavior, corresponding to 5 periods + cap_price_periods = 5 - # new behavior, corresponding to periods whocse duration sums to at least 4 + # new behavior, corresponding to periods whose duration sums to at least 4 cap_period_duration = 4 ``` + Both cases create five price periods with a duration of `4`, but instead of specifying the number of periods periods, we now define the duration of each price period. + 2. When specifying a vector, the previous scaling based on the chosen value of `op_per_strat` was removed as it is in our opinion more straightforward to base it on the actual operational time structure. The following change is hence required: ```julia # time structure - ts = Twolevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0) + ts = TwoLevel(2, 1, SimpleTimes(10, 2); op_per_strat=8760.0) - # old behavior, corresponding to 5 periods a 1752 duration based on `op_per_strat` + # old behavior, corresponding to 5 periods of a total duration of duration based on `op_per_strat` cap_period_duration = [1752, 1752, 1752, 1752, 1752] - # new behavior, corresponding to 5 periods a 4 duration based on `SimpleTimes` + # new behavior, corresponding to 5 periods of a total duration of 4 based on `SimpleTimes` cap_period_duration = [4, 4, 4, 4, 4] ``` + + With the scaling factor ``8760 / (10 * 2) = 438``, each new value of 4 corresponds to the previously used value of ``4 * 438 = 1752``, *i.e.*, the same five partitions of two time periods each. + +!!! note + Both examples above result in exactly the same behavior. + They illustrate the different approaches that could and can be used for defining a [`CapacityCostLink`](@ref). diff --git a/src/legacy_constructors.jl b/src/legacy_constructors.jl index 8c51571..e4c191d 100644 --- a/src/legacy_constructors.jl +++ b/src/legacy_constructors.jl @@ -18,7 +18,7 @@ function PeriodDemandSink( "3สณแตˆ position, and can accept as well a `Vector` or `TimeProfile`s as input, and\n" * " 3. `period_demand` is moved to the 4แต—สฐ position and requires as input a " * "`PartitionProfile` of the previously provided `Vector`.\n" * - "See the documentation (https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/how-to/update-models/#Adjustments-from-0.3.0) " * + "See the documentation (https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/how-to/update-models/03/PeriodDemandSink) " * "on how to update your model to the latest version.", maxlog = 1 ) diff --git a/src/link/datastructures.jl b/src/link/datastructures.jl index c36375f..18893d1 100644 --- a/src/link/datastructures.jl +++ b/src/link/datastructures.jl @@ -22,11 +22,10 @@ within specified price periods. All other resources have no costs associated wit - **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.*, for investments). The field `data` is conditional through usage of a constructor. -!!! note "Sub periods" - You can specify either the total number of sub periods within a `CapacityCostLink` as - `Int64` or the durations of each sub period if using a `Vector{<:Number}`. The latter - requires you to be careful when considering the durations of the individual sub periods - and the total duration of sub periods within the operational time structure. +!!! note "Changed behavior" + The field `cap_price_periods` was replaced with the field `cap_period_duration` with a + change in meaning. This is explained in the + *[documentation](https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/how-to/update-models/03/CapacityCostLink)*. """ struct CapacityCostLink <: EMB.Link id::Any diff --git a/src/sink/datastructures.jl b/src/sink/datastructures.jl index 6757cd6..9785f25 100644 --- a/src/sink/datastructures.jl +++ b/src/sink/datastructures.jl @@ -47,6 +47,11 @@ each operational period. with conversion value `Real`. - **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.*, for investments). The field `data` is conditional through usage of a constructor. + +!!! note "Changed behavior" + The field `period_length` was replaced with the field `period_duration` with a + change in meaning. In addition, the position was changed. This is explained in the + *[documentation](https://energymodelsx.github.io/EnergyModelsFlex.jl/stable/how-to/update-models/03/PeriodDemandSink)*. """ struct PeriodDemandSink <: AbstractPeriodDemandSink id::Any