Skip to content

Commit 19a6e8c

Browse files
authored
Improved example descriptions (#71)
* Improved example descriptions * Added processing routines
1 parent 7df9124 commit 19a6e8c

12 files changed

Lines changed: 411 additions & 168 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+
## Unversioned
4+
5+
### Minor updates
6+
7+
* Added additional comments (and processing routines) to the examples for improving the understanding.
8+
39
## Version 0.9.2 (2025-07-03)
410

511
### Bugfixes

docs/src/nodes/sink.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ The variables of [`Sink`](@ref) nodes include:
5353
- [``\texttt{opex\_fixed}``](@ref man-opt_var-opex)
5454
- [``\texttt{cap\_use}``](@ref man-opt_var-cap)
5555
- [``\texttt{cap\_inst}``](@ref man-opt_var-cap)
56-
- [``\texttt{flow\_out}``](@ref man-opt_var-flow)
56+
- [``\texttt{flow\_in}``](@ref man-opt_var-flow)
5757
- [``\texttt{sink\_surplus}``](@ref man-opt_var-sink)
5858
- [``\texttt{sink\_deficit}``](@ref man-opt_var-sink)
5959
- [``\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
110110

111111
```math
112112
\begin{aligned}
113-
\texttt{opex\_var}[n, t_{inv}] = & \\
114-
\sum_{t \in t_{inv}} & surplus\_penalty(n, t) \times \texttt{sink\_surplus}[n, t] + \\ &
115-
deficit\_penalty(n, t) \times \texttt{sink\_deficit}[n, t] \times \\ &
116-
scale\_op\_sp(t_{inv}, t)
113+
\texttt{opex\_var}[n, t_{inv}] = & scale\_op\_sp(t_{inv}, t) \times \\
114+
\sum_{t \in t_{inv}} & ( surplus\_penalty(n, t) \times \texttt{sink\_surplus}[n, t] + \\ &
115+
deficit\_penalty(n, t) \times \texttt{sink\_deficit}[n, t]) \\ &
116+
117117
\end{aligned}
118118
```
119119

examples/network.jl

Lines changed: 98 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ more expensive natural gas power plant with CCS to reduce emissions.
2323
function generate_example_network()
2424
@info "Generate case data - Simple network example"
2525

26-
# Define the different resources and their emission intensity in tCO2/MWh
27-
NG = ResourceEmit("NG", 0.2)
28-
Coal = ResourceCarrier("Coal", 0.35)
29-
Power = ResourceCarrier("Power", 0.0)
30-
CO2 = ResourceEmit("CO2", 1.0)
31-
products = [NG, Coal, Power, CO2]
26+
# Define the different resources and their emission intensity in t CO₂/MWh
27+
ng = ResourceEmit("NG", 0.2)
28+
coal = ResourceCarrier("Coal", 0.35)
29+
power = ResourceCarrier("Power", 0.0)
30+
co2 = ResourceEmit("CO₂", 1.0)
31+
products = [ng, coal, power, co2]
3232

3333
# Variables for the individual entries of the time structure
34-
op_duration = 2 # Each operational period has a duration of 2
34+
op_duration = 2 # Each operational period has a duration of 2 (hours)
3535
op_number = 4 # There are in total 4 operational periods
3636
operational_periods = SimpleTimes(op_number, op_duration)
3737

@@ -45,19 +45,24 @@ function generate_example_network()
4545
T = TwoLevel(4, 1, operational_periods; op_per_strat)
4646
model = OperationalModel(
4747
Dict( # Emission cap for CO₂ in t/8h and for NG in MWh/8h
48-
CO2 => StrategicProfile([160, 140, 120, 100]),
49-
NG => FixedProfile(1e6),
48+
co2 => StrategicProfile([160, 140, 120, 100]),
49+
ng => FixedProfile(1e6),
5050
),
5151
Dict( # Emission price for CO₂ in EUR/t and for NG in EUR/MWh
52-
CO2 => FixedProfile(0),
53-
NG => FixedProfile(0),
52+
co2 => FixedProfile(0),
53+
ng => FixedProfile(0),
5454
),
55-
CO2, # CO2 instance
55+
co2, # CO₂ instance
5656
)
5757

5858
# Creation of the emission data for the individual nodes.
59-
capture_data = CaptureEnergyEmissions(0.9)
6059
emission_data = EmissionsEnergy()
60+
# Line above: `EmissionsEnergy` implies that the emissions data corresponds to
61+
# emissions through fuel usage as calculated by the CO₂ intensity and efficiency.
62+
capture_data = CaptureEnergyEmissions(0.9)
63+
# Line above: `CaptureEnergyEmissions` implies that the emissions data corresponds
64+
# to emissions through fuel usage as calculated by the CO₂ intensity and efficiency.
65+
# 90 % of the CO₂ emissions are captured as given by the value 0.9.
6166

6267
# Create the individual test nodes, corresponding to a system with an electricity demand/sink,
6368
# coal and nautral gas sources, coal and natural gas (with CCS) power plants and CO₂ storage.
@@ -68,59 +73,64 @@ function generate_example_network()
6873
FixedProfile(100), # Capacity in MW
6974
FixedProfile(30), # Variable OPEX in EUR/MW
7075
FixedProfile(0), # Fixed OPEX in EUR/MW/8h
71-
Dict(NG => 1), # Output from the Node, in this case, NG
76+
Dict(ng => 1), # Output from the Node, in this case, ng
7277
),
7378
RefSource(
7479
"coal source", # Node id
7580
FixedProfile(100), # Capacity in MW
7681
FixedProfile(9), # Variable OPEX in EUR/MWh
7782
FixedProfile(0), # Fixed OPEX in EUR/MW/8h
78-
Dict(Coal => 1), # Output from the Node, in this case, coal
83+
Dict(coal => 1), # Output from the Node, in this case, coal
7984
),
8085
RefNetworkNode(
8186
"NG+CCS power plant", # Node id
8287
FixedProfile(25), # Capacity in MW
8388
FixedProfile(5.5), # Variable OPEX in EUR/MWh
8489
FixedProfile(0), # Fixed OPEX in EUR/MW/8h
85-
Dict(NG => 2), # Input to the node with input ratio
86-
Dict(Power => 1, CO2 => 1), # Output from the node with output ratio
87-
# Line above: CO2 is required as output for variable definition, but the
88-
# value does not matter
90+
Dict(ng => 2), # Input to the node with input ratio
91+
Dict(power => 1, co2 => 1), # Output from the node with output ratio
92+
# Line above: `co2` is required as output for variable definition, but the
93+
# value does not matter as it is not utilized in the model.
8994
[capture_data], # Additional data for emissions and CO₂ capture
9095
),
9196
RefNetworkNode(
9297
"coal power plant", # Node id
9398
FixedProfile(25), # Capacity in MW
9499
FixedProfile(6), # Variable OPEX in EUR/MWh
95100
FixedProfile(0), # Fixed OPEX in EUR/MW/8h
96-
Dict(Coal => 2.5), # Input to the node with input ratio
97-
Dict(Power => 1), # Output from the node with output ratio
101+
Dict(coal => 2.5), # Input to the node with input ratio
102+
Dict(power => 1), # Output from the node with output ratio
98103
[emission_data], # Additional data for emissions
99104
),
100105
RefStorage{AccumulatingEmissions}(
101-
"CO2 storage", # Node id
106+
"CO₂ storage", # Node id
102107
StorCapOpex(
103108
FixedProfile(60), # Charge capacity in t/h
104109
FixedProfile(9.1), # Storage variable OPEX for the charging in EUR/t
105110
FixedProfile(0) # Storage fixed OPEX for the charging in EUR/(t/h 8h)
106111
),
107112
StorCap(FixedProfile(600)), # Storage capacity in t
108-
CO2, # Stored resource
109-
Dict(CO2 => 1, Power => 0.02), # Input resource with input ratio
110-
# Line above: This implies that storing CO₂ requires Power
111-
Dict(CO2 => 1), # Output from the node with output ratio
112-
# In practice, for CO₂ storage, this is never used.
113+
co2, # Stored resource
114+
Dict(co2 => 1, power => 0.02), # Input resource with input ratio
115+
# Line above: This implies that storing CO₂ requires power
116+
Dict(co2 => 1), # Output from the node with output ratio
117+
# Line above: In the case of `AccumulatingEmissions`, you must provide the
118+
# stored resource as one of the keys. Its value does however not matter as the
119+
# outlet flow value is fixed to 0.
113120
),
114121
RefSink(
115122
"electricity demand", # Node id
116123
OperationalProfile([20, 30, 40, 30]), # Demand in MW
117124
Dict(:surplus => FixedProfile(0), :deficit => FixedProfile(1e6)),
118125
# Line above: Surplus and deficit penalty for the node in EUR/MWh
119-
Dict(Power => 1), # Energy demand and corresponding ratio
126+
Dict(power => 1), # Energy demand and corresponding ratio
120127
),
121128
]
122129

