Skip to content

Commit a479760

Browse files
authored
Fixed a bug when using OperationalScenarios (#70)
* Storage balance wrong when no RepresentativePeriods are used * CyclicRepresentative still led to correct results
1 parent a02c729 commit a479760

4 files changed

Lines changed: 195 additions & 17 deletions

File tree

NEWS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# Release notes
22

3-
## Unversioned
3+
## Version 0.8.4 (2025-07-03)
44

55
### Bugfixes
66

77
* Fixed a bug when a `Link` did not have the same `input` and `output` `Resource`.
8+
* Fixed a bug when using `Storage{CyclicStrategic}` with `TwoLevel{OperationalScenarios}`:
9+
* The storage balance was wrongly calculated.
10+
* Updated constraints on the storage level when using `OperationalScenarios` without `RepresentativePeriods`.
11+
* Added tests to avoid these problems in the future.
812

913
## Version 0.8.3 (2024-11-29)
1014

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "EnergyModelsBase"
22
uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50"
33
authors = ["Lars Hellemo <Lars.Hellemo@sintef.no>, Julian Straus <Julian.Straus@sintef.no>"]
4-
version = "0.8.3"
4+
version = "0.8.4"
55

66
[deps]
77
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

src/constraint_functions.jl

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,15 +478,39 @@ function constraints_level_scp(m, n::Storage, per, modeltype::EnergyModel)
478478
return nothing
479479
end
480480
"""
481-
constraints_level_scp(m, n::Storage{CyclicRepresentative}, per, modeltype::EnergyModel)
481+
constraints_level_scp(m, n::Storage{<:Cyclic}, per::TS.AbstractStrategicPeriod, modeltype::EnergyModel)
482+
483+
When a Storage{<:Cyclic} is used in a time structure without `RepresentativeProfile`, the
484+
final level in an operational scenario is constrained to be the same in all operational
485+
scenarios for the cyclic constraints.
486+
"""
487+
function constraints_level_scp(
488+
m,
489+
n::Storage{<:Cyclic},
490+
per::TS.AbstractStrategicPeriod,
491+
modeltype::EnergyModel,
492+
)
493+
# Declaration of the required subsets
494+
𝒯ˢᶜ = opscenarios(per)
495+
last_scp = [last(t_scp) for t_scp 𝒯ˢᶜ]
496+
497+
# Constraint that level is similar to the level in the first scenario
498+
for t last_scp[2:end]
499+
@constraint(m, m[:stor_level][n, t] == m[:stor_level][n, first(last_scp)])
500+
end
501+
end
502+
"""
503+
constraints_level_scp(m, n::Storage{CyclicRepresentative}, per::TS.AbstractRepresentativePeriod, modeltype::EnergyModel)
482504
483505
When a Storage{CyclicRepresentative} is used, the final level in an operational scenario is
484506
constrained to be the same in all operational scenarios.
507+
508+
This function is required in addition to enforce the [`CyclicRepresentative`](@ref) constraint.
485509
"""
486510
function constraints_level_scp(
487511
m,
488512
n::Storage{CyclicRepresentative},
489-
per,
513+
per::TS.AbstractRepresentativePeriod,
490514
modeltype::EnergyModel,
491515
)
492516
# Declaration of the required subsets

test/test_nodes.jl

Lines changed: 163 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
@testset "Node utilities" begin
2-
32
# Resources used in the analysis
43
NG = ResourceEmit("NG", 0.2)
54
Coal = ResourceCarrier("Coal", 0.35)
@@ -156,9 +155,7 @@
156155
end
157156
end
158157

159-
160158
@testset "Test RefSource and RefSink" begin
161-
162159
# Resources used in the analysis
163160
Power = ResourceCarrier("Power", 0.0)
164161
CO2 = ResourceEmit("CO2", 1.0)
@@ -867,7 +864,6 @@ end
867864
end
868865

869866
@testset "SimpleTimes without storage" begin
870-
871867
# Run the model and extract the data
872868
m, case, model = simple_graph()
873869
𝒯 = case[:T]
@@ -918,7 +914,6 @@ end
918914
end
919915

920916
@testset "SimpleTimes with storage" begin
921-
922917
# Run the model and extract the data
923918
m, case, model = simple_graph(; demand = OperationalProfile([10, 15, 5, 15, 5]))
924919
𝒯 = case[:T]
@@ -964,8 +959,74 @@ end
964959
@test sum(value.(m[:stor_level][stor, t]) 0 for t 𝒯, atol = TEST_ATOL) == 4
965960
end
966961

967-
@testset "RepresentativePeriods with storage" begin
962+
@testset "OperationalScenarios with storage" begin
963+
# Run the model and extract the data
964+
op_profile_1 = OperationalProfile([15, 5, 15, 5, 15, 5, 15, 5, 15, 5])
965+
op_profile_2 = OperationalProfile([20, 20, 0, 0, 20, 0, 20, 0, 20, 0])
966+
demand = ScenarioProfile([op_profile_1, op_profile_2])
967+
968+
op_1 = SimpleTimes(10, 2)
969+
op_2 = SimpleTimes(10, 2)
970+
971+
ops = OperationalScenarios(2, [op_1, op_2], [0.5, 0.5])
972+
973+
m, case, model = simple_graph(; ops, op_per_strat = 20, demand)
974+
975+
𝒯 = case[:T]
976+
𝒯ᴵⁿᵛ = strategic_periods(𝒯)
977+
𝒯ˢᶜ = opscenarios(𝒯)
978+
𝒩 = case[:nodes]
979+
stor = 𝒩[3]
980+
981+
# Run the general tests
982+
general_tests(m, case, model)
983+
984+
# All the tests following are for the function, its individual methods, and the
985+
# caleed functions within the function.
986+
# function constraints_level_iterate(
987+
# m,
988+
# n::Storage,
989+
# prev_pers::PreviousPeriods,
990+
# cyclic_pers::CyclicPeriods,
991+
# per,
992+
# _::,
993+
# modeltype,
994+
# )
995+
996+
# Test that the level for starting an operational scenario is required to be the
997+
# same in the different operational scenarios
998+
first_scp = [first(t_scp) for t_scp 𝒯ˢᶜ]
999+
@test sum(
1000+
value.(m[:stor_level][stor, t]) -
1001+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t) 40 for t first_scp,
1002+
atol = TEST_ATOL
1003+
) length(first_scp) atol = TEST_ATOL
1004+
1005+
for t_inv 𝒯ᴵⁿᵛ
1006+
𝒯ʳᵖ = repr_periods(t_inv)
1007+
for (t_rp_prev, t_rp) withprev(𝒯ʳᵖ), (t_prev, t) withprev(t_rp)
1008+
if isnothing(t_prev)
1009+
# Test for the linking between the first and the last operational period
1010+
@test value.(m[:stor_level][stor, t])
1011+
value.(m[:stor_level][stor, last(t_rp)]) +
1012+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t) atol =
1013+
TEST_ATOL
1014+
end
1015+
end
1016+
end
1017+
# Test for the correct accounting in all other operational periods
1018+
@test sum(
1019+
value.(m[:stor_level][stor, t])
1020+
value.(m[:stor_level][stor, t_prev]) +
1021+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t) for
1022+
(t_prev, t) withprev(𝒯), atol = TEST_ATOL if !isnothing(t_prev)
1023+
) length(𝒯) - length(𝒯ᴵⁿᵛ) * ops.len atol = TEST_ATOL
1024+
1025+
# Check that the level is 0 exactly 2 times
1026+
@test sum(value.(m[:stor_level][stor, t]) 0 for t 𝒯) 2 atol = TEST_ATOL
1027+
end
9681028

