From 506c9fa9837f6131538923f7a5be3345c6092a99 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 13:39:18 +0200 Subject: [PATCH 1/4] Minor simplification in checks --- src/checks.jl | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/checks.jl b/src/checks.jl index 5cf0dbf..3f89ab2 100644 --- a/src/checks.jl +++ b/src/checks.jl @@ -710,12 +710,12 @@ function check_representative_profile(time_profile::TimeProfile, message::String # Iterate through the strategic profiles, if existing if isa(time_profile, StrategicProfile) for l1_profile ∈ time_profile.vals - sub_msg = "in strategic profiles " * message - bool_rp = check_repr_sub_profile(l1_profile, sub_msg, bool_rp) + sub_msg_1 = "in strategic profiles " * message + bool_rp = check_repr_sub_profile(l1_profile, sub_msg_1, bool_rp) if isa(l1_profile, RepresentativeProfile) for l2_profile ∈ l1_profile.vals - sub_msg = "in representative profiles in strategic profiles " * message - bool_rp = check_repr_sub_profile(l2_profile, sub_msg, bool_rp) + sub_msg_2 = "in representative profiles " * sub_msg_1 + bool_rp = check_repr_sub_profile(l2_profile, sub_msg_2, bool_rp) end end end @@ -748,8 +748,8 @@ Function for checking that an individual `TimeProfile` does not include the wron scenario indexing. ## Checks -- `TimeProfile`s accessed in `RepresentativePeriod`s cannot include `OperationalProfile` - or `ScenarioProfile` as this is not allowed through indexing on the `TimeProfile`. +- `TimeProfile`s accessed in `OperationalScenario`s cannot include `OperationalProfile` as + this is not allowed through indexing on the `TimeProfile`. """ function check_scenario_profile(time_profile::TimeProfile, message::String) # Check on the highest level @@ -758,23 +758,23 @@ function check_scenario_profile(time_profile::TimeProfile, message::String) # Iterate through the strategic profiles, if existing if isa(time_profile, StrategicProfile) for l1_profile ∈ time_profile.vals - sub_msg = "in strategic profiles " * message - bool_scp = check_osc_sub_profile(l1_profile, sub_msg, bool_scp) + sub_msg_1 = "in strategic profiles " * message + bool_scp = check_osc_sub_profile(l1_profile, sub_msg_1, bool_scp) if isa(l1_profile, RepresentativeProfile) + sub_msg_2 = "in representative profiles " * sub_msg_1 for l2_profile ∈ l1_profile.vals - sub_msg = "in representative profiles in strategic profiles " * message - bool_scp = check_osc_sub_profile(l2_profile, sub_msg, bool_scp) + bool_scp = check_osc_sub_profile(l2_profile, sub_msg_2, bool_scp) if isa(l2_profile, ScenarioProfile) + sub_msg_3 = "in scenario profiles in " * sub_msg_2 for l3_profile ∈ l2_profile.vals - sub_msg = "in scenario profiles in representative profiles in strategic profiles " * message - bool_scp = check_osc_sub_profile(l3_profile, sub_msg, bool_scp) + bool_scp = check_osc_sub_profile(l3_profile, sub_msg_3, bool_scp) end end end elseif isa(l1_profile, ScenarioProfile) for l2_profile ∈ l1_profile.vals - sub_msg = "in scenario profiles in strategic profiles " * message - bool_scp = check_osc_sub_profile(l2_profile, sub_msg, bool_scp) + sub_msg_2 = "in scenario profiles " * sub_msg_1 + bool_scp = check_osc_sub_profile(l2_profile, sub_msg_2, bool_scp) end end end @@ -783,12 +783,12 @@ function check_scenario_profile(time_profile::TimeProfile, message::String) # Iterate through the representative profiles, if existing if isa(time_profile, RepresentativeProfile) for l1_profile ∈ time_profile.vals - sub_msg = "in representative profiles " * message - bool_scp = check_osc_sub_profile(l1_profile, sub_msg, bool_scp) + sub_msg_1 = "in representative profiles " * message + bool_scp = check_osc_sub_profile(l1_profile, sub_msg_1, bool_scp) if isa(l1_profile, ScenarioProfile) for l2_profile ∈ l1_profile.vals - sub_msg = "in scenario profiles in representative profiles " * message - bool_scp = check_osc_sub_profile(l2_profile, sub_msg, bool_scp) + sub_msg_2 = "in scenario profiles " * sub_msg_1 + bool_scp = check_osc_sub_profile(l2_profile, sub_msg_2, bool_scp) end end end From 52f7f4ec7bd9109c2e13e9e8ac01a74500411ba5 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 13:40:14 +0200 Subject: [PATCH 2/4] Fixed problem with `scale_op_sp` and `TwoLevelTree` --- src/model.jl | 2 +- src/utils.jl | 12 ++++- test/test_general.jl | 59 ++++++++++++---------- test/test_investments.jl | 102 +++++++++++++++++++++------------------ 4 files changed, 98 insertions(+), 77 deletions(-) diff --git a/src/model.jl b/src/model.jl index bf7340b..5facc05 100644 --- a/src/model.jl +++ b/src/model.jl @@ -762,7 +762,7 @@ function objective(m, 𝒳ᵛᵉᶜ, 𝒫, 𝒯, modeltype::EnergyModel) # Calculation of the objective function. @objective(m, Max, -sum( - sum(𝒳[t_inv] for 𝒳 ∈ opex) * duration_strat(t_inv) + sum(𝒳[t_inv] for 𝒳 ∈ opex) * duration_strat(t_inv) * probability_branch(t_inv) for t_inv ∈ 𝒯ᴵⁿᵛ) ) end diff --git a/src/utils.jl b/src/utils.jl index 24dab8d..629e959 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -319,11 +319,19 @@ end Provides a simplified function for returning the multiplication -``duration(t) * multiple\\_strat(t\\_inv, t) * probability(t)`` +``duration(t) * multiple\\_strat(t\\_inv, t) * probability(t) / probability_branch(t)`` when operational periods are coupled with strategic periods. It is used to scale the value provided for operational periods to a duration of 1 of a strategic period. +!!! note "`TwoLevelTree` application" + The function does not consider the probability of a branch when using a + [`TwoLevelTree`](@extref TimeStruct.TwoLevelTree) time structure. The reason is that we + do not consider any scaling or discounting for the individual strategic variables. This + is implicitly included by utilizing the function [`probability_branch`](@extref TimeStruct.probability_branch) + or [`objective_weight`](@extref TimeStruct.objective_weight) in the function + [`objective`](@ref). + # Example ```julia @@ -340,7 +348,7 @@ scale_op_sp(t_inv, t) ``` """ scale_op_sp(t_inv::TS.AbstractStrategicPeriod, t::TS.TimePeriod) = - duration(t) * multiple_strat(t_inv, t) * probability(t) + duration(t) * multiple_strat(t_inv, t) * probability(t) / probability_branch(t) function multiple(t_inv, t) @warn( diff --git a/test/test_general.jl b/test/test_general.jl index 0e15d08..d4e425e 100644 --- a/test/test_general.jl +++ b/test/test_general.jl @@ -1,11 +1,11 @@ -function generate_data() +function generate_data(; 𝒯 = TwoLevel(4, 2, SimpleTimes(4, 2), op_per_strat = 8.0)) # Define the different resources NG = ResourceEmit("NG", 0.2) Coal = ResourceCarrier("Coal", 0.35) Power = ResourceCarrier("Power", 0.0) CO2 = ResourceEmit("CO2", 1.0) - products = [NG, Coal, Power, CO2] + 𝒫 = [NG, Coal, Power, CO2] # Creation of the emission data for the individual nodes. capture_data = CaptureEnergyEmissions(0.9) @@ -13,10 +13,10 @@ function generate_data() # Create the individual test nodes, corresponding to a system with an electricity demand/sink, # coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO2 storage. - nodes = [ - GenAvailability(1, products), - RefSource(2, FixedProfile(1e12), FixedProfile(30), FixedProfile(0), Dict(NG => 1)), - RefSource(3, FixedProfile(1e12), FixedProfile(9), FixedProfile(0), Dict(Coal => 1)), + 𝒩 = [ + GenAvailability(1, 𝒫), + RefSource(2, FixedProfile(100), FixedProfile(30), FixedProfile(0), Dict(NG => 1)), + RefSource(3, FixedProfile(100), FixedProfile(9), FixedProfile(0), Dict(Coal => 1)), RefNetworkNode( 4, FixedProfile(25), @@ -52,34 +52,33 @@ function generate_data() ] # Connect all nodes with the availability node for the overall energy/mass balance - links = [ - Direct(14, nodes[1], nodes[4], Linear()) - Direct(15, nodes[1], nodes[5], Linear()) - Direct(16, nodes[1], nodes[6], Linear()) - Direct(17, nodes[1], nodes[7], Linear()) - Direct(21, nodes[2], nodes[1], Linear()) - Direct(31, nodes[3], nodes[1], Linear()) - Direct(41, nodes[4], nodes[1], Linear()) - Direct(51, nodes[5], nodes[1], Linear()) - Direct(61, nodes[6], nodes[1], Linear()) + ℒ = [ + Direct(14, 𝒩[1], 𝒩[4], Linear()) + Direct(15, 𝒩[1], 𝒩[5], Linear()) + Direct(16, 𝒩[1], 𝒩[6], Linear()) + Direct(17, 𝒩[1], 𝒩[7], Linear()) + Direct(21, 𝒩[2], 𝒩[1], Linear()) + Direct(31, 𝒩[3], 𝒩[1], Linear()) + Direct(41, 𝒩[4], 𝒩[1], Linear()) + Direct(51, 𝒩[5], 𝒩[1], Linear()) + Direct(61, 𝒩[6], 𝒩[1], Linear()) ] - # Creation of the time structure and global data - T = TwoLevel(4, 2, SimpleTimes(4, 2), op_per_strat = 8) - model = OperationalModel( + # Creation of the modeltype + modeltype = OperationalModel( Dict(CO2 => StrategicProfile([160, 140, 120, 100]), NG => FixedProfile(1e6)), Dict(CO2 => FixedProfile(10)), CO2, ) # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) - return case, model + case = Case(𝒯, 𝒫, [𝒩, ℒ], [[get_nodes, get_links]]) + return case, modeltype end @testset "General tests" begin - case, model = generate_data() - m = run_model(case, model, HiGHS.Optimizer) + case, modeltype = generate_data() + m = run_model(case, modeltype, HiGHS.Optimizer) # Retrieve data from the case structure 𝒫 = get_products(case) @@ -100,6 +99,8 @@ end ℒ = get_links(case) + objective_TwoLevel = objective_value(m) + # Check for the objective value # (*2 compared to 0.6.0 due to change in strategic period duration) # (-10400 = 2*10*(160+140+120+100) compared to 0.8.3 due to inclusion of co2 emissions) @@ -113,10 +114,10 @@ end # - constraints_emissions(m, 𝒩, 𝒯, 𝒫, modeltype::EnergyModel) @test all( value.(m[:emissions_strategic])[t_inv, CO2] <= - EMB.emission_limit(model, CO2, t_inv) for t_inv ∈ 𝒯ᴵⁿᵛ + EMB.emission_limit(modeltype, CO2, t_inv) for t_inv ∈ 𝒯ᴵⁿᵛ ) @test all( - value.(m[:emissions_strategic])[t_inv, NG] <= EMB.emission_limit(model, NG, t_inv) + value.(m[:emissions_strategic])[t_inv, NG] <= EMB.emission_limit(modeltype, NG, t_inv) for t_inv ∈ 𝒯ᴵⁿᵛ ) @@ -140,7 +141,7 @@ end value.(m[:opex_var][n, t_inv]) + value.(m[:opex_fixed][n, t_inv]) for n ∈ 𝒩ᵒᵖᵉˣ) + sum( - value.(m[:emissions_total][t, CO2]) * emission_price(model, CO2, t) * + value.(m[:emissions_total][t, CO2]) * emission_price(modeltype, CO2, t) * scale_op_sp(t_inv, t) for t ∈ t_inv) ) * duration_strat(t_inv) for t_inv ∈ 𝒯ᴵⁿᵛ ) ≈ objective_value(m) atol = TEST_ATOL @@ -184,4 +185,10 @@ end p ∈ EMB.link_res(l), atol ∈ TEST_ATOL ) for l ∈ ℒ, atol ∈ TEST_ATOL ) + + # Test that the results are exactly the same for an equivalent `TwoLevelTree` + 𝒯 = TwoLevelTree(2, [2, 2, 2], SimpleTimes(4, 2), op_per_strat = 8.0) + case, modeltype = generate_data(; 𝒯) + m = run_model(case, modeltype, HiGHS.Optimizer) + @test objective_TwoLevel ≈ objective_value(m) end diff --git a/test/test_investments.jl b/test/test_investments.jl index 3d12a3e..976652a 100644 --- a/test/test_investments.jl +++ b/test/test_investments.jl @@ -2,13 +2,13 @@ using EnergyModelsInvestments @testset "Simple network" begin # Create simple model - function investment_model() + function investment_model(; 𝒯 = TwoLevel(4, 1, SimpleTimes(24, 1), op_per_strat = 24)) # Define the different resources NG = ResourceEmit("NG", 0.2) Coal = ResourceCarrier("Coal", 0.35) Power = ResourceCarrier("Power", 0.0) CO2 = ResourceEmit("CO2", 1.0) - products = [NG, Coal, Power, CO2] + 𝒫 = [NG, Coal, Power, CO2] op_profile = OperationalProfile([ 20, @@ -37,8 +37,8 @@ using EnergyModelsInvestments 20, ]) - nodes = [ - GenAvailability(1, products), + 𝒩 = [ + GenAvailability(1, 𝒫), RefSink( 2, op_profile, @@ -187,32 +187,31 @@ using EnergyModelsInvestments ], ), ] - links = [ - Direct(15, nodes[1], nodes[5], Linear()) - Direct(16, nodes[1], nodes[6], Linear()) - Direct(17, nodes[1], nodes[7], Linear()) - Direct(18, nodes[1], nodes[8], Linear()) - Direct(19, nodes[1], nodes[9], Linear()) - Direct(110, nodes[1], nodes[10], Linear()) - Direct(12, nodes[1], nodes[2], Linear()) - Direct(31, nodes[3], nodes[1], Linear()) - Direct(41, nodes[4], nodes[1], Linear()) - Direct(51, nodes[5], nodes[1], Linear()) - Direct(61, nodes[6], nodes[1], Linear()) - Direct(71, nodes[7], nodes[1], Linear()) - Direct(81, nodes[8], nodes[1], Linear()) - Direct(91, nodes[9], nodes[1], Linear()) - Direct(101, nodes[10], nodes[1], Linear()) + ℒ = [ + Direct(15, 𝒩[1], 𝒩[5], Linear()) + Direct(16, 𝒩[1], 𝒩[6], Linear()) + Direct(17, 𝒩[1], 𝒩[7], Linear()) + Direct(18, 𝒩[1], 𝒩[8], Linear()) + Direct(19, 𝒩[1], 𝒩[9], Linear()) + Direct(110, 𝒩[1], 𝒩[10], Linear()) + Direct(12, 𝒩[1], 𝒩[2], Linear()) + Direct(31, 𝒩[3], 𝒩[1], Linear()) + Direct(41, 𝒩[4], 𝒩[1], Linear()) + Direct(51, 𝒩[5], 𝒩[1], Linear()) + Direct(61, 𝒩[6], 𝒩[1], Linear()) + Direct(71, 𝒩[7], 𝒩[1], Linear()) + Direct(81, 𝒩[8], 𝒩[1], Linear()) + Direct(91, 𝒩[9], 𝒩[1], Linear()) + Direct(101, 𝒩[10], 𝒩[1], Linear()) ] # Creation of the time structure and global data - T = TwoLevel(4, 1, SimpleTimes(24, 1), op_per_strat = 24) em_limits = Dict(NG => FixedProfile(1e6), CO2 => StrategicProfile([450, 400, 350, 300])) em_cost = Dict(NG => FixedProfile(0), CO2 => FixedProfile(0)) modeltype = InvestmentModel(em_limits, em_cost, CO2, 0.07) # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + case = Case(𝒯, 𝒫, [𝒩, ℒ], [[get_nodes, get_links]]) return case, modeltype end @@ -229,6 +228,7 @@ using EnergyModelsInvestments # capacity any longer in 0.7.x) # (-10736 compared to 0.9.x due to the potential of early retirement) # (-16689 compared to 10.1.x due to the bugfix 0.9.1 in EMI) + objective_TwoLevel = objective_value(m) @test round(objective_value(m)) ≈ -296671.0 # Test that investments are happening @@ -248,6 +248,12 @@ using EnergyModelsInvestments @test sum( sum(value.(m[:stor_charge_add][n, t_inv]) > 0 for n ∈ 𝒩ᶜʰᵃʳᵍᵉ) for t_inv ∈ 𝒯ᴵⁿᵛ) > 0 + + # Test that the results are exactly the same for an equivalent `TwoLevelTree` + 𝒯 = TwoLevelTree(1, [2, 2, 2], SimpleTimes(24, 1), op_per_strat = 24.0) + case, modeltype = investment_model(; 𝒯) + m = run_model(case, modeltype, HiGHS.Optimizer) + @test objective_TwoLevel ≈ objective_value(m) end @testset "Link - OPEX and investments" begin @@ -313,21 +319,21 @@ end ), ] - products = [Power, CO2] - nodes = [source_1, source_2, sink] - links = Link[ + 𝒫 = [Power, CO2] + 𝒩 = [source_1, source_2, sink] + ℒ = Link[ OpexDirect("OpexDirect", source_1, sink, Linear()), InvDirect("InvDirect", source_2, sink, data_link), ] # Creation of the time structure and global data - T = TwoLevel(4, 1, SimpleTimes(24, 1), op_per_strat = 24) + 𝒯 = TwoLevel(4, 1, SimpleTimes(24, 1), op_per_strat = 24) em_limits = Dict(CO2 => StrategicProfile([450, 400, 350, 300])) em_cost = Dict(CO2 => FixedProfile(0)) modeltype = InvestmentModel(em_limits, em_cost, CO2, 0.0) # Input data structure - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + case = Case(𝒯, 𝒫, [𝒩, ℒ], [[get_nodes, get_links]]) return run_model(case, modeltype, HiGHS.Optimizer), case, modeltype end @@ -386,7 +392,7 @@ EMB.TEST_ENV = true min_add = FixedProfile(0), max_add = FixedProfile(10), inv_data = nothing, - T = TwoLevel(4, 10, SimpleTimes(4, 1)) + 𝒯 = TwoLevel(4, 10, SimpleTimes(4, 1)) ) if isnothing(inv_data) inv_data = [ @@ -400,7 +406,7 @@ EMB.TEST_ENV = true CO2 = ResourceEmit("CO2", 1.0) Power = ResourceCarrier("Power", 0.0) - products = [Power, CO2] + 𝒫 = [Power, CO2] source = RefSource( "-src", @@ -416,9 +422,9 @@ EMB.TEST_ENV = true Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e4)), Dict(Power => 1), ) - nodes = [source, sink] - links = [Direct("scr-sink", nodes[1], nodes[2], Linear())] - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + 𝒩 = [source, sink] + ℒ = [Direct("scr-sink", 𝒩[1], 𝒩[2], Linear())] + case = Case(𝒯, 𝒫, [𝒩, ℒ], [[get_nodes, get_links]]) em_limits = Dict(CO2 => StrategicProfile([450, 400, 350, 300])) em_cost = Dict(CO2 => FixedProfile(0)) @@ -450,13 +456,13 @@ EMB.TEST_ENV = true StrategicProfile([4]), StrategicProfile([oprofile, oprofile, oprofile, oprofile]) ] - T = TwoLevelTree(10, [2, 2, 1], SimpleTimes(4, 1)) + 𝒯 = TwoLevelTree(10, [2, 2, 1], SimpleTimes(4, 1)) for tp ∈ profiles @test_throws AssertionError check_graph_inv_node(; max_add=tp) @test_throws AssertionError check_graph_inv_node(; max_inst=tp) - @test_throws AssertionError check_graph_inv_node(; max_add=tp, T) - @test_throws AssertionError check_graph_inv_node(; max_inst=tp, T) + @test_throws AssertionError check_graph_inv_node(; max_add=tp, 𝒯) + @test_throws AssertionError check_graph_inv_node(; max_inst=tp, 𝒯) end # Check that we receive an error if the capacity is an operational profile @@ -520,7 +526,7 @@ EMB.TEST_ENV = true CO2 = ResourceEmit("CO2", 1.0) Power = ResourceCarrier("Power", 0.0) - products = [Power, CO2] + 𝒫 = [Power, CO2] source = RefSource( "-src", @@ -544,14 +550,14 @@ EMB.TEST_ENV = true Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e4)), Dict(Power => 1), ) - nodes = [source, storage, sink] - links = [ - Direct("src-stor", nodes[1], nodes[2], Linear()) - Direct("src-snk", nodes[1], nodes[3], Linear()) - Direct("stor-snk", nodes[2], nodes[3], Linear()) + 𝒩 = [source, storage, sink] + ℒ = [ + Direct("src-stor", 𝒩[1], 𝒩[2], Linear()) + Direct("src-snk", 𝒩[1], 𝒩[3], Linear()) + Direct("stor-snk", 𝒩[2], 𝒩[3], Linear()) ] - T = TwoLevel(4, 10, SimpleTimes(4, 1)) - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + 𝒯 = TwoLevel(4, 10, SimpleTimes(4, 1)) + case = Case(𝒯, 𝒫, [𝒩, ℒ], [[get_nodes, get_links]]) em_limits = Dict(CO2 => StrategicProfile([450, 400, 350, 300])) em_cost = Dict(CO2 => FixedProfile(0)) @@ -660,7 +666,7 @@ EMB.TEST_ENV = true CO2 = ResourceEmit("CO2", 1.0) Power = ResourceCarrier("Power", 0.0) - products = [Power, CO2] + 𝒫 = [Power, CO2] source = RefSource( "-src", @@ -675,10 +681,10 @@ EMB.TEST_ENV = true Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e4)), Dict(Power => 1), ) - nodes = [source, sink] - links = [InvDirect("scr-sink", nodes[1], nodes[2], inv_data)] - T = TwoLevel(4, 10, SimpleTimes(4, 1)) - case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) + 𝒩 = [source, sink] + ℒ = [InvDirect("scr-sink", 𝒩[1], 𝒩[2], inv_data)] + 𝒯 = TwoLevel(4, 10, SimpleTimes(4, 1)) + case = Case(𝒯, 𝒫, [𝒩, ℒ], [[get_nodes, get_links]]) em_limits = Dict(CO2 => StrategicProfile([450, 400, 350, 300])) em_cost = Dict(CO2 => FixedProfile(0)) From 5045f14d976b9628c6e4dbb200c08e052b35667c Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 14:20:20 +0200 Subject: [PATCH 3/4] Fixed NEWS.md and documentation --- NEWS.md | 8 ++++++++ Project.toml | 4 ++-- docs/src/manual/optimization-variables.md | 10 ++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/NEWS.md b/NEWS.md index abb0612..512d4dd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # Release notes +## Version 0.10.4 (2026-06-15) + +### Bug fixes + +* Fixed a bug regarding the branch probability when using a `TwoLevelTree` structure: + * This bug resulted in strategic variables being scaled when calculated from operational variables through the function `scale_op_sp`. + * As a consequence, as an example, emission limits where wrongly applied. + ## Version 0.10.3 (2026-06-10) ### Bug fixes diff --git a/Project.toml b/Project.toml index c7df5d9..f216017 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "EnergyModelsBase" uuid = "5d7e687e-f956-46f3-9045-6f5a5fd49f50" authors = ["Lars Hellemo , Julian Straus "] -version = "0.10.3" +version = "0.10.4" [deps] JuMP = "4076af6c-e467-56ae-b986-b466b2749572" @@ -18,5 +18,5 @@ EMIExt = "EnergyModelsInvestments" EnergyModelsInvestments = "0.9" JuMP = "1" SparseVariables = "0.7.3" -TimeStruct = "0.9" +TimeStruct = "0.9.11" julia = "1.10" diff --git a/docs/src/manual/optimization-variables.md b/docs/src/manual/optimization-variables.md index ba59bd1..6c889ca 100644 --- a/docs/src/manual/optimization-variables.md +++ b/docs/src/manual/optimization-variables.md @@ -9,7 +9,7 @@ The latter is the recommended approach. !!! note The majority of the variables in `EnergyModelsBase` are rate variables. This imples that they are calculated for either an operational period duration of 1, when indexed over operational period ``t`` or a strategic period duration of 1, when indexed over strategic period ``t_\texttt{inv}``. - Typical units for rates are MW for energy streams, tonne/hour for mass streams, tonne/year for strategic emissions, and €/year for operational expenditures. + Typical units for rates are MW for energy streams, tonne/hour for mass streams, tonne/year for strategic emissions, and €/year for operating expenses. In this example, the duration of an operational period of 1 corresponds to an hour, while the duration of a strategic period of 1 corresponds to a year. Variables that are energy/mass based have that property highlighted in the documentation below. @@ -27,9 +27,15 @@ The multiplication then leads to an energy/mass quantity in stead of an energy/m The coupling of strategic and operational periods can be achieved through the function `scale_op_sp(t, t_inv)`. This functions allows for considering the scaling of the operational periods within a strategic period. +!!! note "`TwoLevelTree` and variables" + All variables that are indexed over strategic periods do not take into consideration the branch probability. + This is, *e.g.*, the case for the strategic emission variables or the variable operating expenses. + + The branch probability is however taken into account when calculating the objective function. + ## [Operational cost variables](@id man-opt_var-opex) -Operational cost variables are included to account for operational expenditures (OPEX) of the model. +Operational cost variables are included to account for operating expenses (OPEX) of the model. These costs are pure dependent on either the use or the installed capacity of a node ``n``. All nodes ``n`` (except [`Availability`](@ref)-nodes) have the following variables representing the operational costs of the nodes: From 4b77722fc384f3d679a075f739f4fa9d8064bf31 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Mon, 15 Jun 2026 15:04:00 +0200 Subject: [PATCH 4/4] Updated the docs based on the feedback --- docs/src/manual/optimization-variables.md | 3 ++- src/utils.jl | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/src/manual/optimization-variables.md b/docs/src/manual/optimization-variables.md index 6c889ca..59b3a55 100644 --- a/docs/src/manual/optimization-variables.md +++ b/docs/src/manual/optimization-variables.md @@ -29,7 +29,8 @@ This functions allows for considering the scaling of the operational periods wit !!! note "`TwoLevelTree` and variables" All variables that are indexed over strategic periods do not take into consideration the branch probability. - This is, *e.g.*, the case for the strategic emission variables or the variable operating expenses. + This is, *e.g.*, the case for the strategic emission variables or the variable operating expenses that apply per scenario/branch. + The reason for this approach is to simplify the comparison of individual values between different branches without the need to consider the probability. The branch probability is however taken into account when calculating the objective function. diff --git a/src/utils.jl b/src/utils.jl index 629e959..4ca53b4 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -327,10 +327,14 @@ provided for operational periods to a duration of 1 of a strategic period. !!! note "`TwoLevelTree` application" The function does not consider the probability of a branch when using a [`TwoLevelTree`](@extref TimeStruct.TwoLevelTree) time structure. The reason is that we - do not consider any scaling or discounting for the individual strategic variables. This - is implicitly included by utilizing the function [`probability_branch`](@extref TimeStruct.probability_branch) - or [`objective_weight`](@extref TimeStruct.objective_weight) in the function - [`objective`](@ref). + do not consider any scaling or discounting for the individual strategic variables to + avoid scaling the corresponding bounds. In addition, it allows a simple comparison + of branches with different probabilities. + + The scaling is however included in the function [`objective`](@ref) by utilizing the + function [`probability_branch`](@extref TimeStruct.probability_branch) + or [`objective_weight`](@extref TimeStruct.objective_weight) when considering + investments. # Example