Skip to content

Commit 50c6a4d

Browse files
authored
Improvement to CapacityCostLink (#8)
* Modified `CapacityCostLink` to allow for `Vector{<:Number}` * Represents sub periods with different durations
1 parent 4802a91 commit 50c6a4d

8 files changed

Lines changed: 295 additions & 111 deletions

File tree

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release notes
22

3+
## Version 0.2.11 (2026-02-16)
4+
5+
### Enhancements
6+
7+
* Extended the link `CapacityCostLink` to also accept a vector of durations for the individual sub periods.
8+
39
## Version 0.2.10 (2026-01-05)
410

511
### Enhancements

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "EnergyModelsFlex"
22
uuid = "a81b9388-333d-4b63-81f2-910b060b544c"
33
authors = ["Sigrid Aunsmo, Sigmund Eggen Holm, Jon Vegard Venås, and Per Åslid"]
4-
version = "0.2.10"
4+
version = "0.2.11"
55

66
[deps]
77
EnergyModelsBase = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"

docs/src/library/internals/methods-EMF.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ Pages = ["methods-EMF.md"]
99
## [Check methods](@id lib-int-met-check)
1010

1111
```@docs
12-
EnergyModelsFlex.check_period_ts
13-
EnergyModelsFlex.check_limits_default
14-
EnergyModelsFlex.check_input
12+
EMF.check_period_ts
13+
EMF.check_limits_default
14+
EMF.check_input
15+
EMF.check_cap_price_periods
1516
```
1617

1718
## [Utility functions](@id lib-int-links-fun_utils)
1819

1920
```@docs
20-
EnergyModelsFlex.avg_cap_price
21-
EnergyModelsFlex.create_sub_periods
21+
EMF.get_avg_cap_price
22+
EMF.get_sub_periods
23+
EMF.get_sub_pers_durations
2224
```

docs/src/links/capacitycostlink.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,22 @@ The following additional fields are included for [`CapacityCostLink`](@ref) link
4444
The price per unit of maximum capacity usage over the sub-periods.
4545
This value is averaged over sub-periods as defined by `cap_price_periods`.
4646
All values have to be non-negative.
47-
- **`cap_price_periods::Int64`** :\
48-
The number of sub-periods within a year for which the capacity cost is calculated.
47+
!!! danger "Price values"
48+
49+
The value given in `cap_price` is interpreted on the strategic-period scale (*e.g.*, if a strategic-period duration of `1` corresponds to 1 year, then the natural unit is €/GW/year).
50+
Capacity costs are calculated per sub-period and then summed over the strategic period.
51+
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.
52+
53+
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.
54+
55+
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).
56+
57+
It is planned to change this behavior in the future.
58+
The change corresponds to a breaking change as we change the behavior of the model.
59+
- **`cap_price_periods::Union{Int64, Vector{<:Number}}`** :\
60+
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}`).
4961
This allows modeling of varying peak demands across seasons.
50-
The value must be positive.
62+
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}`.
5163

5264
!!! tip "Number of sub-periods"
5365
For investment periods with many operational periods, consider increasing the number of `cap_price_periods`.
@@ -126,15 +138,19 @@ The capacity cost is calculated as:
126138
\texttt{ccl\_cap\_use\_cost}[l, t_{sub}] = \texttt{ccl\_cap\_use\_max}[l, t_{sub}] \times \overline{cap\_price}(l, t_{sub})
127139
```
128140

129-
where ``\overline{cap\_price}`` is the average capacity price over the sub-period.
141+
where ``\overline{cap\_price}`` is the average capacity price over the sub-period calculated as:
142+
143+
```math
144+
\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)}
145+
```
130146

131147
Finally, costs are aggregated to each strategic period:
132148

133149
```math
134150
\texttt{link\_opex\_var}[l, t_{inv}] = \sum_{t_{sub} \in t_{inv}} \texttt{ccl\_cap\_use\_cost}[l, t_{sub}]
135151
```
136152

137-
In addition, the energy flow of the constrained resource should not exceed the maximum pipe capacity, which is included through the following constraint:
153+
In addition, the energy flow of the constrained resource should not exceed the maximum capacity, which is included through the following constraint:
138154

139155
```math
140156
\texttt{flow\_in}[l, t, cap\_resource(l)] \leq \texttt{link\_cap\_inst}[l, t]

