ewlya commited on
Commit
4407904
·
verified ·
1 Parent(s): 181f1d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +883 -290
app.py CHANGED
@@ -1,347 +1,940 @@
1
- # %%
2
  # -*- coding: utf-8 -*-
3
  """
4
- Spyder Editor
5
 
6
- This is a temporary script file.
7
  """
8
 
9
-
10
-
11
-
12
- from numpy import arange
13
- import xarray as xr
14
- import highspy
15
- from linopy import Model, EQUAL
16
  import pandas as pd
 
17
  import plotly.express as px
18
  import streamlit as st
 
 
 
19
  import sourced as src
20
- st.set_page_config(layout="wide")
21
- # you can create columns to better manage the flow of your page
22
- # this command makes 3 columns of equal width
23
- col1, col2, col3, col4 = st.columns(4)
24
- col1.header("Data Input")
25
- col4.header("Download Results")
26
-
27
- # Color dictionary for figures
28
- color_dict = {'Biomass': 'lightgreen',
29
- 'Lignite': 'brown',
30
- 'Fossil Gas': 'grey',
31
- 'Fossil Hard coal': 'darkgrey',
32
- 'Fossil Oil': 'maroon',
33
- 'RoR': 'aquamarine',
34
- 'Hydro Water Reservoir': 'azure',
35
- 'Nuclear': 'orange',
36
- 'PV': 'yellow',
37
- 'WindOff': 'darkblue',
38
- 'WindOn': 'green',
39
- 'H2': 'crimson',
40
- 'Pumped Hydro Storage': 'lightblue',
41
- 'Battery storages': 'red',
42
- 'Electrolyzer': 'olive'}
43
-
44
- # %%
45
- with col1:
46
- with open('Input_Jahr_2021.xlsx', 'rb') as f:
47
- st.download_button('Download Excel Template', f, file_name='Input_Jahr_2021.xlsx') # Defaults to 'application/octet-stream'
48
-
49
- #url_excel = r'Input_Jahr_2021.xlsx'
50
- url_excel = st.file_uploader(label = 'Excel Upload')
51
-
52
-
53
- if url_excel == None:
54
- url_excel = r'Input_Jahr_2021.xlsx'
55
- sets_dict, params_dict= src.load_data_from_excel(url_excel, load_from_pickle_flag = True)
56
- with col4:
57
- st.write('Running with standard data')
58
- else:
59
- sets_dict, params_dict= src.load_data_from_excel(url_excel, load_from_pickle_flag = False)
60
- with col4:
61
- st.write('Running with user data')
62
-
63
- # # %%
64
-
65
- def timstep_aggregate(time_steps_aggregate, xr ):
66
- return xr.rolling( t = time_steps_aggregate).mean().sel(t = t[0::time_steps_aggregate])
67
-
68
- #s_t_r_iRes = timstep_aggregate(6,s_t_r_iRes)
69
-
70
-
71
-
72
- # %%
73
- #sets_dict, params_dict= src.load_data_from_excel(url_excel,write_to_pickle_flag=True)
74
-
75
- # %%
76
-
77
-
78
-
79
- #sets_dict, params_dict= load_data_from_excel(url_excel, load_from_pickle_flag = False)
80
-
81
-
82
- dt = 6
83
- # Unpack sets_dict into the workspace
84
- t = sets_dict['t']
85
- i = sets_dict['i']
86
- iSto = sets_dict['iSto']
87
- iConv = sets_dict['iConv']
88
- iPtG = sets_dict['iPtG']
89
- iRes = sets_dict['iRes']
90
- iHyRes = sets_dict['iHyRes']
91
-
92
- # Unpack params_dict into the workspace
93
- l_co2 = params_dict['l_co2']
94
- p_co2 = params_dict['p_co2']
95
-
96
- eff_i = params_dict['eff_i']
97
- c_fuel_i = params_dict['c_fuel_i']
98
- c_other_i = params_dict['c_other_i']
99
- c_inv_i = params_dict['c_inv_i']
100
- co2_factor_i = params_dict['co2_factor_i']
101
- #c_var_i = params_dict['c_var_i']
102
- K_0_i = params_dict['K_0_i']
103
- e2p_iSto = params_dict['e2p_iSto']
104
- # Aggregate time series
105
- D_t = timstep_aggregate(dt,params_dict['D_t'])
106
- s_t_r_iRes = timstep_aggregate(dt,params_dict['s_t_r_iRes'])
107
- h_t = timstep_aggregate(dt,params_dict['h_t'])
108
- t = D_t.get_index('t')
109
- partial_year_factor = (8760/len(t))/dt
110
-
111
- # Sliders and input boxes for parameters
112
- with col2:
113
- # Slider for CO2 limit [mio. t]
114
- l_co2 = st.slider(value=int(params_dict['l_co2']), min_value=0, max_value=750, label="CO2 limit [mio. t]", step=50)
115
-
116
- # Slider for H2 price / usevalue [€/MWH_th]
117
- price_h2 = st.slider(value=100, min_value=0, max_value=300, label="Hydrogen price [€/MWh]", step=10)
118
 
119
- for i_idx in c_fuel_i.get_index('i'):
120
- if i_idx in ['Lignite']:
121
- c_fuel_i.loc[i_idx] = st.slider(value=int(c_fuel_i.loc[i_idx]), min_value=0, max_value=300, label=i_idx + ' Price' , step=10)
122
 
123
- dt = st.number_input(label="Length of timesteps [int]", min_value=1, max_value=len(t), value=6, help="Enter only integers between 1 and 8760 (or 8784 for leap years).")
 
 
 
 
 
124
 
125
- with col3:
126
- # Slider for CO2 limit [mio. t]
127
- for i_idx in c_fuel_i.get_index('i'):
128
- if i_idx in ['Fossil Hard coal', 'Fossil Oil','Fossil Gas']:
129
- c_fuel_i.loc[i_idx] = st.slider(value=int(c_fuel_i.loc[i_idx]), min_value=0, max_value=300, label=i_idx + ' Price' , step=10)
130
 
131
- technologies_invest = st.multiselect(label='Technologies for investment', options=i, default=['Lignite','Fossil Gas','Fossil Hard coal','Fossil Oil','PV','WindOff','WindOn','H2','Pumped Hydro Storage','Battery storages'])
132
- technologies_no_invest = [x for x in i if x not in technologies_invest]
 
 
 
 
 
133
 
134
- #time_steps_aggregate = 6
135
- #= xr_profiles.rolling( time_step = time_steps_aggregate).mean().sel(time_step = time[0::time_steps_aggregate])
136
- price_co2 = 0
 
 
137
 
138
- # Aggregate time series
139
- #D_t = timstep_aggregate(dt,params_dict['D_t'])
140
- #s_t_r_iRes = timstep_aggregate(dt,params_dict['s_t_r_iRes'])
141
- #h_t = timstep_aggregate(dt,params_dict['h_t'])
142
- #t = D_t.get_index('t')
143
- #partial_year_factor = (8760/len(t))/dt
144
 
145
- #technologies_no_invest = st.multiselect(label='Technolgy invest', options=i)
146
- #technologies_no_invest = ['Electrolyzer','Biomass','RoR','Hydro Water Reservoir','Nuclear']
147
- # %%
148
- ### Variables
149
- m = Model()
150
 
151
- C_tot = m.add_variables(name = 'C_tot') # Total costs
152
- C_op = m.add_variables(name = 'C_op', lower = 0) # Operational costs
153
- C_inv = m.add_variables(name = 'C_inv', lower = 0) # Investment costs
154
 
155
- K = m.add_variables(coords = [i], name = 'K', lower = 0) # Endogenous capacity
156
- y = m.add_variables(coords = [t,i], name = 'y', lower = 0) # Electricity production --> für Elektrolyseure ausschließen
157
- y_ch = m.add_variables(coords = [t,i], name = 'y_ch', lower = 0) # Electricity consumption --> für alles außer Elektrolyseure und Speicher ausschließen
158
- l = m.add_variables(coords = [t,i], name = 'l', lower = 0) # Storage filling level
159
- w = m.add_variables(coords = [t], name = 'w', lower = 0) # RES curtailment
160
- y_curt = m.add_variables(coords = [t,i], name = 'y_curt', lower = 0)
161
- y_h2 = m.add_variables(coords = [t,i], name = 'y_h2', lower = 0)
162
-
163
- ## Objective function
164
- C_tot = C_op + C_inv
165
- m.add_objective(C_tot)
166
-
167
- ## Costs terms for objective function
168
- # Operational costs minus revenue for produced hydrogen
169
- C_op_sum = m.add_constraints((y * c_fuel_i/eff_i).sum() * dt - (y_h2.sel(i = iPtG) * price_h2).sum() * dt == C_op, name = 'C_op_sum')
170
-
171
- # Investment costs
172
- C_inv_sum = m.add_constraints((K * c_inv_i).sum() == C_inv, name = 'C_inv_sum')
173
-
174
- ## Load serving
175
- loadserve_t = m.add_constraints((((y ).sum(dims = 'i') - y_ch.sum(dims = 'i')) * dt == D_t.sel(t = t) * dt), name = 'load')
176
-
177
- ## Maximum capacity limit
178
- maxcap_i_t = m.add_constraints((y - K <= K_0_i), name = 'max_cap')
179
-
180
- ## Maximum capacity limit
181
- maxcap_invest_i = m.add_constraints((K.sel(i = technologies_no_invest) <= 0), name = 'max_cap_invest')
182
-
183
- ## Prevent power production by PtG
184
- no_power_prod_iPtG_t = m.add_constraints((y.sel(i = iPtG) <= 0), name = 'prevent_ptg_prod')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
- ## Maximum storage charging and discharging
187
- maxcha_iSto_t = m.add_constraints((y.sel(i = iSto) + y_ch.sel(i = iSto) - K.sel(i = iSto) <= K_0_i.sel(i = iSto)), name = 'max_cha')
188
 