1029+
@testset "RepresentativePeriods with storage" begin
9691030
# Run the model and extract the data
9701031
op_profile_1 = FixedProfile(0)
9711032
op_profile_2 = FixedProfile(20)
@@ -1055,6 +1116,97 @@ end
10551116
@test sum(value.(m[:flow_out][stor, t, Power]) > 0 for t 𝒯)
10561117
length(𝒯ᴵⁿᵛ) * length(op_2)
10571118
end
1119+
1120+
@testset "OperationalScenarios and RepresentativePeriods with storage" begin
1121+
# Run the model and extract the data
1122+
op_profile_11 = OperationalProfile([15, 5, 15, 5, 10])
1123+
op_profile_12 = OperationalProfile([20, 20, 0, 0, 10])
1124+
op_profile_21 = OperationalProfile([5, 10, 5, 10, 10])
1125+
op_profile_22 = OperationalProfile([0, 10, 0, 10, 10])
1126+
scen_profile_1 = ScenarioProfile([op_profile_11, op_profile_12])
1127+
scen_profile_2 = ScenarioProfile([op_profile_21, op_profile_22])
1128+
demand = RepresentativeProfile([scen_profile_1, scen_profile_2])
1129+
1130+
op = SimpleTimes(5, 2)
1131+
scps = OperationalScenarios(2, [op, op], [0.5, 0.5])
1132+
ops = RepresentativePeriods(2, 40, [0.5, 0.5], [scps, scps])
1133+
1134+
m, case, model = simple_graph(; ops, op_per_strat = 40, demand)
1135+
1136+
𝒯 = case[:T]
1137+
𝒯ᴵⁿᵛ = strategic_periods(𝒯)
1138+
𝒯ʳᵖ = repr_periods(𝒯)
1139+
𝒯ˢᶜ = opscenarios(𝒯)
1140+
𝒩 = case[:nodes]
1141+
stor = 𝒩[3]
1142+
1143+
# Run the general tests
1144+
general_tests(m, case, model)
1145+
1146+
# All the tests following are for the function, its individual methods, and the
1147+
# caleed functions within the function.
1148+
# function constraints_level_iterate(
1149+
# m,
1150+
# n::Storage,
1151+
# prev_pers::PreviousPeriods,
1152+
# cyclic_pers::CyclicPeriods,
1153+
# per,
1154+
# _::,
1155+
# modeltype,
1156+
# )
1157+
1158+
# Test that the level for starting an operational scenario is required to be the
1159+
# same in the different operational scenarios within a representative period
1160+
for t_rp 𝒯ʳᵖ
1161+
first_scp = [first(t_scp) for t_scp opscenarios(t_rp)]
1162+
@test value.(m[:stor_level][stor, first_scp[1]]) -
1163+
value.(m[:stor_level_Δ_op][stor, first_scp[1]]) * duration(first_scp[1])
1164+
value.(m[:stor_level][stor, first_scp[2]]) -
1165+
value.(m[:stor_level_Δ_op][stor, first_scp[2]]) * duration(first_scp[2])
1166+
atol = TEST_ATOL
1167+
end
1168+
1169+
# Test that the level for starting a representative period is not required to be the
1170+
# same in the different representative periods
1171+
first_rp = [first(t_rp) for t_rp 𝒯ʳᵖ]
1172+
@test sum(
1173+
value.(m[:stor_level][stor, t]) -
1174+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t) 0
1175+
for t first_rp, atol = TEST_ATOL
1176+
) == length(𝒯ᴵⁿᵛ)
1177+
@test sum(
1178+
value.(m[:stor_level][stor, t]) -
1179+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t) 40 for t first_rp,
1180+
atol = TEST_ATOL
1181+
) == length(𝒯ᴵⁿᵛ)
1182+
1183+
for t_inv 𝒯ᴵⁿᵛ
1184+
𝒯ʳᵖ = repr_periods(t_inv)
1185+
for (t_rp_prev, t_rp) withprev(𝒯ʳᵖ)
1186+
𝒯ˢᶜ = opscenarios(t_rp)
1187+
for t_sc 𝒯ˢᶜ
1188+
t = first(t_sc)
1189+
# Test for the linking between the first and the last operational period
1190+
# in the individual operational scenarios is working correctly
1191+
t_rp_prev = isnothing(t_rp_prev) ? last(𝒯ʳᵖ) : t_rp_prev
1192+
@test value.(m[:stor_level][stor, t]) -
1193+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t)
1194+
value.(m[:stor_level][stor, first(t_rp_prev)]) -
1195+
value.(m[:stor_level_Δ_op][stor, first(t_rp_prev)]) *
1196+
duration(first(t_rp_prev)) +
1197+
value.(m[:stor_level_Δ_rp][stor, t_rp_prev]) atol =
1198+
TEST_ATOL
1199+
end
1200+
end
1201+
end
1202+
# Test for the correct accounting in all other operational periods
1203+
@test sum(
1204+
value.(m[:stor_level][stor, t])
1205+
value.(m[:stor_level][stor, t_prev]) +
1206+
value.(m[:stor_level_Δ_op][stor, t]) * duration(t) for
1207+
(t_prev, t) withprev(𝒯), atol = TEST_ATOL if !isnothing(t_prev)
1208+
) length(𝒯) - length(𝒯ᴵⁿᵛ) * ops.len * scps.len atol = TEST_ATOL
1209+
end
10581210
end
10591211

