From 3e04d276b5e65512d05c0ff7d886305e2e9894f9 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 23 Sep 2025 08:55:19 +0200 Subject: [PATCH 1/2] Improved example descriptions --- NEWS.md | 6 +++ docs/src/nodes/sink.md | 10 ++-- examples/network.jl | 68 ++++++++++++++----------- examples/network_invest.jl | 90 ++++++++++++++++++++-------------- examples/sink_source.jl | 27 +++++----- examples/sink_source_invest.jl | 50 ++++++++++--------- src/structures/case.jl | 4 +- src/structures/model.jl | 4 +- test/test_modeltype.jl | 4 +- 9 files changed, 153 insertions(+), 110 deletions(-) diff --git a/NEWS.md b/NEWS.md index dbdb0ea2..e4a58496 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # Release notes +## Unversioned + +### Minor updates + +* Added additional comments to the examples for a simplified understanding. + ## Version 0.9.2 (2025-07-03) ### Bugfixes diff --git a/docs/src/nodes/sink.md b/docs/src/nodes/sink.md index d348fdb1..98624e09 100644 --- a/docs/src/nodes/sink.md +++ b/docs/src/nodes/sink.md @@ -53,7 +53,7 @@ The variables of [`Sink`](@ref) nodes include: - [``\texttt{opex\_fixed}``](@ref man-opt_var-opex) - [``\texttt{cap\_use}``](@ref man-opt_var-cap) - [``\texttt{cap\_inst}``](@ref man-opt_var-cap) -- [``\texttt{flow\_out}``](@ref man-opt_var-flow) +- [``\texttt{flow\_in}``](@ref man-opt_var-flow) - [``\texttt{sink\_surplus}``](@ref man-opt_var-sink) - [``\texttt{sink\_deficit}``](@ref man-opt_var-sink) - [``\texttt{emissions\_node}``](@ref man-opt_var-emissions) if `EmissionsData` is added to the field `data` @@ -110,10 +110,10 @@ Hence, if you do not have to call additional functions, but only plan to include ```math \begin{aligned} - \texttt{opex\_var}[n, t_{inv}] = & \\ - \sum_{t \in t_{inv}} & surplus\_penalty(n, t) \times \texttt{sink\_surplus}[n, t] + \\ & - deficit\_penalty(n, t) \times \texttt{sink\_deficit}[n, t] \times \\ & - scale\_op\_sp(t_{inv}, t) + \texttt{opex\_var}[n, t_{inv}] = & scale\_op\_sp(t_{inv}, t) \times \\ + \sum_{t \in t_{inv}} & ( surplus\_penalty(n, t) \times \texttt{sink\_surplus}[n, t] + \\ & + deficit\_penalty(n, t) \times \texttt{sink\_deficit}[n, t]) \\ & + \end{aligned} ``` diff --git a/examples/network.jl b/examples/network.jl index 547a2ddd..26dbfc23 100644 --- a/examples/network.jl +++ b/examples/network.jl @@ -23,15 +23,15 @@ more expensive natural gas power plant with CCS to reduce emissions. function generate_example_network() @info "Generate case data - Simple network example" - # Define the different resources and their emission intensity in tCO2/MWh - 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] + # Define the different resources and their emission intensity in t CO₂/MWh + ng = ResourceEmit("NG", 0.2) + coal = ResourceCarrier("Coal", 0.35) + power = ResourceCarrier("Power", 0.0) + co2 = ResourceEmit("CO₂", 1.0) + products = [ng, coal, power, co2] # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 + op_duration = 2 # Each operational period has a duration of 2 (hours) op_number = 4 # There are in total 4 operational periods operational_periods = SimpleTimes(op_number, op_duration) @@ -45,19 +45,24 @@ function generate_example_network() T = TwoLevel(4, 1, operational_periods; op_per_strat) model = OperationalModel( Dict( # Emission cap for CO₂ in t/8h and for NG in MWh/8h - CO2 => StrategicProfile([160, 140, 120, 100]), - NG => FixedProfile(1e6), + co2 => StrategicProfile([160, 140, 120, 100]), + ng => FixedProfile(1e6), ), Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh - CO2 => FixedProfile(0), - NG => FixedProfile(0), + co2 => FixedProfile(0), + ng => FixedProfile(0), ), - CO2, # CO2 instance + co2, # CO₂ instance ) # Creation of the emission data for the individual nodes. - capture_data = CaptureEnergyEmissions(0.9) emission_data = EmissionsEnergy() + # Line above: `EmissionsEnergy` implies that the emissions data corresponds to + # emissions through fuel usage as calculated by the CO₂ intensity and efficiency. + capture_data = CaptureEnergyEmissions(0.9) + # Line above: `CaptureEnergyEmissions` implies that the emissions data corresponds + # to emissions through fuel usage as calculated by the CO₂ intensity and efficiency. + # 90 % of the CO₂ emissions are captured as given by the value 0.9. # 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 CO₂ storage. @@ -68,24 +73,24 @@ function generate_example_network() FixedProfile(100), # Capacity in MW FixedProfile(30), # Variable OPEX in EUR/MW FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(NG => 1), # Output from the Node, in this case, NG + Dict(ng => 1), # Output from the Node, in this case, ng ), RefSource( "coal source", # Node id FixedProfile(100), # Capacity in MW FixedProfile(9), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(Coal => 1), # Output from the Node, in this case, coal + Dict(coal => 1), # Output from the Node, in this case, coal ), RefNetworkNode( "NG+CCS power plant", # Node id FixedProfile(25), # Capacity in MW FixedProfile(5.5), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(NG => 2), # Input to the node with input ratio - Dict(Power => 1, CO2 => 1), # Output from the node with output ratio - # Line above: CO2 is required as output for variable definition, but the - # value does not matter + Dict(ng => 2), # Input to the node with input ratio + Dict(power => 1, co2 => 1), # Output from the node with output ratio + # Line above: `co2` is required as output for variable definition, but the + # value does not matter as it is not utilized in the model. [capture_data], # Additional data for emissions and CO₂ capture ), RefNetworkNode( @@ -93,34 +98,39 @@ function generate_example_network() FixedProfile(25), # Capacity in MW FixedProfile(6), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(Coal => 2.5), # Input to the node with input ratio - Dict(Power => 1), # Output from the node with output ratio + Dict(coal => 2.5), # Input to the node with input ratio + Dict(power => 1), # Output from the node with output ratio [emission_data], # Additional data for emissions ), RefStorage{AccumulatingEmissions}( - "CO2 storage", # Node id + "CO₂ storage", # Node id StorCapOpex( FixedProfile(60), # Charge capacity in t/h FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h 8h) ), StorCap(FixedProfile(600)), # Storage capacity in t - CO2, # Stored resource - Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio - # Line above: This implies that storing CO₂ requires Power - Dict(CO2 => 1), # Output from the node with output ratio - # In practice, for CO₂ storage, this is never used. + co2, # Stored resource + Dict(co2 => 1, power => 0.02), # Input resource with input ratio + # Line above: This implies that storing CO₂ requires power + Dict(co2 => 1), # Output from the node with output ratio + # Line above: In the case of `AccumulatingEmissions`, you must provide the + # stored resource as one of the keys. Its value does however not matter as the + # outlet flow value is fixed to 0. ), RefSink( "electricity demand", # Node id OperationalProfile([20, 30, 40, 30]), # Demand in MW Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio + Dict(power => 1), # Energy demand and corresponding ratio ), ] # Connect all nodes with the availability node for the overall energy/mass balance + # NOTE: This hard coding based on indexing is error prone. It is in general advised to + # use a mapping dictionary to avoid any problems when introducing new technology + # nodes. links = [ Direct("Av-NG_pp", nodes[1], nodes[4], Linear()) Direct("Av-coal_pp", nodes[1], nodes[5], Linear()) @@ -134,6 +144,8 @@ function generate_example_network() ] # Input data structure + # It is also explained on + # https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/public/case_element/ case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) return case, model end diff --git a/examples/network_invest.jl b/examples/network_invest.jl index fc69d5a6..a6841f87 100644 --- a/examples/network_invest.jl +++ b/examples/network_invest.jl @@ -24,19 +24,19 @@ the natural gas power plant with CCS and the CO₂ storage node. function generate_example_network_investment() @info "Generate case data - Simple network example with investments" - # Define the different resources and their emission intensity in tCO2/MWh - 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] + # Define the different resources and their emission intensity in t CO₂/MWh + ng = ResourceEmit("ng", 0.2) + coal = ResourceCarrier("coal", 0.35) + power = ResourceCarrier("power", 0.0) + co2 = ResourceEmit("CO₂", 1.0) + products = [ng, coal, power, co2] # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 + op_duration = 2 # Each operational period has a duration of 2 (hours) op_number = 4 # There are in total 4 operational periods operational_periods = SimpleTimes(op_number, op_duration) - # Each operational period should correspond to a duration of 2 h while a duration if 1 + # Each operational period should correspond to a duration of 2 h while a duration of 1 # of a strategic period should correspond to a year. # This implies, that a strategic period is 8760 times longer than an operational period, # resulting in the values below as "/year". @@ -46,21 +46,26 @@ function generate_example_network_investment() T = TwoLevel(4, 1, operational_periods; op_per_strat) model = InvestmentModel( Dict( # Emission cap for CO₂ in t/year and for NG in MWh/year - CO2 => StrategicProfile([170, 150, 130, 110]) * 1000, - NG => FixedProfile(1e6), + co2 => StrategicProfile([170, 150, 130, 110]) * 1000, + ng => FixedProfile(1e6), ), Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh - CO2 => FixedProfile(0), - NG => FixedProfile(0), + co2 => FixedProfile(0), + ng => FixedProfile(0), ), - CO2, # CO2 instance + co2, # CO₂ instance 0.07, # Discount rate in absolute value ) # Creation of the emission data for the individual nodes. - capture_data = CaptureEnergyEmissions(0.9) emission_data = EmissionsEnergy() + # Line above: `EmissionsEnergy` implies that the emissions data corresponds to + # emissions through fuel usage as calculated by the CO₂ intensity and efficiency. + capture_data = CaptureEnergyEmissions(0.9) + # Line above: `CaptureEnergyEmissions` implies that the emissions data corresponds + # to emissions through fuel usage as calculated by the CO₂ intensity and efficiency. + # 90 % of the CO₂ emissions are captured as given by the value 0.9. # 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 CO₂ storage. @@ -71,33 +76,35 @@ function generate_example_network_investment() FixedProfile(100), # Capacity in MW FixedProfile(30), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(NG => 1), # Output from the Node, in this case, NG + Dict(ng => 1), # Output from the Node, in this case, ng ), RefSource( "coal source", # Node id FixedProfile(100), # Capacity in MW FixedProfile(9), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(Coal => 1), # Output from the Node, in this case, coal + Dict(coal => 1), # Output from the Node, in this case, coal ), RefNetworkNode( "NG+CCS power plant", # Node id FixedProfile(0), # Capacity in MW FixedProfile(5.5), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(NG => 2), # Input to the node with input ratio - Dict(Power => 1, CO2 => 0), # Output from the node with output ratio - # Line above: CO2 is required as output for variable definition, but the - # value does not matter + Dict(ng => 2), # Input to the node with input ratio + Dict(power => 1, co2 => 0), # Output from the node with output ratio + # Line above: `co2` is required as output for variable definition, but the + # value does not matter as it is not utilized in the model. [ capture_data, # Additional data for emissions and CO₂ capture SingleInvData( - FixedProfile(600 * 1e3), # Capex in EUR/MW - FixedProfile(40), # Max installed capacity [MW] + FixedProfile(600 * 1e3), # Capex in EUR/MW + FixedProfile(40), # Maximum installed capacity [MW] SemiContinuousInvestment(FixedProfile(5), FixedProfile(40)), # Line above: Investment mode with the following arguments: - # 1. argument: min added capactity per sp [MW] - # 2. argument: max added capactity per sp [MW] + # 1. argument: minimum added capactity per sp [MW] + # 2. argument: maximum added capactity per sp [MW] + # `SemiContinuousInvestment` implies that one either invests at least in + # the minimum added capacity, or not at all. ), ], ), @@ -106,33 +113,37 @@ function generate_example_network_investment() FixedProfile(40), # Capacity in MW FixedProfile(6), # Variable OPEX in EUR/MWh FixedProfile(0), # Fixed OPEX in EUR/MW/year - Dict(Coal => 2.5), # Input to the node with input ratio - Dict(Power => 1), # Output from the node with output ratio + Dict(coal => 2.5), # Input to the node with input ratio + Dict(power => 1), # Output from the node with output ratio [emission_data], # Additonal data for emissions ), RefStorage{AccumulatingEmissions}( - "CO2 storage", # Node id + "CO₂ storage", # Node id StorCapOpex( FixedProfile(0), # Charge capacity in t/h FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h year) ), StorCap(FixedProfile(1e8)), # Storage capacity in t - CO2, # Stored resource - Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio - # Line above: This implies that storing CO₂ requires Power - Dict(CO2 => 1), # Output from the node with output ratio - # In practice, for CO₂ storage, this is never used. + co2, # Stored resource + Dict(co2 => 1, power => 0.02), # Input resource with input ratio + # Line above: This implies that storing CO₂ requires power + Dict(co2 => 1), # Output from the node with output ratio + # Line above: In the case of `AccumulatingEmissions`, you must provide the + # stored resource as one of the keys. Its value does however not matter as the + # outlet flow value is fixed to 0. [ StorageInvData( charge = NoStartInvData( - FixedProfile(200 * 1e3), # CAPEX [EUR/(t/h)] - FixedProfile(60), # Max installed capacity [EUR/(t/h)] + FixedProfile(200 * 1e3), # CAPEX [EUR/(t/h)] + FixedProfile(60), # Maximum installed capacity [EUR/(t/h)] ContinuousInvestment(FixedProfile(0), FixedProfile(5)), # Line above: Investment mode with the following arguments: # 1. argument: min added capactity per sp [t/h] # 2. argument: max added capactity per sp [t/h] - UnlimitedLife(), # Lifetime mode + # `ContinuousInvestment` implies you can invest in a capacity between + # 0 and 5 t/h in each strategic period + UnlimitedLife(), # Lifetime mode corresponding to no retirement of capacity ), ), ], @@ -142,11 +153,14 @@ function generate_example_network_investment() OperationalProfile([20, 30, 40, 30]), # Demand in MW Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio + Dict(power => 1), # Energy demand and corresponding ratio ), ] # Connect all nodes with the availability node for the overall energy/mass balance + # NOTE: This hard coding based on indexing is error prone. It is in general advised to + # use a mapping dictionary to avoid any problems when introducing new technology + # nodes. links = [ Direct("Av-NG_pp", nodes[1], nodes[4], Linear()) Direct("Av-coal_pp", nodes[1], nodes[5], Linear()) @@ -160,6 +174,8 @@ function generate_example_network_investment() ] # Input data structure + # It is also explained on + # https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/public/case_element/ case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) return case, model end @@ -180,7 +196,7 @@ pretty_table( header = [:StrategicPeriod, :InvestCapacity], ), ) -@info "Invested capacity for the CO2 storage in the beginning of the +@info "Invested capacity for the CO₂ storage in the beginning of the individual strategic periods" pretty_table( JuMP.Containers.rowtable( diff --git a/examples/sink_source.jl b/examples/sink_source.jl index b026e570..208d1116 100644 --- a/examples/sink_source.jl +++ b/examples/sink_source.jl @@ -22,13 +22,13 @@ the source adjusts to the demand. function generate_example_ss() @info "Generate case data - Simple sink-source example" - # Define the different resources and their emission intensity in tCO2/MWh - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [Power, CO2] + # Define the different resources and their emission intensity in t CO₂/MWh + power = ResourceCarrier("power", 0.0) + co2 = ResourceEmit("CO₂", 1.0) + products = [power, co2] # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 + op_duration = 2 # Each operational period has a duration of 2 (hours) op_number = 4 # There are in total 4 operational periods operational_periods = SimpleTimes(op_number, op_duration) @@ -41,9 +41,9 @@ function generate_example_ss() # Creation of the time structure and global data T = TwoLevel(2, 1, operational_periods; op_per_strat) model = OperationalModel( - Dict(CO2 => FixedProfile(10)), # Emission cap for CO₂ in t/8h - Dict(CO2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t - CO2, # CO₂ instance + Dict(co2 => FixedProfile(10)), # Emission cap for CO₂ in t/8h + Dict(co2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t + co2, # CO₂ instance ) # Create the individual test nodes, corresponding to a system with an electricity @@ -54,23 +54,28 @@ function generate_example_ss() FixedProfile(50), # Capacity in MW FixedProfile(30), # Variable OPEX in EUR/MW FixedProfile(0), # Fixed OPEX in EUR/MW/8h - Dict(Power => 1), # Output from the Node, in this case, Power + Dict(power => 1), # Output from the Node, in this case, power ), RefSink( "electricity demand", # Node id OperationalProfile([20, 30, 40, 30]), # Demand in MW Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio + Dict(power => 1), # Energy demand and corresponding ratio ), ] - # Connect all nodes with the availability node for the overall energy/mass balance + # Connect the two nodes + # NOTE: This hard coding based on indexing is error prone. It is in general advised to + # use a mapping dictionary to avoid any problems when introducing new technology + # nodes. links = [ Direct("source-demand", nodes[1], nodes[2], Linear()), ] # Input data structure + # It is also explained on + # https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/public/case_element/ case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) return case, model end diff --git a/examples/sink_source_invest.jl b/examples/sink_source_invest.jl index 4826de40..afe6e044 100644 --- a/examples/sink_source_invest.jl +++ b/examples/sink_source_invest.jl @@ -15,25 +15,25 @@ using PrettyTables using TimeStruct """ - generate_example_ss_investment(lifemode = RollingLife; discount_rate = 0.05) + generate_example_ss_investment() Generate the data for an example consisting of an electricity source and sink. The electricity source has initially no capacity. Hence, investments are required. """ -function generate_example_ss_investment(lifemode = RollingLife; discount_rate = 0.05) +function generate_example_ss_investment() @info "Generate case data - Simple sink-source example" - # Define the different resources and their emission intensity in tCO2/MWh - Power = ResourceCarrier("Power", 0.0) - CO2 = ResourceEmit("CO2", 1.0) - products = [Power, CO2] + # Define the different resources and their emission intensity in t CO₂/MWh + power = ResourceCarrier("power", 0.0) + co2 = ResourceEmit("CO₂", 1.0) + products = [power, co2] # Variables for the individual entries of the time structure - op_duration = 2 # Each operational period has a duration of 2 + op_duration = 2 # Each operational period has a duration of 2c op_number = 4 # There are in total 4 operational periods operational_periods = SimpleTimes(op_number, op_duration) - # Each operational period should correspond to a duration of 2 h while a duration if 1 + # Each operational period should correspond to a duration of 2 h while a duration of 1 # of a strategic period should correspond to a year. # This implies, that a strategic period is 8760 times longer than an operational period, # resulting in the values below as "/year". @@ -46,29 +46,30 @@ function generate_example_ss_investment(lifemode = RollingLife; discount_rate = # Create the global data model = InvestmentModel( - Dict(CO2 => FixedProfile(10)), # Emission cap for CO₂ in t/year - Dict(CO2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t - CO2, # CO₂ instance - discount_rate, # Discount rate in absolute value + Dict(co2 => FixedProfile(10)), # Emission cap for CO₂ in t/year + Dict(co2 => FixedProfile(0)), # Emission price for CO₂ in EUR/t + co2, # CO₂ instance + 0.05, # Discount rate in absolute value ) - # The lifetime of the technology is 15 years, requiring reinvestment in the # 5th investment period lifetime = FixedProfile(15) # Create the investment data for the source node investment_data_source = SingleInvData( - FixedProfile(300 * 1e3), # capex [€/MW] - FixedProfile(50), # max installed capacity [MW] + FixedProfile(300 * 1e3), # CAPEX [€/MW] + FixedProfile(50), # Maximum installed capacity [MW] ContinuousInvestment(FixedProfile(0), FixedProfile(30)), # Line above: Investment mode with the following arguments: - # 1. argument: min added capactity per sp [MW] - # 2. argument: max added capactity per sp [MW] - lifemode(lifetime), # Lifetime mode + # 1. argument: minimum added capactity per sp [MW] + # 2. argument: maximum added capactity per sp [MW] + # `ContinuousInvestment` implies you can invest in a capacity between 0 and 30 MW in + # each strategic period + RollingLife(lifetime), # Lifetime mode + # Line above: As default we are using `RollingLife` with a lifetime of 15 years ) - # Create the individual test nodes, corresponding to a system with an electricity # demand/sink and source nodes = [ @@ -77,7 +78,7 @@ function generate_example_ss_investment(lifemode = RollingLife; discount_rate = FixedProfile(0), # Capacity in MW FixedProfile(10), # Variable OPEX in EUR/MW FixedProfile(5), # Fixed OPEX in EUR/MW/year - Dict(Power => 1), # Output from the Node, in this case, Power + Dict(power => 1), # Output from the Node, in this case, power [investment_data_source], # Additional data used for adding the investment data ), RefSink( @@ -85,16 +86,21 @@ function generate_example_ss_investment(lifemode = RollingLife; discount_rate = OperationalProfile([20, 30, 40, 30]), # Demand in MW Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)), # Line above: Surplus and deficit penalty for the node in EUR/MWh - Dict(Power => 1), # Energy demand and corresponding ratio + Dict(power => 1), # Energy demand and corresponding ratio ), ] - # Connect all nodes with the availability node for the overall energy/mass balance + # Connect the two nodes + # NOTE: This hard coding based on indexing is error prone. It is in general advised to + # use a mapping dictionary to avoid any problems when introducing new technology + # nodes. links = [ Direct("source-demand", nodes[1], nodes[2], Linear()), ] # Input data structure + # It is also explained on + # https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/public/case_element/ case = Case(T, products, [nodes, links], [[get_nodes, get_links]]) return case, model end diff --git a/src/structures/case.jl b/src/structures/case.jl index 8058a568..8e17f9d8 100644 --- a/src/structures/case.jl +++ b/src/structures/case.jl @@ -18,11 +18,11 @@ for providing the input to a model. - **`products::Vector{<:Resource}`** are the resources that should be incorporated into the model. !!! tip - It must containt all [`ResourceEmit`](@ref) in `EnergyModelsBase`, but it is not + It must contain all [`ResourceEmit`](@ref)s in `EnergyModelsBase`, but it is not necessary that the [`ResourceCarrier`](@ref) are included. It is however advisable to include all resources. - **`elements::Vector{Vector}`** are the vectors of [`AbstractElement`](@ref) - that should be included in the analysis. It must contain at least vectors of nodes and + that should be included in the analysis. It must contain at least the vectors of nodes and links for an analysis to be useful. - **`couplings::Vector{Vector{Function}}`** are the couplings between the individual function element types. These elements are represented through a corresponding function, *e.g.*, diff --git a/src/structures/model.jl b/src/structures/model.jl index 377c5000..cb50c75f 100644 --- a/src/structures/model.jl +++ b/src/structures/model.jl @@ -64,8 +64,8 @@ This abstract model type should be used when creating additional [`EnergyModel`] that should utilize investments. !!! note - Although it is declared within `EnergyModelsBase`, its concrete is only accessible if - `EnergyModelsInvestments` is loaded + Although it is declared within `EnergyModelsBase`, its concrete type is only accessible + if `EnergyModelsInvestments` is loaded An example for additional types is given by the inclusion of, *e.g.*, `SDDP`. """ diff --git a/test/test_modeltype.jl b/test/test_modeltype.jl index bbd4cb21..b1d177f4 100644 --- a/test/test_modeltype.jl +++ b/test/test_modeltype.jl @@ -48,7 +48,6 @@ @testset "Emission cap" begin # Test that the emission cap is enforced in the case when it may lead to a deficit - cap = 30.0 # Emission cap in a strategic period em_data = EmissionsProcess(Dict(CO2 => 1.0)) em_cap = FixedProfile(cap) @@ -64,7 +63,7 @@ general_tests(m, case) # Test that the strategic emission limits hold - # - constraints_emissions(m, 𝒩, 𝒯, 𝒫, modeltype::EnergyModel + # - constraints_emissions(m, 𝒩, 𝒯, 𝒫, modeltype::EnergyModel) @test all( value.(m[:emissions_strategic][t_inv, CO2]) ≈ cap for t_inv ∈ 𝒯ᴵⁿᵛ, atol ∈ TEST_ATOL @@ -78,7 +77,6 @@ @testset "Emission price" begin # Test that the price for emissions is correctly calculated when there is not deficit - cap = 40.0 # Emission cap in a strategic period price = 1.0 # Emission price per emitted unit of CO2 em_data = EmissionsProcess(Dict(CO2 => 1.0)) From 83a311adcbacb4b0281ddc324b9b79a9578fd413 Mon Sep 17 00:00:00 2001 From: Julian Straus Date: Tue, 23 Sep 2025 09:55:58 +0200 Subject: [PATCH 2/2] Added processing routines --- NEWS.md | 2 +- examples/network.jl | 74 +++++++++++++++++++++------ examples/network_invest.jl | 93 +++++++++++++++++++++++++++------- examples/sink_source.jl | 53 ++++++++++++++++--- examples/sink_source_invest.jl | 84 ++++++++++++++++++++++++------ test/Project.toml | 1 + test/runtests.jl | 4 +- test/test_examples.jl | 9 ++++ 8 files changed, 260 insertions(+), 60 deletions(-) diff --git a/NEWS.md b/NEWS.md index e4a58496..3926ca20 100644 --- a/NEWS.md +++ b/NEWS.md @@ -4,7 +4,7 @@ ### Minor updates -* Added additional comments to the examples for a simplified understanding. +* Added additional comments (and processing routines) to the examples for improving the understanding. ## Version 0.9.2 (2025-07-03) diff --git a/examples/network.jl b/examples/network.jl index 26dbfc23..10d3d3cc 100644 --- a/examples/network.jl +++ b/examples/network.jl @@ -155,21 +155,63 @@ case, model = generate_example_network() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = run_model(case, model, optimizer) +""" + process_network_results(m, case) + +Function for processing the results to be represented in the a table afterwards. +""" +function process_network_results(m, case) + # Extract the nodes and resources from the case data + ng_ccs_pp, coal_pp, = get_nodes(case)[[4, 5]] + co2 = get_products(case)[4] + 𝒯ⁱⁿᵛ = strategic_periods(get_time_struct(case)) + + # Node variables + coal_pp_use = sort( # Capacity usage of the coal pp + [( + t_inv=t_inv, + val=sum(value.(m[:cap_use][coal_pp, t])*scale_op_sp(t_inv, t) for t ∈ t_inv) + ) for t_inv ∈ 𝒯ⁱⁿᵛ], + by = x -> x.t_inv, + ) + ng_ccs_pp_use = sort( # Capacity usage of the ng pp + [( + t_inv=t_inv, + val=sum(value.(m[:cap_use][ng_ccs_pp, t])*scale_op_sp(t_inv, t) for t ∈ t_inv) + ) for t_inv ∈ 𝒯ⁱⁿᵛ], + by = x -> x.t_inv, + ) + + # Emission variables + strat_emit = sort( # Strategic emissions + JuMP.Containers.rowtable( + value, + m[:emissions_strategic][:, co2]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + + # Set up the individual named tuples as a single named tuple + table = [( + t_inv = repr(con_1.t_inv), + coal_pp_use = round(con_1.val; digits=1), + ng_ccs_pp_use = round(con_2.val; digits=1), + CO2_emissions = round(con_3.val; digits=1), + ) for (con_1, con_2, con_3) ∈ + zip(coal_pp_use, ng_ccs_pp_use, strat_emit) + ] + return table +end + # Display some results -ng_ccs_pp, coal_pp, = get_nodes(case)[[4, 5]] -@info "Capacity usage of the coal power plant" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:cap_use][coal_pp, :]; - header = [:t, :Value], - ), -) -@info "Capacity usage of the natural gas + CCS power plant" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:cap_use][ng_ccs_pp, :]; - header = [:t, :Value], - ), +table = process_network_results(m, case) + +@info( + "Individual strategic results from the simple network:\n" * + "The coal power plant is the preferred power generation unit due to the generation costs.\n" * + "Its usage declines however in subsequent strategic period due to the emission constraints.\n" * + "It is replaced by the natural gas power plant with CO₂ capture as the total strategic\n" * + "emissions follow the emission limits." ) +pretty_table(table) diff --git a/examples/network_invest.jl b/examples/network_invest.jl index a6841f87..f917a71b 100644 --- a/examples/network_invest.jl +++ b/examples/network_invest.jl @@ -185,23 +185,80 @@ case, model = generate_example_network_investment() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = run_model(case, model, optimizer) +""" + process_network_investment_results(m, case) + +Function for processing the results to be represented in the a table afterwards. +""" +function process_network_investment_results(m, case) + # Extract the nodes and resources from the case data + ng_ccs_pp, coal_pp, co2_stor = get_nodes(case)[[4, 5, 6]] + co2 = get_products(case)[4] + 𝒯ⁱⁿᵛ = strategic_periods(get_time_struct(case)) + + # Node variables + coal_pp_use = sort( # Capacity usage of the coal pp + [( + t_inv=t_inv, + val=sum(value.(m[:cap_use][coal_pp, t])*scale_op_sp(t_inv, t) for t ∈ t_inv) + ) for t_inv ∈ 𝒯ⁱⁿᵛ], + by = x -> x.t_inv, + ) + ng_ccs_pp_use = sort( # Capacity usage of the ng pp + [( + t_inv=t_inv, + val=sum(value.(m[:cap_use][ng_ccs_pp, t])*scale_op_sp(t_inv, t) for t ∈ t_inv) + ) for t_inv ∈ 𝒯ⁱⁿᵛ], + by = x -> x.t_inv, + ) + ng_ccs_pp_add = sort( # Added capacity of the ng pp + JuMP.Containers.rowtable( + value, + m[:cap_add][ng_ccs_pp, :]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + co2_stor_add = sort( # Added capacity of the CO₂ storage + JuMP.Containers.rowtable( + value, + m[:stor_charge_add][co2_stor, :]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + + # Emission variables + strat_emit = sort( # Strategic emissions + JuMP.Containers.rowtable( + value, + m[:emissions_strategic][:, co2]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + + # Set up the individual named tuples as a single named tuple + table = [( + t_inv = repr(con_1.t_inv), + coal_pp_use = round(con_1.val; digits=1), + ng_ccs_pp_use = round(con_2.val; digits=1), + ng_ccs_pp_invest = round(con_3.val; digits=1), + CO2_storage_invest = round(con_4.val; digits=1), + CO2_emissions = round(con_5.val; digits=0), + ) for (con_1, con_2, con_3, con_4, con_5) ∈ + zip(coal_pp_use, ng_ccs_pp_use, ng_ccs_pp_add, co2_stor_add, strat_emit) + ] + return table +end + # Display some results -ng_ccs_pp, CO2_stor, = get_nodes(case)[[4, 6]] -@info "Invested capacity for the natural gas plant in the beginning of the \ -individual strategic periods" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:cap_add][ng_ccs_pp, :]; - header = [:StrategicPeriod, :InvestCapacity], - ), -) -@info "Invested capacity for the CO₂ storage in the beginning of the -individual strategic periods" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:stor_charge_add][CO2_stor, :]; - header = [:StrategicPeriod, :InvestCapacity], - ), +table = process_network_investment_results(m, case) + +@info( + "Individual strategic results from the simple network:\n" * + "We have overinvestments in the natural gas power plant in strategic period 1 as the\n" * + "investment is semi continuous. The investments in the CO₂ storage is however happening\n" * + "in all strategic periods as it is continuous." ) +pretty_table(table) diff --git a/examples/sink_source.jl b/examples/sink_source.jl index 208d1116..81b2c126 100644 --- a/examples/sink_source.jl +++ b/examples/sink_source.jl @@ -85,13 +85,50 @@ case, model = generate_example_ss() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = run_model(case, model, optimizer) +""" + process_ss_results(m, case) + +Function for processing the results to be represented in the a table afterwards. +""" +function process_ss_results(m, case) + # Extract the nodes from the case data + source, sink = get_nodes(case) + + # Node variables + source_use = sort( # Usage of the source node + JuMP.Containers.rowtable( + value, + m[:cap_use][source, :]; + header = [:t, :val], + ), + by = x -> x.t, + ) + sink_use = sort( # Usage of the source node + JuMP.Containers.rowtable( + value, + m[:cap_use][sink, :]; + header = [:t, :val], + ), + by = x -> x.t, + ) + + # Set up the individual named tuples as a single named tuple + table = [( + t = repr(con_1.t), + source_use = round(con_1.val; digits=1), + sink_use = round(con_2.val; digits=1), + ) for (con_1, con_2, ) ∈ + zip(source_use, sink_use, ) + ] + return table +end + # Display some results -source, sink = get_nodes(case) -@info "Capacity usage of the power source" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:cap_use][source, :]; - header = [:t, :Value], - ), +table = process_ss_results(m, case) + +@info( + "Individual operational results from the source-sink example:\n" * + "The capacity usage of the source and the sink node are the same as the penalty for not\n" * + "delivering power is significantly higher than the variable OPEX in the source node." ) +pretty_table(table) diff --git a/examples/sink_source_invest.jl b/examples/sink_source_invest.jl index afe6e044..3218d9cf 100644 --- a/examples/sink_source_invest.jl +++ b/examples/sink_source_invest.jl @@ -110,21 +110,73 @@ case, model = generate_example_ss_investment() optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true) m = run_model(case, model, optimizer) +""" + process_ss_investment_results(m, case) + +Function for processing the results to be represented in the a table afterwards. +""" +function process_ss_investment_results(m, case) + # Extract the nodes and time structure from the case data + source, sink = get_nodes(case) + 𝒯ⁱⁿᵛ = strategic_periods(get_time_struct(case)) + + # Node variables + source_current = sort( # Capacity of the source node + JuMP.Containers.rowtable( + value, + m[:cap_current][source, :]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + source_add = sort( # Investments in the source node + JuMP.Containers.rowtable( + value, + m[:cap_add][source, :]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + source_remove = sort( # Retirement from the source node + JuMP.Containers.rowtable( + value, + m[:cap_rem][source, :]; + header = [:t_inv, :val], + ), + by = x -> x.t_inv, + ) + + sink_deficit = sort( # Sink deficit in each strategic period + [( + t_inv=t_inv, + val=sum(value.(m[:sink_deficit][sink, t])*scale_op_sp(t_inv, t) for t ∈ t_inv) + ) for t_inv ∈ 𝒯ⁱⁿᵛ], + by = x -> x.t_inv, + ) + + # Set up the individual named tuples as a single named tuple + table = [( + t_inv = repr(con_1.t_inv), + capacity = round(con_1.val; digits=1), + capacity_addition = round(con_2.val; digits=1), + capacity_removal = round(con_3.val; digits=1), + deficit = round(con_4.val; digits=1), + ) for (con_1, con_2, con_3, con_4) ∈ + zip(source_current, source_add, source_remove, sink_deficit) + ] + return table +end + # Display some results -source, sink = get_nodes(case) -@info "Invested capacity for the source in the beginning of the individual strategic periods" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:cap_add][source, :]; - header = [:StrategicPeriod, :InvestCapacity], - ), -) -@info "Retired capacity of the source at the end of the individual strategic periods" -pretty_table( - JuMP.Containers.rowtable( - value, - m[:cap_rem][source, :]; - header = [:StrategicPeriod, :InvestCapacity], - ), +table = process_ss_investment_results(m, case) + +@info( + "Individual strategic results from the source-sink example:\n" * + "The source node receives investments in the first strategic period. However, due to the\n" * + "maximum capacity additions, we still see a deficit. It is hence required to invest also\n" * + "in the second strategic period. The capacities are removed at the end of the lifetime\n" * + "given by 3 strategic periods (3*5 years = 15 years). As a consequence, reinvestments are\n" * + "neccesary in strategic period 4. Note that the removal is happening at the end of a\n" * + "strategic period." ) +pretty_table(table) diff --git a/test/Project.toml b/test/Project.toml index 98d133e0..367040c8 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -2,6 +2,7 @@ EnergyModelsInvestments = "fca3f8eb-b383-437d-8e7b-aac76bb2004f" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" TimeStruct = "f9ed5ce0-9f41-4eaa-96da-f38ab8df101c" diff --git a/test/runtests.jl b/test/runtests.jl index e259ebfd..b12417d6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,9 @@ -using EnergyModelsBase using HiGHS using JuMP +using Logging using Test + +using EnergyModelsBase using TimeStruct const EMB = EnergyModelsBase diff --git a/test/test_examples.jl b/test/test_examples.jl index bd1d2c83..546b6362 100644 --- a/test/test_examples.jl +++ b/test/test_examples.jl @@ -1,4 +1,10 @@ @testset "Run examples" begin + # Get the global logger and set the loglevel to Warn + logger_org = global_logger() + logger_new = ConsoleLogger(Warn) + global_logger(logger_new) + + # Iterate through all examples and test the examples exdir = joinpath(@__DIR__, "../examples") files = filter(endswith(".jl"), readdir(exdir)) for file ∈ files @@ -10,4 +16,7 @@ end end Pkg.activate(@__DIR__) + + # Reset the loglevel + global_logger(logger_org) end