189
- ## Maximum electrolyzer capacity
190
- ptg_prod_iPtG_t = m.add_constraints((y_ch.sel(i = iPtG) - K.sel(i = iPtG) <= K_0_i.sel(i = iPtG)), name = 'max_cha_ptg')
 
 
 
191
 
192
- ## PtG H2 production
193
- h2_prod_iPtG_t = m.add_constraints(y_ch.sel(i = iPtG) * eff_i.sel(i = iPtG) == y_h2.sel(i = iPtG), name = 'ptg_h2_prod')
194
 
195
- ## Infeed of renewables
196
- infeed_iRes_t = m.add_constraints((y.sel(i = iRes) - s_t_r_iRes.sel(i = iRes).sel(t = t) * K.sel(i = iRes) + y_curt.sel(i = iRes) == s_t_r_iRes.sel(i = iRes).sel(t = t) * K_0_i.sel(i = iRes)), name = 'infeed')
 
197
 
198
- ## Maximum filling level restriction storage power plant
199
- maxcapsto_iSto_t = m.add_constraints((l.sel(i = iSto) - K.sel(i = iSto) * e2p_iSto.sel(i = iSto) <= K_0_i.sel(i = iSto) * e2p_iSto.sel(i = iSto)), name = 'max_sto_filling')
 
 
 
 
 
 
 
200
 
201
- ## Filling level restriction hydro reservoir
202
- filling_iHydro_t = m.add_constraints(l.sel(i = iHyRes) - l.sel(i = iHyRes).roll(t = -1) + y.sel(i = iHyRes) * dt == h_t.sel(t = t) * dt, name = 'filling_level_hydro')
203
 
204
- ## Filling level restriction other storages
205
- filling_iSto_t = m.add_constraints(l.sel(i = iSto) - (l.sel(i = iSto).roll(t = -1) + (y.sel(i = iSto) / eff_i.sel(i = iSto)) * dt - y_ch.sel(i = iSto) * eff_i.sel(i = iSto) * dt) == 0, name = 'filling_level')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
- ## CO2 limit
208
- CO2_limit = m.add_constraints(((y / eff_i) * co2_factor_i * dt).sum() <= l_co2 * 1_000_000 , name = 'CO2_limit')
209
 
 
210
 
211
- # %%
212
- m.solve(solver_name = 'highs')
213
 
214
- st.markdown("---")
 
 
 
215
 
216
- colb1, colb2 = st.columns(2)
 
217
 
218
- # %%
219
- #c_var_i.to_dataframe(name='VarCosts')
220
- # %%
221
- # Installed Cap
222
- # Assuming df_excel has columns 'All' and 'Capacities'
223
 
224
- fig = px.bar((m.solution['K']+K_0_i).to_dataframe(name='K').reset_index(), \
225
- y='i', x='K', orientation='h', title='Total Installed Capacities [MW]', color='i')
 
 
226
 
227
- #fig
228
 