10601212
@testset "Test RefStorage{CyclicRepresentative}" begin
@@ -1177,7 +1329,6 @@ end
11771329
end
11781330

11791331
@testset "SimpleTimes without storage" begin
1180-
11811332
# Run the model and extract the data
11821333
m, case, model = simple_graph()
11831334
𝒯 = case[:T]
@@ -1227,8 +1378,8 @@ end
12271378
) for t_inv 𝒯ᴵⁿᵛ, atol = TEST_ATOL
12281379
)
12291380
end
1230-
@testset "SimpleTimes with storage" begin
12311381

1382+
@testset "SimpleTimes with storage" begin
12321383
# Run the model and extract the data
12331384
m, case, model = simple_graph(; demand = OperationalProfile([10, 15, 5, 15, 5]))
12341385
𝒯 = case[:T]
@@ -1274,8 +1425,8 @@ end
12741425
# Test that the level is 0 exactly 4 times
12751426
@test sum(value.(m[:stor_level][stor, t]) 0 for t 𝒯, atol = TEST_ATOL) == 4
12761427
end
1277-
@testset "OperationalScenarios with storage" begin
12781428

1429+
@testset "OperationalScenarios with storage" begin
12791430
# Run the model and extract the data
12801431
op_profile_1 = OperationalProfile([15, 5, 15, 5, 15, 5, 15, 5, 15, 5])
12811432
op_profile_2 = OperationalProfile([20, 20, 0, 0, 20, 0, 20, 0, 20, 0])
@@ -1341,8 +1492,8 @@ end
13411492
# Check that the level is 0 exactly 2 times
13421493
@test sum(value.(m[:stor_level][stor, t]) 0 for t 𝒯) 2 atol = TEST_ATOL
13431494
end
1344-
@testset "RepresentativePeriods with storage" begin
13451495