src/link/checks.jl

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
This method checks that the *[`CapacityCostLink`](@ref)* link is valid.
55
66
## Checks
7-
- The field `cap` is required to be non-negative.
8-
- The field `cap_price` is required to be non-negative.
9-
- The field `cap_price_period` is required to be positive.
7+
- The field `cap` is required to be non-negative.
8+
- The field `cap_price` is required to be non-negative.
9+
- The field `check_cap_price_periods` must follow the rules oulined in the subfunction
10+
[`check_cap_price_periods`](@ref), that is it must either be a positive number or the
11+
durations must sum up to the value `op_per_strat` of the time structure.
1012
"""
1113
function EMB.check_link(l::CapacityCostLink, 𝒯, ::EnergyModel, ::Bool)
1214
@assert_or_log(
@@ -17,14 +19,46 @@ function EMB.check_link(l::CapacityCostLink, 𝒯, ::EnergyModel, ::Bool)
1719
all(cap_price(l)[t] 0 for t 𝒯),
1820
"The capacity price must be non-negative."
1921
)
22+
check_cap_price_periods(l, 𝒯, cap_price_periods(l))
23+
end
24+
25+
"""
26+
check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Int64)
27+
check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Vector{<:Number})
28+
29+
This method checks that the field `check_cap_price_periods` is correct.
30+
31+
## Checks
32+
- If the field is an `Int64`, the field `check_cap_price_periods` is required to be positive.
33+
- If the field is an `Vector{<:Number}`, the field `check_cap_price_periods` is required to sum
34+
up to the value `op_per_strat` and each value in the `Vector` must be positive.
35+
- The individual capacity price periods must be able to represent the operational time
36+
structure
37+
"""
38+
function check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Int64)
39+
@assert_or_log(
40+
price_pers > 0,
41+
"The number of sub periods of a strategic period must be positive."
42+
)
43+
price_pers > 0 && @assert_or_log(
44+
vcat(get_sub_periods(l, 𝒯)...) == collect(𝒯),
45+
"The operational period durations could not accumulate into `cap_price_periods =
46+
$(price_pers)` sub periods of each strategic period."
47+
)
48+
end
49+
function check_cap_price_periods(l::CapacityCostLink, 𝒯, price_pers::Vector{<:Number})
50+
@assert_or_log(
51+
sum(price_pers) 𝒯.op_per_strat,
52+
"The duration of all sub periods must be equal to the value `op_per_strat` of the " *
53+
"time structure."
54+
)
2055
@assert_or_log(
21-
cap_price_periods(l) > 0,
22-
"The the number of sub periods of a year must be positive."
56+
all(price_pers .> 0),
57+
"Each sub period must have a positive duration."
2358
)
24-
sub_periods = create_sub_periods(l, 𝒯)
2559
@assert_or_log(
26-
vcat(sub_periods...) == collect(𝒯),
60+
vcat(get_sub_periods(l, 𝒯)...) == collect(𝒯),
2761
"The operational period durations could not accumulate into `cap_price_periods =
28-
$(cap_price_periods(l))` sub periods of each strategic period."
62+
$(length(price_pers))` sub periods of each strategic period."
2963
)
3064
end

src/link/datastructures.jl

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
CapacityCostLink
2+
struct CapacityCostLink <: Link
33
44
A link between two nodes with costs on the link usage for the resource `cap_resource`. All
55
other resources have no costs associated with their usage (follows the
@@ -11,20 +11,28 @@ other resources have no costs associated with their usage (follows the
1111
- **`to::Node`** is the node to which there is flow out of the link.
1212
- **`cap::TimeProfile`** is the capacity of the link for the `cap_resource`.
1313
- **`cap_price::TimeProfile`** is the price of capacity usage for the `cap_resource`.
14-
- **`cap_price_periods::Int64`** is the number of sub periods of a year.
14+
- **`cap_price_periods::Union{Int64, Vector{<:Number}}`** is either the number of sub periods
15+
within a strategic period (if specified as `Int64`) or the minimum durations of the
16+
individual sub periods within a strategic period (if specified as `Vector{<:Number}`).
1517
- **`cap_resource::Resource`** is the resource used by `CapacityCostLink`
16-
- **`formulation::Formulation`** is the used formulation of links. The field
17-
`formulation` is conditional through usage of a constructor.
18+
- **`formulation::Formulation`** is the used formulation of links. The field `formulation`
19+
is conditional through usage of a constructor.
1820
- **`data::Vector{<:ExtensionData}`** is the additional data (*e.g.*, for investments). The
1921
field `data` is conditional through usage of a constructor.
22+
23+
!!! note "Sub periods"
24+
You can specify either the total number of sub periods within a `CapacityCostLink` as
25+
`Int64` or the durations of each sub period if using a `Vector{<:Number}`. The latter
26+
requires you to be careful when considering the durations of the individual sub periods
27+
and the total duration of sub periods within the operational time structure.
2028
"""
2129
struct CapacityCostLink <: EMB.Link
2230
id::Any
2331
from::EMB.Node
2432
to::EMB.Node
2533
cap::TimeProfile
2634
cap_price::TimeProfile
27-
cap_price_periods::Int64
35+
cap_price_periods::Union{Int64, Vector{<:Number}}
2836
cap_resource::Resource
2937
formulation::EMB.Formulation
3038
data::Vector{<:ExtensionData}
@@ -36,7 +44,7 @@ function CapacityCostLink(
3644
to::EMB.Node,
3745
cap::TimeProfile,
3846
cap_price::TimeProfile,
39-
cap_price_periods::Int64,
47+
cap_price_periods::Union{Int64, Vector{<:Number}},
4048
cap_resource::Resource,
4149
formulation::EMB.Formulation,
4250
)
@@ -58,7 +66,7 @@ function CapacityCostLink(
5866
to::EMB.Node,
5967
cap::TimeProfile,
6068
cap_price::TimeProfile,
61-
cap_price_periods::Int64,
69+
cap_price_periods::Union{Int64, Vector{<:Number}},
6270
cap_resource::Resource,
6371
data::Vector{<:ExtensionData},
6472
)
@@ -80,7 +88,7 @@ function CapacityCostLink(
8088
to::EMB.Node,
8189
cap::TimeProfile,
8290
cap_price::TimeProfile,
83-
cap_price_periods::Int64,
91+
cap_price_periods::Union{Int64, Vector{<:Number}},
8492
cap_resource::Resource,
8593
)
8694
return CapacityCostLink(
@@ -147,8 +155,8 @@ cap_price(l::CapacityCostLink, t) = l.cap_price[t]
147155
"""
148156
cap_price_periods(l::CapacityCostLink)
149157
150-
Returns the number of sub-periods within a year for which a price is calculated of a capacity
151-
cost link `l`.
158+
Returns either the number of sub-periods within a strategic period for which a price is
159+
calculated or the vector of the durations of the sub-periods of a capacity cost link `l`.
152160
"""
153161
cap_price_periods(l::CapacityCostLink) = l.cap_price_periods
154162

src/link/model.jl

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ Creates the following additional variable for **ALL** capacity cost links:
88
for a [`CapacityCostLink`](@ref) `l` in operational period `t`.
99
"""
1010
function EMB.variables_element(m, ℒˢᵘᵇ::Vector{<:CapacityCostLink}, 𝒯, ::EnergyModel)
11-
@variable(m, ccl_cap_use_max[ℒˢᵘᵇ, 𝒯] >= 0)
12-
@variable(m, ccl_cap_use_cost[ℒˢᵘᵇ, 𝒯] >= 0)
11+
@variable(m, ccl_cap_use_max[ℒˢᵘᵇ, 𝒯] 0)
12+
@variable(m, ccl_cap_use_cost[ℒˢᵘᵇ, 𝒯] 0)
1313
end
1414

1515
"""
@@ -33,7 +33,7 @@ function EMB.create_link(
3333
p_cap = cap_resource(l)
3434

3535
# Create sub-periods based on the user-defined number of sub periods of a year
36-
𝒯ˢᵘᵇ = create_sub_periods(l, 𝒯)
36+
𝒯ˢᵘᵇ = get_sub_periods(l, 𝒯)
3737

3838
# Capacity cost link where output equals input (no losses)
3939
@constraint(m, [t 𝒯],
@@ -52,7 +52,7 @@ function EMB.create_link(
5252
# Capacity cost constraint
5353
@constraint(m, [t_sub 𝒯ˢᵘᵇ],
5454
m[:ccl_cap_use_cost][l, t_sub[end]] ==
55-
m[:ccl_cap_use_max][l, t_sub[end]] * avg_cap_price(l, t_sub)
55+
m[:ccl_cap_use_max][l, t_sub[end]] * get_avg_cap_price(l, t_sub)
5656
)
5757

5858
# Sum up costs for each sub_period into the strategic period cost
@@ -68,42 +68,57 @@ function EMB.create_link(
6868
end
6969

7070
"""
71-
avg_cap_price(l::CapacityCostLink, t_sub::Vector{TS.TimePeriod})
71+
get_avg_cap_price(l::CapacityCostLink, t_sub::Vector{TS.TimePeriod})
7272
7373
Return the average capacity price over the sub period `t_sub` for the [`CapacityCostLink`](@ref) `l`.
7474
"""
75-
function avg_cap_price(l::CapacityCostLink, t_sub::Vector{<:TS.TimePeriod})
75+
function get_avg_cap_price(l::CapacityCostLink, t_sub::Vector{<:TS.TimePeriod})
7676
return sum(cap_price(l, t) * duration(t) for t t_sub) / sum(duration(t) for t t_sub)
7777
end
7878

7979
"""
80-
create_sub_periods(l::CapacityCostLink, 𝒯)
80+
get_sub_periods(l::CapacityCostLink, 𝒯)
8181
82-
Extract sub periods of the [`CapacityCostLink`](@ref) `l`.
82+
Return the vector of sub periods of the [`CapacityCostLink`](@ref) `l`.
8383
"""
84-
function create_sub_periods(l::CapacityCostLink, 𝒯)
85-
# Calculate the length of each sub period
86-
sub_period_duration::Float64 = 𝒯.op_per_strat / cap_price_periods(l)
84+
function get_sub_periods(l::CapacityCostLink, 𝒯)
85+
# Calculate the duration of each sub period
86+
sub_pers_durations = get_sub_pers_durations(l, 𝒯)
8787

88-
# Create a vector collecting all `TimePeriod`s of each sub period into a vector for each
89-
# sub period
88+
# Create a vector collecting all `TimePeriod`s of each sub period into a vector
9089
sub_periods = Vector{TS.TimePeriod}[]
9190
for t_inv strategic_periods(𝒯)
92-
accumulated_duration::Float64 = 0.0
91+
idx = 1
92+
accumulated_duration = 0.0
9393
sub_period = TS.TimePeriod[]
9494
for t t_inv
9595
push!(sub_period, t)
9696
accumulated_duration += duration(t) * multiple_strat(t_inv, t)
9797

9898
# Check if the accumulated time of the periods in `sub_period` fills up a sub
9999
# period duration
100-
if accumulated_duration sub_period_duration
100+
if accumulated_duration sub_pers_durations[idx]
101101
push!(sub_periods, sub_period)
102102
sub_period = TS.TimePeriod[]
103-
accumulated_duration = 0
103+
accumulated_duration = 0.0
104+
idx += 1
104105
end
105106
end
106107
end
107108

108109
return sub_periods
109110
end
111+
112+
"""
113+
get_sub_pers_durations(l::CapacityCostLink, 𝒯)
114+
get_sub_pers_durations(l::CapacityCostLink, 𝒯, price_pers::Int64)
115+
get_sub_pers_durations(l::CapacityCostLink, 𝒯, price_pers::Vector{<:Number})
116+
117+
Returns the individual durations of the different sub periods within a time structure `𝒯`
118+
and [`CapacityCostLink`](@ref) `l`.
119+
"""
120+
get_sub_pers_durations(l::CapacityCostLink, 𝒯) =
121+
get_sub_pers_durations(l, 𝒯, cap_price_periods(l))
122+
get_sub_pers_durations(l::CapacityCostLink, 𝒯, price_pers::Int64) =
123+
ones(price_pers) * 𝒯.op_per_strat / price_pers
124+
get_sub_pers_durations(l::CapacityCostLink, 𝒯, price_pers::Vector{<:Number}) = price_pers

0 commit comments

Comments
 (0)