229
- # %%
230
- total_costs = float(m.solution['C_inv'].values) + float(m.solution['C_op'].values)
231
- total_costs_rounded = round(total_costs/1e9, 2)
232
- df_total_costs = pd.DataFrame({'Total costs':[total_costs]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
 
234
- with colb1:
235
- st.write('Total costs: ' + str(total_costs_rounded) + ' bn. €')
236
 
237
- # %%
238
- #df_Co2_price = pd.DataFrame({'CO2_Price: ':[float(m.constraints['CO2_limit'].dual.values) * (-1)]})
239
- CO2_price = float(m.constraints['CO2_limit'].dual.values) * (-1)
240
- CO2_price_rounded = round(CO2_price, 2)
241
- df_CO2_price = pd.DataFrame({'CO2 price':[CO2_price]})
242
 
243
- with colb2:
244
- #st.write(str(df_Co2_price))
245
- st.write('CO2 price: ' + str(CO2_price_rounded) + ' €/t')
 
 
246
 
247
- # %%
248
- df_new_capacities = m.solution['K'].to_dataframe().reset_index()
249
- fig = px.bar(m.solution['K'].to_dataframe().reset_index(), y='i', x='K', orientation='h', title='New Capacities [MW]', color='i', color_discrete_map=color_dict)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- with colb1:
252
- fig
 
253
 
254
- # %%
255
- i_with_capacity = m.solution['K'].where( m.solution['K'] > 0).dropna(dim = 'i').get_index('i')
256
- df_production = m.solution['y'].sel(i = i_with_capacity).to_dataframe().reset_index()
257
- fig = px.area(m.solution['y'].sel(i = i_with_capacity).to_dataframe().reset_index(), y='y', x='t', title='Production [MWh]', color='i', color_discrete_map=color_dict)
258
- fig.update_traces(line=dict(width=0))
259
- fig.for_each_trace(lambda trace: trace.update(fillcolor = trace.line.color))
260
 
261
- with colb2:
262
- fig
263
 
264
- # %%
265
 
266
- df_price = m.constraints['load'].dual.to_dataframe().reset_index()
267
- #df_price['dual'] = df_price['dual']
268
 
269
- # %%
270
- fig = px.line(df_price, y='dual', x='t', title='Electricity prices [€/MWh]', range_y=[0,250])
271
- with colb1:
272
- fig
 
 
 
273
 
274
- # %%
 
275
 
276
- df_contr_marg = m.constraints['max_cap'].dual.to_dataframe().reset_index()
277
- df_contr_marg['dual'] = df_contr_marg['dual'] / dt * (-1)
 
 
 
 
 
 
 
 
278
 
279
- # %%
 
 
 
 
280
 
281
- fig = px.line(df_contr_marg, y='dual', x='t',title='Contribution margin [€]', color='i', range_y=[0,250], color_discrete_map=color_dict)
282
- with colb2:
283
- fig
284
 
285
- # %%
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- # curtailment
288
- df_curtailment = m.solution['y_curt'].sel(i = iRes).to_dataframe().reset_index()
289
- fig = px.area(m.solution['y_curt'].sel(i = iRes).to_dataframe().reset_index(), y='y_curt', x='t', title='Curtailment [MWh]', color='i', color_discrete_map=color_dict)
290
- fig.update_traces(line=dict(width=0))
291
- fig.for_each_trace(lambda trace: trace.update(fillcolor = trace.line.color))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
 
293
- with colb1:
294
- fig
 
295
 
296
- # %%
297
- df_charging = m.solution['y_ch'].sel(i = iSto).to_dataframe().reset_index()
298
- fig = px.area(m.solution['y_ch'].sel(i = iSto).to_dataframe().reset_index(), y='y_ch', x='t', title='Storage charging [MWh]', color='i', color_discrete_map=color_dict)
299
- fig.update_traces(line=dict(width=0))
300
- fig.for_each_trace(lambda trace: trace.update(fillcolor = trace.line.color))
301
 
302
- with colb2:
303
- fig
304
 
305
- # %%
306
- df_h2_prod = m.solution['y_h2'].sel(i = iPtG).to_dataframe().reset_index()
307
- fig = px.area(m.solution['y_h2'].sel(i = iPtG).to_dataframe().reset_index(), y='y_h2', x='t', title='Hydrogen production [MWh_th]', color='i', color_discrete_map=color_dict)
308
- fig.update_traces(line=dict(width=0))
309
- fig.for_each_trace(lambda trace: trace.update(fillcolor = trace.line.color))
310
 
311
- with colb2:
312
- fig
 
 
 
 
313
 
314
- # %%
315
- ((m.solution['y'] / eff_i) * co2_factor_i * dt).sum()
316
- # %%
 
317
 
318
- import pandas as pd
319
- from io import BytesIO
320
- #from pyxlsb import open_workbook as open_xlsb
321
- import streamlit as st
322
- import xlsxwriter
323
- # %%
324
- output = BytesIO()
325
-
326
-
327
- # Create a Pandas Excel writer using XlsxWriter as the engine
328
- with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
329
- # Write each DataFrame to a different sheet
330
- df_total_costs.to_excel(writer, sheet_name='Total costs', index=False)
331
- df_CO2_price.to_excel(writer, sheet_name='CO2 price', index=False)
332
- df_price.to_excel(writer, sheet_name='Prices', index=False)
333
- df_contr_marg.to_excel(writer, sheet_name='Contribution Margin', index=False)
334
- df_new_capacities.to_excel(writer, sheet_name='Capacities', index=False)
335
- df_production.to_excel(writer, sheet_name='Production', index=False)
336
- df_charging.to_excel(writer, sheet_name='Charging', index=False)
337
- D_t.to_dataframe().reset_index().to_excel(writer, sheet_name='Demand', index=False)
338
- df_curtailment.to_excel(writer, sheet_name='Curtailment', index=False)
339
- df_h2_prod.to_excel(writer, sheet_name='H2 production', index=False)
340
-
341
- with col4:
342
- st.download_button(
343
- label="Download Excel workbook Results",
344
- data=output.getvalue(),
345
- file_name="workbook.xlsx",
346
- mime="application/vnd.ms-excel"
347
- )
 
 
1
  # -*- coding: utf-8 -*-
2
  """
3
+ Energy system optimization model
4
 
5
+ HEMF EWL: Christopher Jahns, Julian Radek, Hendrik Kramer, Cornelia Klüter, Yannik Pflugfelder
6
  """
7
 
8
+ import numpy as np
 
 
 
 
 
 
9
  import pandas as pd
10
+ import xarray as xr
11
  import plotly.express as px
12
  import streamlit as st
13
+ from io import BytesIO
14
+ import xlsxwriter
15
+ from linopy import Model
16
  import sourced as src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
 
 
 
18
 
19
+ # Main function to run the Streamlit app
20
+ def main():
21
+ """
22
+ Main function to set up and solve the energy system optimization model, and handle user inputs and outputs.
23
+ """
24
+ setup_page()
25
 
26
+ settings = load_settings()
 
 
 
 
27
 
28
+ # fill session space with variables that are needed on all pages
29
+ if 'settings' not in st.session_state:
30
+ st.session_state.df = load_settings()
31
+ st.session_state.settings = settings
32
+
33
+ if 'url_excel' not in st.session_state:
34
+ st.session_state.url_excel = None
35
 
36
+ if 'ui_model' not in st.session_state:
37
+ st.session_state.url_excel = None
38
+
39
+ if 'output' not in st.session_state:
40
+ st.session_state.output = BytesIO()
41
 
 
 
 
 
 
 
42
 
43
+ setup_sidebar(st.session_state.settings["df"])
 
 
 
 
44
 
 
 
 
45
 
46
+
47
+ # Navigation
48
+ pg = st.navigation([st.Page(page_model, title=st.session_state.settings["df"].loc['menu_modell',st.session_state.lang], icon="📊"),
49
+ st.Page(page_documentation, title=st.session_state.settings["df"].loc['menu_doku',st.session_state.lang], icon="📓"),
50
+ st.Page(page_about_us, title=st.session_state.settings["df"].loc['menu_impressum',st.session_state.lang], icon="💬")],
51
+ expanded=True)
52
+
53
+ # # Run the app
54
+ pg.run()
55
+
56
+
57
+
58
+
59
+
60
+ # Load settings and initial configurations
61
+ def load_settings():
62
+ """
63
+ Load settings for the app, including colors and language information.
64
+ """
65
+ settings = {
66
+ 'write_pickle_from_standard_excel': True,
67
+ 'df': pd.read_csv("language.csv", encoding="iso-8859-1", index_col="Label", sep=";"),
68
+ 'color_dict': {
69
+ 'Biomass': 'lightgreen',
70
+ 'Lignite': 'brown',
71
+ 'Fossil Gas': 'grey',
72
+ 'Fossil Hard coal': 'darkgrey',
73
+ 'Fossil Oil': 'maroon',
74
+ 'RoR': 'aquamarine',
75
+ 'Hydro Water Reservoir': 'azure',
76
+ 'Nuclear': 'orange',
77
+ 'PV': 'yellow',
78
+ 'WindOff': 'darkblue',
79
+ 'WindOn': 'green',
80
+ 'H2': 'crimson',
81
+ 'Pumped Hydro Storage': 'lightblue',
82
+ 'Battery storages': 'red',
83
+ 'Electrolyzer': 'olive'
84
+ },
85
+ 'colors': {
86
+ 'hemf_blau_dunkel': "#00386c",
87
+ 'hemf_blau_hell': "#00529f",
88
+ 'hemf_rot_dunkel': "#8b310d",
89
+ 'hemf_rot_hell': "#d04119",
90
+ 'hemf_grau': "#dadada"
91
+ }
92
+ }
93
+ return settings
94
+
95
+ # Initialize Streamlit app
96
+ def setup_page():
97
+ """
98
+ Set up the Streamlit page with a specific layout, title, and favicon.
99
+ """
100
+ st.set_page_config(layout="wide", page_title="Investment tool", page_icon="media/favicon.ico", initial_sidebar_state="expanded")
101
+
102
+
103
+ # Sidebar for language and links
104
+ def setup_sidebar(df):
105
+ """
106
+ Set up the sidebar with language options and external links.
107
+ """
108
+ st.session_state.lang = st.sidebar.selectbox("Language", ["🇬🇧 EN", "🇩🇪 DE"], key="foo", label_visibility="collapsed")[-2:]
109
+
110
+ st.sidebar.markdown("""
111
+ <style>
112
+ text-align: center;
113
+ display: block;
114
+ margin-left: auto;
115
+ margin-right: auto;
116
+ width: 100%;
117
+ </style>
118
+ """, unsafe_allow_html=True)
119
+
120
+ with st.sidebar:
121
+ left_co, cent_co, last_co = st.columns([0.1, 0.8, 0.1])
122
+ with cent_co:
123
+ st.text(" ") # add vertical empty space
124
+ ""+df.loc['menu_text', st.session_state.lang]
125
+ st.text(" ") # add vertical empty space
126
+
127
+ if st.session_state.lang == "DE":
128
+ st.write("Schaue vorbei beim")
129
+ st.markdown(r'[Lehrstuhl für Energiewirtschaft](https://www.ewl.wiwi.uni-due.de)', unsafe_allow_html=True)
130
+ elif st.session_state.lang == "EN":
131
+ st.write("Get in touch with the")
132
+ st.markdown(r'[Chair of Management Science and Energy Economics](https://www.ewl.wiwi.uni-due.de/en)', unsafe_allow_html=True)
133
+
134
+ st.text(" ") # add vertical empty space
135
+ st.image("media/Logo_HEMF.svg", width=200)
136
+ st.image("media/Logo_UDE.svg", width=200)
137
+
138
+
139
+ # Load model input data
140
+ def load_model_input(df, write_pickle_from_standard_excel):
141
+ """
142
+ Load model input data from Excel or Pickle based on user input.
143
+ """
144
+ if st.session_state.url_excel is None:
145
+ if write_pickle_from_standard_excel:
146
+ url_excel = r'Input_Jahr_2021.xlsx'
147
+ sets_dict, params_dict = src.load_data_from_excel(url_excel, write_to_pickle_flag=True)
148
+ sets_dict, params_dict = src.load_from_pickle()
149
+ #st.write(df.loc['model_title1.1', st.session_state.lang])
150
+ # st.write('Running with standard data')
151
+ else:
152
+ url_excel = st.session_state.url_excel
153
+ sets_dict, params_dict = src.load_data_from_excel(url_excel, load_from_pickle_flag=False)
154
+ st.write(df.loc['model_title1.2', st.session_state.lang])
155
+
156
+ return sets_dict, params_dict
157
+
158
+
159
+
160
+ def page_documentation():
161
+ """
162
+ Display documentation and mathematical model details.
163
+ """
164
+
165
+ df = st.session_state.settings["df"]
166
+
167
+ st.header(df.loc['constr_header1', st.session_state.lang])
168
+ st.write(df.loc['constr_header2', st.session_state.lang])
169
+
170
+ col1, col2 = st.columns([6, 4])
171
+
172
+ with col1:
173
+ st.header(df.loc['constr_header3', st.session_state.lang])
174
+
175
+ with st.container():
176
+
177
+ # Objective function
178
+ st.subheader(df.loc['constr_subheader_obj_func', st.session_state.lang])
179
+ st.write(df.loc['constr_subheader_obj_func_descr', st.session_state.lang])
180
+ st.latex(r''' \text{min } C^{tot} = C^{op} + C^{inv}''')
181
+
182
+ # Operational costs minus revenue for produced hydrogen
183
+ st.write(df.loc['constr_c_op', st.session_state.lang])
184
+ st.latex(r''' \sum_{i} y_{t,i} \cdot \left( \frac{c^{fuel}_{i}}{\eta_i} + c_{i}^{other} \right) \cdot \Delta t - \sum_{i \in \mathcal{I}^{PtG}} y^{h2}_{t,i} \cdot p^{h2} \cdot \Delta t = C^{op}''')
185
+
186
+ # Investment costs
187
+ st.write(df.loc['constr_c_inv', st.session_state.lang])
188
+ st.latex(r''' \sum_{i} a_{i} \cdot K_{i} \cdot c^{inv}_{i} = C^{inv}''')
189
+
190
+ # Load-serving constraint
191
+ st.write(df.loc['constr_load_serve', st.session_state.lang])
192
+ st.latex(r''' \left( \sum_{i} y_{t,i} - \sum_{i} y_{t,i}^{ch} \right) \cdot \Delta t = D_t \cdot \Delta t, \quad \forall t \in \mathcal{T}''')
193
+
194
+ # Maximum capacity limit
195
+ st.write(df.loc['constr_max_cap', st.session_state.lang])
196
+ st.latex(r''' y_{t,i} - K_{i} \leq K_{0,i}, \quad \forall i \in \mathcal{I}''')
197
+
198
+ # Capacity limits for investment
199
+ st.write(df.loc['constr_inv_cap', st.session_state.lang])
200
+ st.latex(r''' K_{i} \leq 0, \quad \forall i \in \mathcal{I}^{no\_invest}''')
201
+
202
+ # Prevent power production by PtG
203
+ st.write(df.loc['constr_prevent_ptg', st.session_state.lang])
204
+ st.latex(r''' y_{t,i} = 0, \quad \forall i \in \mathcal{I}^{PtG}''')
205
+
206
+ # Prevent charging for non-storage technologies
207
+ st.write(df.loc['constr_prevent_chg', st.session_state.lang])
208
+ st.latex(r''' y_{t,i}^{ch} = 0, \quad \forall i \in \mathcal{I} \setminus \{ \mathcal{I}^{PtG} \cup \mathcal{I}^{Sto} \}''')
209
+
210
+ # Maximum storage charging and discharging
211
+ st.write(df.loc['constr_max_chg', st.session_state.lang])
212
+ st.latex(r''' y_{t,i} + y_{t,i}^{ch} - K_{i} \leq K_{0,i}, \quad \forall i \in \mathcal{I}^{Sto}''')
213
+
214
+ # Maximum electrolyzer capacity
215
+ st.write(df.loc['constr_max_cap_electrolyzer', st.session_state.lang])
216
+ st.latex(r''' y_{t,i}^{ch} - K_{i} \leq K_{0,i}, \quad \forall i \in \mathcal{I}^{PtG}''')
217
+
218
+ # PtG H2 production
219
+ st.write(df.loc['constr_prod_ptg', st.session_state.lang])
220
+ st.latex(r''' y_{t,i}^{ch} \cdot \eta_i = y_{t,i}^{h2}, \quad \forall i \in \mathcal{I}^{PtG}''')
221
+
222
+ # Infeed of renewables
223
+ st.write(df.loc['constr_inf_res', st.session_state.lang])
224
+ st.latex(r''' y_{t,i} + y_{t,i}^{curt} = s_{t,r,i} \cdot (K_{0,i} + K_i), \quad \forall i \in \mathcal{I}^{Res}''')
225
+
226
+ # Maximum filling level restriction for storage power plants
227
+ st.write(df.loc['constr_max_fil_sto', st.session_state.lang])
228
+ # st.latex(r''' l_{t,i} \leq K_{0,i} \cdot e2p_i, \quad \forall i \in \mathcal{I}^{Sto}''')
229
+ st.latex(r''' l_{t,i} \leq (K_{0,i} + K_{i}) \cdot \gamma_i^{Sto}, \quad \forall i \in \mathcal{I}^{Sto}''')
230
+
231
+ # Filling level restriction for hydro reservoir
232
+ st.write(df.loc['constr_fil_hyres', st.session_state.lang])
233
+ st.latex(r''' l_{t+1,i} = l_{t,i} + ( h_{t,i} - y_{t,i}) \cdot \Delta t, \quad \forall i \in \mathcal{I}^{HyRes}''')
234
+
235
+ # Filling level restriction for other storages
236
+ st.write(df.loc['constr_fil_sto', st.session_state.lang])
237
+ st.latex(r''' l_{t+1,i} = l_{t,i} - \left(\frac{y_{t,i}}{\eta_i} - y_{t,i}^{ch} \cdot \eta_i \right) \cdot \Delta t, \quad \forall i \in \mathcal{I}^{Sto}''')
238
+
239
+ # CO2 emission constraint
240
+ st.write(df.loc['constr_co2_lim', st.session_state.lang])
241
+ st.latex(r''' \sum_{t} \sum_{i} \frac{y_{t,i}}{\eta_i} \cdot \chi^{CO2}_i \cdot \Delta t \leq L^{CO2}''')
242
+
243
+
244
+ with col2:
245
+
246
+ symbols_container = st.container()
247
+ with symbols_container:
248
+ st.header(df.loc['symb_header1', st.session_state.lang])
249
+ st.write(df.loc['symb_header2', st.session_state.lang])
250
+
251
+ st.subheader(df.loc['symb_header_sets', st.session_state.lang])
252
+ st.write(f"$\mathcal{{T}}$: {df.loc['symb_time_steps', st.session_state.lang]}")
253
+ st.write(f"$\mathcal{{I}}$: {df.loc['symb_tech', st.session_state.lang]}")
254
+ st.write(f"$\mathcal{{I}}^{{\\text{{Sto}}}}$: {df.loc['symb_sto_tech', st.session_state.lang]}")
255
+ st.write(f"$\mathcal{{I}}^{{\\text{{Conv}}}}$: {df.loc['symb_conv_tech', st.session_state.lang]}")
256
+ st.write(f"$\mathcal{{I}}^{{\\text{{PtG}}}}$: {df.loc['symb_ptg', st.session_state.lang]}")
257
+ st.write(f"$\mathcal{{I}}^{{\\text{{Res}}}}$: {df.loc['symb_res', st.session_state.lang]}")
258
+ st.write(f"$\mathcal{{I}}^{{\\text{{HyRes}}}}$: {df.loc['symb_hyres', st.session_state.lang]}")
259
+ st.write(f"$\mathcal{{I}}^{{\\text{{no\_invest}}}}$: {df.loc['symb_no_inv', st.session_state.lang]}")
260
+
261
+
262
+
263
+ # Variables section
264
+ st.subheader(df.loc['symb_header_variables', st.session_state.lang])
265
+ st.write(f"$C^{{tot}}$: {df.loc['symb_tot_costs', st.session_state.lang]}")
266
+ st.write(f"$C^{{op}}$: {df.loc['symb_c_op', st.session_state.lang]}")
267
+ st.write(f"$C^{{inv}}$: {df.loc['symb_c_inv', st.session_state.lang]}")
268
+ st.write(f"$K_i$: {df.loc['symb_inst_cap', st.session_state.lang]}")
269
+ st.write(f"$y_{{t,i}}$: {df.loc['symb_el_prod', st.session_state.lang]}")
270
+ st.write(f"$y_{{t, i}}^{{ch}}$: {df.loc['symb_el_ch', st.session_state.lang]}")
271
+ st.write(f"$l_{{t,i}}$: {df.loc['symb_sto_fil', st.session_state.lang]}")
272
+ st.write(f"$y_{{t, i}}^{{curt}}$: {df.loc['symb_curt', st.session_state.lang]}")
273
+ st.write(f"$y_{{t, i}}^{{h2}}$: {df.loc['symb_h2_ptg', st.session_state.lang]}")
274
+
275
+
276
+ # Parameters section
277
+ st.subheader(df.loc['symb_header_parameters', st.session_state.lang])
278
+ st.write(f"$D_t$: {df.loc['symb_energy_demand', st.session_state.lang]}")
279
+ st.write(f"$p^{{h2}}$: {df.loc['symb_price_h2', st.session_state.lang]}")
280
+ st.write(f"$c^{{fuel}}_{{i}}$: {df.loc['symb_fuel_costs', st.session_state.lang]}")
281
+ st.write(f"$c_{{i}}^{{other}}$: {df.loc['symb_c_op_other', st.session_state.lang]}")
282
+ st.write(f"$c^{{inv}}_{{i}}$: {df.loc['symb_c_inv_tech', st.session_state.lang]}")
283
+ st.write(f"$a_{{i}}$: {df.loc['symb_annuity', st.session_state.lang]}")
284
+ st.write(f"$\eta_i$: {df.loc['symb_eff_fac', st.session_state.lang]}")
285
+ st.write(f"$K_{{0,i}}$: {df.loc['symb_max_cap_tech', st.session_state.lang]}")
286
+ st.write(f"$\chi^{{CO2}}_i$: {df.loc['symb_co2_fac', st.session_state.lang]}")
287
+ st.write(f"$L^{{CO2}}$: {df.loc['symb_co2_limit', st.session_state.lang]}")
288
+ # st.write(f"$e2p_{{\\text{{Sto}}, i}}$: {df.loc['symb_etp', st.session_state.lang]}")
289
+ st.write(f"$\gamma^{{\\text{{Sto}}}}_{{i}}$: {df.loc['symb_etp', st.session_state.lang]}")
290
+ st.write(f"$s_{{t, r, i}}$: {df.loc['symb_res_supply', st.session_state.lang]}")
291
+ st.write(f"$h_{{t, i}}$: {df.loc['symb_hyRes_inflow', st.session_state.lang]}")
292
+
293
+ # css = float_css_helper(top="50")
294
+ # symbols_container.float(css)
295
 
 
 
296
 
297
+ def page_about_us():
298
+ """
299
+ Display information about the team and the project.
300
+ """
301
+ st.write("About Us/Impressum")
302
 
 
 
303
 
304
+ def page_model(): #, write_pickle_from_standard_excel, color_dict):
305
+ """
306
+ Display the main model page for energy system optimization.
307
 
308
+ This function sets up the user interface for the model input parameters, loads data, and configures the
309
+ optimization model before solving it and presenting the results.
310
+ """
311
+
312
+ df = st.session_state.settings["df"]
313
+ color_dict = st.session_state.settings["color_dict"]
314
+ write_pickle_from_standard_excel = st.session_state.settings["write_pickle_from_standard_excel"]
315
+
316
+
317
 
 
 
318
 
319
+ # Load data from Excel or Pickle
320
+ sets_dict, params_dict = load_model_input(df, write_pickle_from_standard_excel)
321
+
322
+ # Unpack sets_dict into the workspace
323
+ t = sets_dict['t']
324
+ t_original = sets_dict['t']
325
+ i = sets_dict['i']
326
+ iSto = sets_dict['iSto']
327
+ iConv = sets_dict['iConv']
328
+ iPtG = sets_dict['iPtG']
329
+ iRes = sets_dict['iRes']
330
+ iHyRes = sets_dict['iHyRes']
331
+
332
+ # Unpack params_dict into the workspace
333
+ l_co2 = params_dict['l_co2']
334
+ p_co2 = params_dict['p_co2']
335
+ eff_i = params_dict['eff_i']
336
+ life_i = params_dict['life_i']
337
+ c_fuel_i = params_dict['c_fuel_i']
338
+ c_other_i = params_dict['c_other_i']
339
+ c_inv_i = params_dict['c_inv_i']
340
+ co2_factor_i = params_dict['co2_factor_i']
341
+ K_0_i = params_dict['K_0_i']
342
+ e2p_iSto = params_dict['e2p_iSto']
343
+
344
+ # Adjust efficiency for storage technologies
345
+ eff_i.loc[iSto] = np.sqrt(eff_i.loc[iSto]) # Apply square root to cycle efficiency for storage technologies
346
+
347
+ # Create columns for UI layout
348
+ col1, col2 = st.columns([0.30, 0.70], gap="large")
349
+
350
+ # Load input data
351
+ with col1:
352
+
353
+ st.title(df.loc['model_title1', st.session_state.lang])
354
+
355
+ with open('Input_Jahr_2021.xlsx', 'rb') as f:
356
+ st.download_button(df.loc['model_title1.3',st.session_state.lang], f, file_name='Input_Jahr_2021.xlsx') # Download button for Excel template
357
+
358
+ st.session_state.url_excel = st.file_uploader(label=df.loc['model_title1.4',st.session_state.lang]) # File uploader for user Excel file
359
+
360
+ st.title(df.loc['model_title2', st.session_state.lang])
361
+
362
+ st.download_button(label=df.loc['model_title2.1',st.session_state.lang], disabled=(st.session_state.output.getbuffer().nbytes==0), data=st.session_state.output.getvalue(), file_name="workbook.xlsx", mime="application/vnd.ms-excel")
363
 
 
 
364
 
365
+ st.title(df.loc['model_title4', st.session_state.lang])
366
 
 
 
367
 
368
+ if st.session_state.url_excel:
369
+ run_model = st.button(df.loc['model_run_info_excel', st.session_state.lang], key="run_model_button", help=df.loc['run_model_button_info',st.session_state.lang])
370
+ else:
371
+ run_model = st.button(df.loc['model_run_info_gui', st.session_state.lang], key="run_model_button", help=df.loc['run_model_button_info',st.session_state.lang])
372
 
373
+
374
+
375
 
 
 
 
 
 
376
 
377
+ # Set up user interface for parameters
378
+ with col2:
379
+
380
+ st.title(df.loc['model_title3', st.session_state.lang])
381
 
382
+ col1param, col2param = st.columns(2)
383
 
384
+ with col1param:
385
+ l_co2 = st.slider(value=int(params_dict['l_co2']), min_value=0, max_value=750, label=df.loc['model_label_co2',st.session_state.lang], step=50)
386
+ price_h2 = st.slider(value=100, min_value=0, max_value=300, label=df.loc['model_label_h2',st.session_state.lang], step=10)
387
+ for i_idx in params_dict['c_fuel_i'].get_index('i'):
388
+ if i_idx in ['Lignite']:
389
+ params_dict['c_fuel_i'].loc[i_idx] = st.slider(value=int(params_dict['c_fuel_i'].loc[i_idx]),
390
+ min_value=0, max_value=300, label=df.loc[f'model_label_{i_idx}',st.session_state.lang], step=10)
391
+
392
+ dt = st.number_input(label=df.loc['model_label_t',st.session_state.lang], min_value=1, max_value=len(t), value=6,
393
+ help=df.loc['model_label_t_info',st.session_state.lang])
394
+
395
+ with col2param:
396
+ for i_idx in params_dict['c_fuel_i'].get_index('i'):
397
+ if i_idx in ['Fossil Hard coal', 'Fossil Oil', 'Fossil Gas']:
398
+ params_dict['c_fuel_i'].loc[i_idx] = st.slider(value=int(params_dict['c_fuel_i'].loc[i_idx]),
399
+ min_value=0, max_value=300, label=df.loc[f'model_label_{i_idx}',st.session_state.lang], step=10)
400
+
401
+ # Create a dictionary to map German names to English names
402
+ tech_mapping_de_to_en = {
403
+ df.loc[f'tech_{tech.lower()}', 'DE']: df.loc[f'tech_{tech.lower()}', 'EN']
404
+ for tech in sets_dict['i'] if f'tech_{tech.lower()}' in df.index
405
+ }
406
+
407
+ # Set options and default values based on the selected language
408
+ if st.session_state.lang == 'DE':
409
+ # German options for the user interface
410
+ options = [
411
+ df.loc[f'tech_{tech.lower()}', 'DE'] for tech in sets_dict['i'] if f'tech_{tech.lower()}' in df.index
412
+ ]
413
+ default = [
414
+ df.loc[f'tech_{tech.lower()}', 'DE'] for tech in ['Lignite', 'Fossil Gas', 'Fossil Hard coal', 'Fossil Oil', 'PV', 'WindOff', 'WindOn', 'H2', 'Pumped Hydro Storage', 'Battery storages', 'Electrolyzer']
415
+ if f'tech_{tech.lower()}' in df.index
416
+ ]
417
+ else:
418
+ # English options for the user interface
419
+ options = sets_dict['i']
420
+ default = ['Lignite', 'Fossil Gas', 'Fossil Hard coal', 'Fossil Oil', 'PV', 'WindOff', 'WindOn', 'H2', 'Pumped Hydro Storage', 'Battery storages', 'Electrolyzer']
421
+
422
+ # Multiselect for technology options in the user interface
423
+ selected_technologies = st.multiselect(
424
+ label=df.loc['model_label_tech', st.session_state.lang],
425
+ options=options,
426
+ default=[tech for tech in default if tech in options]
427
+ )
428
+
429
+ # If language is German, map selected German names back to their English equivalents
430
+ if st.session_state.lang == 'DE':
431
+ technologies_invest = [tech_mapping_de_to_en[tech] for tech in selected_technologies]
432
+ else:
433
+ technologies_invest = selected_technologies
434
+
435
+ # Technologies that will not be invested in (based on English names)
436
+ technologies_no_invest = [tech for tech in sets_dict['i'] if tech not in technologies_invest]
437
+
438
+
439
+ st.markdown("-------")
440
+
441
+
442
+ # Time series aggregation for various parameters
443
+ D_t = timstep_aggregate(dt, params_dict['D_t'], t)
444
+ s_t_r_iRes = timstep_aggregate(dt, params_dict['s_t_r_iRes'], t)
445
+ h_t = timstep_aggregate(dt, params_dict['h_t'], t)
446
+ t = D_t.get_index('t')
447
+ partial_year_factor = (8760 / len(t)) / dt
448
+
449
+
450
+
451
+
452
+ if run_model:
453
+ # Model setup
454
+ m = Model()
455
+
456
+ # Define Variables
457
+ C_tot = m.add_variables(name='C_tot') # Total costs
458
+ C_op = m.add_variables(name='C_op', lower=0) # Operational costs
459
+ C_inv = m.add_variables(name='C_inv', lower=0) # Investment costs
460
+ K = m.add_variables(coords=[i], name='K', lower=0) # Endogenous capacity
461
+ y = m.add_variables(coords=[t, i], name='y', lower=0) # Electricity production
462
+ y_ch = m.add_variables(coords=[t, i], name='y_ch', lower=0) # Electricity consumption
463
+ l = m.add_variables(coords=[t, i], name='l', lower=0) # Storage filling level
464
+ y_curt = m.add_variables(coords=[t, i], name='y_curt', lower=0) # RES curtailment
465
+ y_h2 = m.add_variables(coords=[t, i], name='y_h2', lower=0) # H2 production
466
+
467
+ # Define Objective function
468
+ C_tot = C_op + C_inv
469
+ m.add_objective(C_tot)
470
+
471
+ # Define Constraints
472
+ # Operational costs minus revenue for produced hydrogen
473
+ m.add_constraints((y * c_fuel_i / eff_i).sum() * dt - (y_h2.sel(i=iPtG) * price_h2).sum() * dt == C_op, name='C_op_sum')
474
+
475
+ # Investment costs
476
+ m.add_constraints((K * c_inv_i).sum() == C_inv, name='C_inv_sum')
477
+
478
+ # Load serving
479
+ m.add_constraints((((y).sum(dims='i') - y_ch.sum(dims='i')) * dt == D_t.sel(t=t) * dt), name='load')
480
+
481
+ # Maximum capacity limit
482
+ m.add_constraints((y - K <= K_0_i), name='max_cap')
483
+
484
+ # Capacity limits for investment
485
+ m.add_constraints((K.sel(i=technologies_no_invest) <= 0), name='max_cap_invest')
486
+
487
+ # Prevent power production by PtG
488
+ m.add_constraints((y.sel(i=iPtG) <= 0), name='prevent_ptg_prod')
489
+
490
+ # Prevent charging for non-storage technologies
491
+ m.add_constraints((y_ch.sel(i=[x for x in i if x not in iPtG and x not in iSto]) <= 0), name='no_charging')
492
+
493
+ # Maximum storage charging and discharging
494
+ m.add_constraints((y.sel(i=iSto) + y_ch.sel(i=iSto) - K.sel(i=iSto) <= K_0_i.sel(i=iSto)), name='max_cha')
495
+
496
+ # Maximum electrolyzer capacity
497
+ m.add_constraints((y_ch.sel(i=iPtG) - K.sel(i=iPtG) <= K_0_i.sel(i=iPtG)), name='max_cha_ptg')
498
+
499
+ # PtG H2 production
500
+ m.add_constraints(y_ch.sel(i=iPtG) * eff_i.sel(i=iPtG) == y_h2.sel(i=iPtG), name='ptg_h2_prod')
501
+
502
+ # Infeed of renewables
503
+ m.add_constraints((y.sel(i=iRes) - s_t_r_iRes.sel(i=iRes).sel(t=t) * K.sel(i=iRes) + y_curt.sel(i=iRes) == s_t_r_iRes.sel(i=iRes).sel(t=t) * K_0_i.sel(i=iRes)), name='infeed')
504
+
505
+ # Maximum filling level restriction for storage power plants
506
+ m.add_constraints((l.sel(i=iSto) - K.sel(i=iSto) * e2p_iSto.sel(i=iSto) <= K_0_i.sel(i=iSto) * e2p_iSto.sel(i=iSto)), name='max_sto_filling')
507
+
508
+ # Filling level restriction for hydro reservoir
509
+ m.add_constraints(l.sel(i=iHyRes) - l.sel(i=iHyRes).roll(t=-1) + y.sel(i=iHyRes) * dt == h_t.sel(t=t) * dt, name='filling_level_hydro')
510
+
511
+ # Filling level restriction for other storages
512
+ m.add_constraints(l.sel(i=iSto) - (l.sel(i=iSto).roll(t=-1) - (y.sel(i=iSto) / eff_i.sel(i=iSto)) * dt + y_ch.sel(i=iSto) * eff_i.sel(i=iSto) * dt) == 0, name='filling_level')
513
+
514
+ # CO2 limit
515
+ m.add_constraints(((y / eff_i) * co2_factor_i * dt).sum() <= l_co2 * 1_000_000, name='CO2_limit')
516
+
517
+ # Solve the model
518
+ m.solve(solver_name='highs')
519
+
520
+ # Prepare columns for figures
521
+ colb1, colb2 = st.columns(2)
522
+
523
+ # Generate and display figures
524
+ st.markdown("---")
525
+
526
+ df_total_costs = plot_total_costs(m, colb1, df)
527
+ df_CO2_price = plot_co2_price(m, colb2, df)
528
+ plot_installed_capacities(m, K_0_i, color_dict)
529
+ df_new_capacities = plot_new_capacities(m, color_dict, colb1, df)
530
+
531
+ # Only plot production for technologies with capacity
532
+ i_with_capacity = m.solution['K'].where(m.solution['K'] > 0).dropna(dim='i').get_index('i')
533
+ df_production = plot_production(m, i_with_capacity, dt, color_dict, colb2, df)
534
+
535
+ df_price = plot_electricity_prices(m, dt, colb1, df)
536
+ df_contr_marg = plot_contribution_margin(m, dt, color_dict, colb2, df)
537
+ df_curtailment = plot_curtailment(m, iRes, color_dict, colb1, df)
538
+ df_charging = plot_storage_charging(m, iSto, color_dict, colb2, df)
539
+ df_h2_prod = plot_hydrogen_production(m, iPtG, color_dict, colb2, df)
540
+
541
+ # Export results
542
+ st.session_state.output = BytesIO()
543
+ with pd.ExcelWriter(st.session_state.output, engine='xlsxwriter') as writer:
544
+ disaggregate_df(df_total_costs, t, t_original, dt).to_excel(writer, sheet_name='Total costs', index=False)
545
+ disaggregate_df(df_CO2_price, t, t_original, dt).to_excel(writer, sheet_name='CO2 price', index=False)
546
+ disaggregate_df(df_price, t, t_original, dt).to_excel(writer, sheet_name='Prices', index=False)
547
+ disaggregate_df(df_contr_marg, t, t_original, dt).to_excel(writer, sheet_name='Contribution Margin', index=False)
548
+ disaggregate_df(df_new_capacities, t, t_original, dt).to_excel(writer, sheet_name='Capacities', index=False)
549
+ disaggregate_df(df_production, t, t_original, dt).to_excel(writer, sheet_name='Production', index=False)
550
+ disaggregate_df(df_charging, t, t_original, dt).to_excel(writer, sheet_name='Charging', index=False)
551
+ disaggregate_df(D_t.to_dataframe().reset_index(), t, t_original, dt).to_excel(writer, sheet_name='Demand', index=False)
552
+ disaggregate_df(df_curtailment, t, t_original, dt).to_excel(writer, sheet_name='Curtailment', index=False)
553
+ disaggregate_df(df_h2_prod, t, t_original, dt).to_excel(writer, sheet_name='H2 production', index=False)
554
+
555
+ st.rerun()
556
+
557
+
558
+ def timstep_aggregate(time_steps_aggregate, xr_data, t):
559
+ """
560
+ Aggregates time steps in the data using rolling mean and selects based on step size.
561
+ """
562
+ return xr_data.rolling(t=time_steps_aggregate).mean().sel(t=t[0::time_steps_aggregate])
563
+
564
+ # Visualization functions
565
+ def plot_installed_capacities(m, K_0_i, color_dict):
566
+ """
567
+ Plots the total installed capacities.
568
+ """
569
+ df_installed_cap = (m.solution['K'] + K_0_i).to_dataframe(name='K').reset_index()
570
+ fig = px.bar(df_installed_cap, y='i', x='K', orientation='h',
571
+ title='Total Installed Capacities [MW]', color='i', color_discrete_map=color_dict)
572
+
573
+ return fig
574
+
575
+
576
+ def plot_total_costs(m, col, df):
577
+ """
578
+ Displays the total costs.
579
+ """
580
+ total_costs = float(m.solution['C_inv'].values) + float(m.solution['C_op'].values)
581
+ total_costs_rounded = round(total_costs / 1e9, 2)
582
+ with col:
583
+ st.write(f"{df.loc['plot_label_total_costs', st.session_state.lang]} {total_costs_rounded}")
584
+ # st.write(f'Total costs: {total_costs_rounded} bn. €')
585
+
586
+ df_total_costs = pd.DataFrame({'Total costs':[total_costs]})
587
+ return df_total_costs
588
+
589
+ def plot_co2_price(m, col, df):
590
+ """
591
+ Displays the CO2 price based on the CO2 constraint dual values.
592
+ """
593
+ CO2_price = float(m.constraints['CO2_limit'].dual.values) * (-1)
594
+ CO2_price_rounded = round(CO2_price, 2)
595
+ df_CO2_price = pd.DataFrame({'CO2 price': [CO2_price]})
596
+ with col:
597
+ st.write(f"{df.loc['plot_label_co2_price', st.session_state.lang]} {CO2_price_rounded}")
598
+
599
+ return df_CO2_price
600
+
601
+
602
+ def plot_new_capacities(m, color_dict, col, df):
603
+ """
604
+ Plots the new capacities installed in MW as a bar chart and pie chart.
605
+ Includes technologies with 0 MW capacity in the bar chart.
606
+ Supports both German and English labels for technologies while ensuring color consistency.
607
+ """
608
+ # Convert the solution for new capacities to a DataFrame
609
+ df_new_capacities = m.solution['K'].round(0).to_dataframe().reset_index()
610
+
611
+ # Store the English technology names in a separate column to maintain color consistency
612
+ df_new_capacities['i_en'] = df_new_capacities['i']
613
+
614
+ # Check if the language is German and map English names to German for display
615
+ if st.session_state.lang == 'DE':
616
+ tech_mapping_en_to_de = {
617
+ df.loc[f'tech_{tech.lower()}', 'EN']: df.loc[f'tech_{tech.lower()}', 'DE']
618
+ for tech in df_new_capacities['i_en'] if f'tech_{tech.lower()}' in df.index
619
+ }
620
+ # Replace the English technology names with German ones for display
621
+ df_new_capacities['i'] = df_new_capacities['i_en'].replace(tech_mapping_en_to_de)
622
+
623
+ # Bar plot for new capacities (including technologies with 0 MW)
624
+ fig_bar = px.bar(df_new_capacities, y='i', x='K', orientation='h',
625
+ title=df.loc['plot_label_new_capacities', st.session_state.lang],
626
+ color='i_en', # Use the English names for consistent coloring
627
+ color_discrete_map=color_dict)
628
+
629
+ # Hide the legend completely since the labels are already next to the bars
630
+ fig_bar.update_layout(showlegend=False)
631
+
632
+ with col:
633
+ st.plotly_chart(fig_bar)
634
+
635
+ # Pie chart for new capacities (only show technologies with K > 0 in pie chart)
636
+ df_new_capacities_filtered = df_new_capacities[df_new_capacities["K"] > 0]
637
+ fig_pie = px.pie(df_new_capacities_filtered, names='i', values='K',
638
+ title=df.loc['plot_label_new_capacities_pie', st.session_state.lang],
639
+ color='i_en', color_discrete_map=color_dict)
640
+
641
+ # Remove English labels (i_en) from the pie chart legend
642
+ fig_pie.update_layout(legend_title_text=df.loc['label_technology', st.session_state.lang])
643
+ fig_pie.for_each_trace(lambda t: t.update(name=df_new_capacities_filtered['i'].iloc[0] if st.session_state.lang == 'DE' else t.name))
644
+
645
+ with col:
646
+ st.plotly_chart(fig_pie)
647
+
648
+ return df_new_capacities
649
+
650
+
651
+ def plot_production(m, i_with_capacity, dt, color_dict, col, df):
652
+ """
653
+ Plots the energy production for technologies with capacity as an area chart.
654
+ Supports both German and English labels for technologies while ensuring color consistency.
655
+ """
656
+ # Convert the production data to a DataFrame
657
+ df_production = m.solution['y'].sel(i=i_with_capacity).to_dataframe().reset_index()
658
+
659
+ # Store the English technology names in a separate column to maintain color consistency
660
+ df_production['i_en'] = df_production['i']
661
+
662
+ # Check if the language is German and map English names to German for display
663
+ if st.session_state.lang == 'DE':
664
+ tech_mapping_en_to_de = {
665
+ df.loc[f'tech_{tech.lower()}', 'EN']: df.loc[f'tech_{tech.lower()}', 'DE']
666
+ for tech in df_production['i_en'] if f'tech_{tech.lower()}' in df.index
667
+ }
668
+ # Replace the English technology names with German ones for display
669
+ df_production['i'] = df_production['i_en'].replace(tech_mapping_en_to_de)
670
+
671
+ # Area plot for energy production
672
+ fig = px.area(df_production, y='y', x='t',
673
+ title=df.loc['plot_label_production', st.session_state.lang],
674
+ color='i_en', # Use the English names for consistent coloring
675
+ color_discrete_map=color_dict)
676
+
677
+ fig.update_traces(line=dict(width=0))
678
+ fig.for_each_trace(lambda trace: trace.update(fillcolor=trace.line.color))
679
+
680
+ with col:
681
+ st.plotly_chart(fig)
682
 
683
+ # Pie chart for total production
684
+ df_production_sum = (df_production.groupby(['i', 'i_en'])['y'].sum() * dt / 1000).round(0).reset_index()
685
 
686
+ # If the language is set to German, display German labels, otherwise use English
687
+ pie_column = 'i' if st.session_state.lang == 'DE' else 'i_en'
 
 
 
688
 
689
+ # Pie chart for total production
690
+ fig_pie = px.pie(df_production_sum, names=pie_column, values='y',
691
+ title=df.loc['plot_label_total_production_pie', st.session_state.lang],
692
+ color='i_en', # Ensure the coloring stays consistent using the 'i_en' column
693
+ color_discrete_map=color_dict)
694
 
695
+ # Update legend title to reflect the correct language
696
+ fig_pie.update_layout(legend_title_text=df.loc['label_technology', st.session_state.lang])
697
+
698
+ with col:
699
+ st.plotly_chart(fig_pie)
700
+
701
+ return df_production
702
+
703
+
704
+ def plot_electricity_prices(m, dt, col, df):
705
+ """
706
+ Plots the electricity price and the price duration curve.
707
+ Supports both German and English labels for the plot titles and axis labels.
708
+ """
709
+ # Convert the dual constraints to a DataFrame
710
+ df_price = m.constraints['load'].dual.to_dataframe().reset_index()
711
+
712
+ # Line plot for electricity prices
713
+ fig_price = px.line(df_price, y='dual', x='t',
714
+ title=df.loc['plot_label_electricity_prices', st.session_state.lang],
715
+ # range_y=[0, 250],
716
+ labels={'dual': df.loc['label_electricity_price', st.session_state.lang],
717
+ 't': df.loc['label_time', st.session_state.lang]})
718
+ with col:
719
+ st.plotly_chart(fig_price)
720
+
721
+ # Create the price duration curve
722
+ df_sorted_price = df_price["dual"].repeat(dt).sort_values(ascending=False).reset_index(drop=True) / int(dt)
723
+ fig_duration = px.line(y=df_sorted_price, x=df_sorted_price.index,
724
+ title=df.loc['plot_label_price_duration_curve', st.session_state.lang],
725
+ # labels={"x": df.loc['label_hours_of_year', st.session_state.lang]},
726
+ # range_y=[0, 250])
727
+ )
728
+ with col:
729
+ st.plotly_chart(fig_duration)
730
+
731
+ return df_price
732
+
733
+
734
+ def plot_contribution_margin(m, dt, color_dict, col, df):
735
+ """
736
+ Plots the contribution margin for each technology.
737
+ Supports both German and English labels for titles and axes while ensuring color consistency.
738
+ """
739
+ # Convert the dual constraints to a DataFrame
740
+ df_contr_marg = m.constraints['max_cap'].dual.to_dataframe().reset_index()
741
+
742
+ # Adjust the 'dual' values for the contribution margin calculation
743
+ df_contr_marg['dual'] = df_contr_marg['dual'] / dt * (-1)
744
+
745
+ # Store the English technology names in a separate column to maintain color consistency
746
+ df_contr_marg['i_en'] = df_contr_marg['i']
747
+
748
+ # Check if the language is German and map English names to German for display
749
+ if st.session_state.lang == 'DE':
750
+ tech_mapping_en_to_de = {
751
+ df.loc[f'tech_{tech.lower()}', 'EN']: df.loc[f'tech_{tech.lower()}', 'DE']
752
+ for tech in df_contr_marg['i_en'] if f'tech_{tech.lower()}' in df.index
753
+ }
754
+ # Replace the English technology names with German ones for display
755
+ df_contr_marg['i'] = df_contr_marg['i_en'].replace(tech_mapping_en_to_de)
756
+
757
+ # Plot contribution margin for each technology
758
+ fig = px.line(df_contr_marg, y='dual', x='t',
759
+ title=df.loc['plot_label_contribution_margin', st.session_state.lang],
760
+ color='i_en', # Use the English names for consistent coloring
761
+ range_y=[0, 250], color_discrete_map=color_dict,
762
+ # labels={
763
+ # 'dual': df.loc['label_contribution_margin', st.session_state.lang],
764
+ # 't': df.loc['label_time', st.session_state.lang],
765
+ # 'i_en': df.loc['label_technology', st.session_state.lang]
766
+ # }
767
+ )
768
+
769
+ # Update legend to display the correct language
770
+ fig.update_layout(legend_title_text=df.loc['label_technology', st.session_state.lang])
771
+
772
+ # For German language, update the legend to show German technology names
773
+ if st.session_state.lang == 'DE':
774
+ fig.for_each_trace(lambda t: t.update(name=df_contr_marg.loc[df_contr_marg['i_en'] == t.name, 'i'].values[0]))
775
 
776
+ # Display the plot
777
+ with col:
778
+ st.plotly_chart(fig)
779
 
780
+ return df_contr_marg
 
 
 
 
 
781
 
 
 
782
 
 
783
 
 
 
784
 
785
+ def plot_curtailment(m, iRes, color_dict, col, df):
786
+ """
787
+ Plots the curtailment of renewable energy.
788
+ Supports both German and English labels for titles and axes while ensuring color consistency.
789
+ """
790
+ # Convert the curtailment solution to a DataFrame
791
+ df_curtailment = m.solution['y_curt'].sel(i=iRes).to_dataframe().reset_index()
792
 
793
+ # Store the English technology names in a separate column to maintain color consistency
794
+ df_curtailment['i_en'] = df_curtailment['i']
795
 
796
+ # Check if the language is German and map English names to German for display
797
+ if st.session_state.lang == 'DE':
798
+ tech_mapping_en_to_de = {
799
+ df.loc[f'tech_{tech.lower()}', 'EN']: df.loc[f'tech_{tech.lower()}', 'DE']
800
+ for tech in df_curtailment['i_en'] if f'tech_{tech.lower()}' in df.index
801
+ }
802
+ # Replace the English technology names with German ones for display
803
+ df_curtailment['i'] = df_curtailment['i_en'].replace(tech_mapping_en_to_de)
804
+ else:
805
+ df_curtailment['i'] = df_curtailment['i_en'] # Use English names if not German
806
 
807
+ # Area plot for curtailment of renewable energy
808
+ fig = px.area(df_curtailment, y='y_curt', x='t',
809
+ title=df.loc['plot_label_curtailment', st.session_state.lang],
810
+ color='i_en', # Use the English names for consistent coloring
811
+ color_discrete_map=color_dict)
812
 
813
+ # Remove line traces and use fill colors for the area plot
814
+ fig.update_traces(line=dict(width=0))
815
+ fig.for_each_trace(lambda trace: trace.update(fillcolor=trace.line.color))
816
 
817
+ # Update the legend title to reflect the correct language (German or English)
818
+ fig.update_layout(legend_title_text=df.loc['label_technology', st.session_state.lang])
819
+
820
+ # For German language, update the legend to show German technology names
821
+ if st.session_state.lang == 'DE':
822
+ fig.for_each_trace(lambda t: t.update(name=df_curtailment.loc[df_curtailment['i_en'] == t.name, 'i'].values[0]))
823
+
824
+ # Display the plot
825
+ with col:
826
+ st.plotly_chart(fig)
827
+
828
+ return df_curtailment
829
+
830
+
831
+
832
+ def plot_storage_charging(m, iSto, color_dict, col, df):
833
+ """
834
+ Plots the charging of storage technologies.
835
+ Supports both German and English labels for titles and axes while ensuring color consistency.
836
+ """
837
+ # Convert the storage charging solution to a DataFrame
838
+ df_charging = m.solution['y_ch'].sel(i=iSto).to_dataframe().reset_index()
839
+
840
+ # Store the English technology names in a separate column to maintain color consistency
841
+ df_charging['i_en'] = df_charging['i']
842
+
843
+ # Check if the language is German and map English names to German for display
844
+ if st.session_state.lang == 'DE':
845
+ tech_mapping_en_to_de = {
846
+ df.loc[f'tech_{tech.lower()}', 'EN']: df.loc[f'tech_{tech.lower()}', 'DE']
847
+ for tech in df_charging['i_en'] if f'tech_{tech.lower()}' in df.index
848
+ }
849
+ # Replace the English technology names with German ones for display
850
+ df_charging['i'] = df_charging['i_en'].replace(tech_mapping_en_to_de)
851
+ else:
852
+ df_charging['i'] = df_charging['i_en'] # Use English names if not German
853
+
854
+ # Area plot for storage charging
855
+ fig = px.area(df_charging, y='y_ch', x='t',
856
+ title=df.loc['plot_label_storage_charging', st.session_state.lang],
857
+ color='i_en', # Use the English names for consistent coloring
858
+ color_discrete_map=color_dict)
859
+
860
+ # Remove line traces and use fill colors for the area plot
861
+ fig.update_traces(line=dict(width=0))
862
+ fig.for_each_trace(lambda trace: trace.update(fillcolor=trace.line.color))
863
+
864
+ # Update the legend title to reflect the correct language (German or English)
865
+ fig.update_layout(legend_title_text=df.loc['label_technology', st.session_state.lang])
866
 
867
+ # For German language, update the legend to show German technology names
868
+ if st.session_state.lang == 'DE':
869
+ fig.for_each_trace(lambda t: t.update(name=df_charging.loc[df_charging['i_en'] == t.name, 'i'].values[0]))
870
+
871
+ # Display the plot
872
+ with col:
873
+ st.plotly_chart(fig)
874
+
875
+ return df_charging
876
+
877
+
878
+
879
+ def plot_hydrogen_production(m, iPtG, color_dict, col, df):
880
+ """
881
+ Plots the hydrogen production.
882
+ Supports both German and English labels for titles and axes while ensuring color consistency.
883
+ """
884
+ # Convert the hydrogen production data to a DataFrame
885
+ df_h2_prod = m.solution['y_h2'].sel(i=iPtG).to_dataframe().reset_index()
886
+
887
+ # Store the English technology names in a separate column to maintain color consistency
888
+ df_h2_prod['i_en'] = df_h2_prod['i']
889
+
890
+ # Check if the language is German and map English names to German for display
891
+ if st.session_state.lang == 'DE':
892
+ tech_mapping_en_to_de = {
893
+ df.loc[f'tech_{tech.lower()}', 'EN']: df.loc[f'tech_{tech.lower()}', 'DE']
894
+ for tech in df_h2_prod['i_en'] if f'tech_{tech.lower()}' in df.index
895
+ }
896
+ # Replace the English technology names with German ones for display
897
+ df_h2_prod['i'] = df_h2_prod['i_en'].replace(tech_mapping_en_to_de)
898
+ else:
899
+ df_h2_prod['i'] = df_h2_prod['i_en'] # Keep English names if not German
900
+
901
+ # Area plot for hydrogen production
902
+ fig = px.area(df_h2_prod, y='y_h2', x='t',
903
+ title=df.loc['plot_label_hydrogen_production', st.session_state.lang],
904
+ color='i_en', # Use the English names for consistent coloring
905
+ color_discrete_map=color_dict)
906
+
907
+ # Remove line traces and use fill colors for the area plot
908
+ fig.update_traces(line=dict(width=0))
909
+ fig.for_each_trace(lambda trace: trace.update(fillcolor=trace.line.color))
910
+
911
+ # Update the legend title to reflect the correct language (German or English)
912
+ fig.update_layout(legend_title_text=df.loc['label_technology', st.session_state.lang])
913
+
914
+ # For German language, update the legend to show German technology names
915
+ if st.session_state.lang == 'DE':
916
+ fig.for_each_trace(lambda t: t.update(name=df_h2_prod.loc[df_h2_prod['i_en'] == t.name, 'i'].values[0]))
917
 
918
+ # Display the plot
919
+ with col:
920
+ st.plotly_chart(fig)
921
 
922
+ return df_h2_prod
 
 
 
 
923
 
 
 
924
 
 
 
 
 
 
925
 
926
+ def disaggregate_df(df, t, t_original, dt):
927
+ """
928
+ Disaggregates the DataFrame based on the original time steps.
929
+ """
930
+ if "t" not in list(df.columns):
931
+ return df
932
 
933
+ df_t_all = pd.DataFrame({"t_all": t_original.to_series(), 't': t.repeat(dt)}).reset_index(drop=True)
934
+ df_output = df.merge(df_t_all, on='t').drop('t', axis=1).rename({'t_all': 't'}, axis=1)
935
+ df_output = df_output[[df_output.columns[-1]] + list(df_output.columns[:-1])]
936
+ return df_output.sort_values('t')
937
 
938
+
939
+ if __name__ == "__main__":
940
+ main()