1496+
@testset "RepresentativePeriods with storage" begin
13461497
# Run the model and extract the data
13471498
op_profile_1 = OperationalProfile([15, 5, 15, 5, 15, 5, 15, 5, 15, 5])
13481499
op_profile_2 = OperationalProfile([20, 20, 0, 0, 20, 0, 20, 0, 20, 0])
@@ -1417,8 +1568,8 @@ end
14171568
(t_prev, t) withprev(𝒯), atol = TEST_ATOL if !isnothing(t_prev)
14181569
) length(𝒯) - length(𝒯ᴵⁿᵛ) * ops.len atol = TEST_ATOL
14191570
end
1420-
@testset "OperationalScenarios and RepresentativePeriods with storage" begin
14211571

1572+
@testset "OperationalScenarios and RepresentativePeriods with storage" begin
14221573
# Run the model and extract the data
14231574
op_profile_11 = OperationalProfile([15, 5, 15, 5, 10])
14241575
op_profile_12 = OperationalProfile([20, 20, 0, 0, 10])
@@ -1440,6 +1591,7 @@ end
14401591
𝒯ˢᶜ = opscenarios(𝒯)
14411592
𝒩 = case[:nodes]
14421593
stor = 𝒩[3]
1594+
14431595
# Run the general tests
14441596
general_tests(m, case, model)
14451597

@@ -1673,7 +1825,6 @@ end
16731825
end
16741826

16751827
@testset "SimpleTimes with storage" begin
1676-
16771828
# Run the model and extract the data
16781829
m, case, model = simple_graph(; stor_cap = 100, em_limit = [100, 4])
16791830
𝒯 = case[:T]
@@ -1733,7 +1884,6 @@ end
17331884
end
17341885

17351886
@testset "RepresentativePeriods with storage" begin
1736-
17371887
# Run the model and extract the data
17381888
op_1 = SimpleTimes(2, 2)
17391889
op_2 = SimpleTimes(2, 2)

0 commit comments

Comments
 (0)