123130
# Connect all nodes with the availability node for the overall energy/mass balance
131+
# NOTE: This hard coding based on indexing is error prone. It is in general advised to
132+
# use a mapping dictionary to avoid any problems when introducing new technology
133+
# nodes.
124134
links = [
125135
Direct("Av-NG_pp", nodes[1], nodes[4], Linear())
126136
Direct("Av-coal_pp", nodes[1], nodes[5], Linear())
@@ -134,6 +144,8 @@ function generate_example_network()
134144
]
135145

136146
# Input data structure
147+
# It is also explained on
148+
# https://energymodelsx.github.io/EnergyModelsBase.jl/stable/library/public/case_element/
137149
case = Case(T, products, [nodes, links], [[get_nodes, get_links]])
138150
return case, model
139151
end
@@ -143,21 +155,63 @@ case, model = generate_example_network()
143155
optimizer = optimizer_with_attributes(HiGHS.Optimizer, MOI.Silent() => true)
144156
m = run_model(case, model, optimizer)
145157

158+
"""
159+
process_network_results(m, case)
160+
161+
Function for processing the results to be represented in the a table afterwards.
162+
"""
163+
function process_network_results(m, case)
164+
# Extract the nodes and resources from the case data
165+
ng_ccs_pp, coal_pp, = get_nodes(case)[[4, 5]]
166+
co2 = get_products(case)[4]
167+
𝒯ⁱⁿᵛ = strategic_periods(get_time_struct(case))
168+
169+
# Node variables
170+
coal_pp_use = sort( # Capacity usage of the coal pp
171+
[(
172+
t_inv=t_inv,
173+
val=sum(value.(m[:cap_use][coal_pp, t])*scale_op_sp(t_inv, t) for t t_inv)
174+
) for t_inv 𝒯ⁱⁿᵛ],
175+
by = x -> x.t_inv,
176+
)
177+
ng_ccs_pp_use = sort( # Capacity usage of the ng pp
178+
[(
179+
t_inv=t_inv,
180+
val=sum(value.(m[:cap_use][ng_ccs_pp, t])*scale_op_sp(t_inv, t) for t t_inv)
181+
) for t_inv 𝒯ⁱⁿᵛ],
182+
by = x -> x.t_inv,
183+
)
184+
185+
# Emission variables
186+
strat_emit = sort( # Strategic emissions
187+
JuMP.Containers.rowtable(
188+
value,
189+
m[:emissions_strategic][:, co2];
190+
header = [:t_inv, :val],
191+
),
192+
by = x -> x.t_inv,
193+
)
194+
195+
# Set up the individual named tuples as a single named tuple
196+
table = [(
197+
t_inv = repr(con_1.t_inv),
198+
coal_pp_use = round(con_1.val; digits=1),
199+
ng_ccs_pp_use = round(con_2.val; digits=1),
200+
CO2_emissions = round(con_3.val; digits=1),
201+
) for (con_1, con_2, con_3)
202+
zip(coal_pp_use, ng_ccs_pp_use, strat_emit)
203+
]
204+
return table
205+
end
206+
146207
# Display some results
147-
ng_ccs_pp, coal_pp, = get_nodes(case)[[4, 5]]
148-
@info "Capacity usage of the coal power plant"
149-
pretty_table(
150-
JuMP.Containers.rowtable(
151-
value,
152-
m[:cap_use][coal_pp, :];
153-
header = [:t, :Value],
154-
),
155-
)
156-
@info "Capacity usage of the natural gas + CCS power plant"
157-
pretty_table(
158-
JuMP.Containers.rowtable(
159-
value,
160-
m[:cap_use][ng_ccs_pp, :];
161-
header = [:t, :Value],
162-
),
208+
table = process_network_results(m, case)
209+
210+
@info(
211+
"Individual strategic results from the simple network:\n" *
212+
"The coal power plant is the preferred power generation unit due to the generation costs.\n" *
213+
"Its usage declines however in subsequent strategic period due to the emission constraints.\n" *
214+
"It is replaced by the natural gas power plant with CO₂ capture as the total strategic\n" *
215+
"emissions follow the emission limits."
163216
)
217+
pretty_table(table)

0 commit comments

Comments
 (0)