pommes 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pommes/__init__.py +0 -0
- pommes/common_plots.py +470 -0
- pommes/inputs.yaml +650 -0
- pommes/io/__init__.py +0 -0
- pommes/io/analyse_input_dataset.py +51 -0
- pommes/io/build_input_dataset.py +402 -0
- pommes/io/read_solution.py +176 -0
- pommes/io/save_solution.py +99 -0
- pommes/model/__init__.py +0 -0
- pommes/model/build_model.py +166 -0
- pommes/model/carbon.py +110 -0
- pommes/model/combined.py +216 -0
- pommes/model/conversion.py +248 -0
- pommes/model/net_import.py +129 -0
- pommes/model/repurposing.py +74 -0
- pommes/model/storage.py +309 -0
- pommes/model/transport.py +214 -0
- pommes/model/turpe.py +112 -0
- pommes/post_process.py +213 -0
- pommes/solver/__init__.py +0 -0
- pommes/solver/utils_xpress.py +156 -0
- pommes/tests/conftest.py +448 -0
- pommes/tests/study/test_case/config.yaml +606 -0
- pommes/tests/study/test_case/data/availability.csv +11 -0
- pommes/tests/study/test_case/data/carbon.csv +5 -0
- pommes/tests/study/test_case/data/combined.csv +2 -0
- pommes/tests/study/test_case/data/combined_factor.csv +5 -0
- pommes/tests/study/test_case/data/combined_operation.csv +3 -0
- pommes/tests/study/test_case/data/conversion.csv +7 -0
- pommes/tests/study/test_case/data/conversion_factor.csv +11 -0
- pommes/tests/study/test_case/data/conversion_operation.csv +5 -0
- pommes/tests/study/test_case/data/demand.csv +13 -0
- pommes/tests/study/test_case/data/discount.csv +5 -0
- pommes/tests/study/test_case/data/eco.csv +2 -0
- pommes/tests/study/test_case/data/import_hourly.csv +4 -0
- pommes/tests/study/test_case/data/import_yearly.csv +5 -0
- pommes/tests/study/test_case/data/load_shedding.csv +5 -0
- pommes/tests/study/test_case/data/spillage.csv +5 -0
- pommes/tests/study/test_case/data/storage.csv +4 -0
- pommes/tests/study/test_case/data/storage_factor.csv +10 -0
- pommes/tests/study/test_case/data/transport.csv +7 -0
- pommes/tests/study/test_case/data/turpe.csv +21 -0
- pommes/tests/study/test_case/data/turpe_calendar.csv +11 -0
- pommes/tests/study/test_case/data_source.yaml +41 -0
- pommes/tests/study/test_case/pre_process/pre_process.py +15 -0
- pommes/tests/study/test_case/pre_process/revert_pre_process.py +7 -0
- pommes/tests/study/test_case/pre_process.yaml +16 -0
- pommes/tests/study/test_case/run_study.py +59 -0
- pommes/tests/test_build_input_dataset.py +38 -0
- pommes/tests/test_carbon_net_import.py +90 -0
- pommes/tests/test_combined.py +217 -0
- pommes/tests/test_conversion.py +358 -0
- pommes/tests/test_net_import.py +51 -0
- pommes/tests/test_storage.py +243 -0
- pommes/tests/test_test_case.py +7 -0
- pommes/tests/test_transport.py +252 -0
- pommes/tools/draft_multi_index.py +32 -0
- pommes/utils.py +313 -0
- pommes-0.0.1.dist-info/AUTHORS +6 -0
- pommes-0.0.1.dist-info/LICENCE.txt +21 -0
- pommes-0.0.1.dist-info/METADATA +214 -0
- pommes-0.0.1.dist-info/RECORD +64 -0
- pommes-0.0.1.dist-info/WHEEL +5 -0
- pommes-0.0.1.dist-info/top_level.txt +1 -0
pommes/__init__.py
ADDED
|
File without changes
|
pommes/common_plots.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import plotly.colors as pc
|
|
5
|
+
import plotly.express as px
|
|
6
|
+
import plotly.graph_objects as go
|
|
7
|
+
import xarray as xr
|
|
8
|
+
from matplotlib import pyplot as plt
|
|
9
|
+
from plotly.subplots import make_subplots
|
|
10
|
+
|
|
11
|
+
from pommes.utils import array_to_datetime, get_main_resource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_color_set(size, color_scale_name="Rainbow"):
|
|
15
|
+
color_scale = getattr(pc.sequential, color_scale_name)
|
|
16
|
+
return pc.sample_colorscale(color_scale, [i / (size - 1) for i in range(size)])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_plot(output_folder, name, dpi=300, show=True):
|
|
20
|
+
if not os.path.exists(output_folder):
|
|
21
|
+
os.makedirs(output_folder)
|
|
22
|
+
plt.savefig(f"{output_folder}/{name}.png", dpi=dpi)
|
|
23
|
+
if show:
|
|
24
|
+
plt.show(block=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def plot_power(model_solution, plot_folder, plot_name="power"):
|
|
28
|
+
fig = px.line(
|
|
29
|
+
data_frame=model_solution.operation_conversion_power.to_series().reset_index(),
|
|
30
|
+
x="hour",
|
|
31
|
+
y="operation_conversion_power",
|
|
32
|
+
color="conversion_tech",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if not os.path.exists(plot_folder):
|
|
36
|
+
os.makedirs(plot_folder)
|
|
37
|
+
fig.write_html(f"{plot_folder}/{plot_name}.html")
|
|
38
|
+
return fig
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def plot_energy_balance(
|
|
42
|
+
model_parameters,
|
|
43
|
+
model_solution,
|
|
44
|
+
plot_folder,
|
|
45
|
+
x_axis=None,
|
|
46
|
+
facet_row=None,
|
|
47
|
+
facet_col=None,
|
|
48
|
+
plot_name="energy_balance",
|
|
49
|
+
threshold=1e-6,
|
|
50
|
+
showlegend=False,
|
|
51
|
+
):
|
|
52
|
+
# TODO: avoid sum on year_invest
|
|
53
|
+
s = model_solution
|
|
54
|
+
p = model_parameters
|
|
55
|
+
|
|
56
|
+
# ----------------
|
|
57
|
+
# Data formatting
|
|
58
|
+
# ----------------
|
|
59
|
+
|
|
60
|
+
demand = (-1 * p.demand).expand_dims(category=["demand"])
|
|
61
|
+
load_shedding = s.operation_load_shedding.expand_dims(category=["load_shedding"])
|
|
62
|
+
spillage = (-1 * s.operation_spillage).expand_dims(category=["spillage"])
|
|
63
|
+
conversion = s.operation_conversion_net_generation.rename(dict(conversion_tech="category"))
|
|
64
|
+
combined = xr.DataArray([], coords=dict(category=[]))
|
|
65
|
+
storage = xr.DataArray([], coords=dict(category=[]))
|
|
66
|
+
net_import = xr.DataArray([], coords=dict(category=[]))
|
|
67
|
+
transport = xr.DataArray([], coords=dict(category=[]))
|
|
68
|
+
|
|
69
|
+
if "combined" in p.keys() and p.combined.any():
|
|
70
|
+
combined = s.operation_combined_net_generation.rename(dict(combined_tech="category"))
|
|
71
|
+
if "storage" in p.keys() and p.storage.any():
|
|
72
|
+
storage = s.operation_storage_net_generation.rename(dict(storage_tech="category"))
|
|
73
|
+
if "net_import" in p.keys() and p.net_import.any():
|
|
74
|
+
net_import = s.operation_net_import_net_generation.expand_dims(category=["net_import"])
|
|
75
|
+
if "transport" in p.keys() and p.transport.any():
|
|
76
|
+
transport = s.operation_transport_net_generation.rename(dict(transport_tech="category"))
|
|
77
|
+
|
|
78
|
+
da = xr.concat(
|
|
79
|
+
[demand, load_shedding, spillage, combined, conversion, storage, net_import, transport],
|
|
80
|
+
dim="category",
|
|
81
|
+
coords="all",
|
|
82
|
+
join="outer",
|
|
83
|
+
compat="broadcast_equals",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if len(da.category) > 24:
|
|
87
|
+
color_set = generate_color_set(len(da.category))
|
|
88
|
+
else:
|
|
89
|
+
color_set = pc.qualitative.Dark24
|
|
90
|
+
|
|
91
|
+
color_map = {category: color_set[i] for i, category in enumerate(da.category.values)}
|
|
92
|
+
|
|
93
|
+
if x_axis is None:
|
|
94
|
+
x_axis = da.hour
|
|
95
|
+
|
|
96
|
+
da = da.sum(
|
|
97
|
+
[dim for dim in da.dims if dim not in [facet_row, facet_col, x_axis.dims[0], "category"]]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# ----------------
|
|
101
|
+
# Plot
|
|
102
|
+
# ----------------
|
|
103
|
+
|
|
104
|
+
# Create stacked area chart
|
|
105
|
+
fig = make_subplots(
|
|
106
|
+
rows=1 if facet_row is None else len(da[facet_row]),
|
|
107
|
+
cols=1 if facet_col is None else len(da[facet_col]),
|
|
108
|
+
shared_xaxes="all",
|
|
109
|
+
shared_yaxes=True,
|
|
110
|
+
row_titles=None if facet_row is None else s[facet_row].values.astype("str").tolist(),
|
|
111
|
+
column_titles=None if facet_col is None else s[facet_col].values.astype("str").tolist(),
|
|
112
|
+
y_title="Energy balance (MWh)",
|
|
113
|
+
x_title="Hours (h)",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
add_to_legend = xr.DataArray(True, coords=[da.category])
|
|
117
|
+
|
|
118
|
+
def plot_bar_chart(
|
|
119
|
+
fig_,
|
|
120
|
+
da0,
|
|
121
|
+
row_var_=None,
|
|
122
|
+
col_var_=None,
|
|
123
|
+
row_=None,
|
|
124
|
+
col_=None,
|
|
125
|
+
showlegend_=showlegend,
|
|
126
|
+
add_to_legend_=None,
|
|
127
|
+
):
|
|
128
|
+
for category_ in da0.category.values:
|
|
129
|
+
da_ = da0.sel(category=category_)
|
|
130
|
+
da_ = da_ if row_var_ is None else da_.sel({facet_row: row_var_})
|
|
131
|
+
da_ = da_ if col_var_ is None else da_.sel({facet_col: col_var_})
|
|
132
|
+
da_ = da_.where(abs(da_) > threshold)
|
|
133
|
+
if add_to_legend_ is not None and showlegend_:
|
|
134
|
+
showlegend_ = bool(add_to_legend_.loc[dict(category=category_)])
|
|
135
|
+
add_to_legend_.loc[dict(category=category_)] = False
|
|
136
|
+
if abs(da_).max() > threshold:
|
|
137
|
+
fig_.add_trace(
|
|
138
|
+
go.Bar(
|
|
139
|
+
x=x_axis,
|
|
140
|
+
y=da_,
|
|
141
|
+
marker=dict(color=color_map[category_], line=dict(width=0)),
|
|
142
|
+
name=str(category_),
|
|
143
|
+
showlegend=showlegend_,
|
|
144
|
+
),
|
|
145
|
+
row=1 if row_ is None else row_ + 1,
|
|
146
|
+
col=1 if col_ is None else col_ + 1,
|
|
147
|
+
)
|
|
148
|
+
return fig_
|
|
149
|
+
|
|
150
|
+
for row in [None] if facet_row is None else range(len(da[facet_row])):
|
|
151
|
+
row_var = None if row is None else s[facet_row].values[row]
|
|
152
|
+
for col in [None] if facet_col is None else range(len(da[facet_col])):
|
|
153
|
+
col_var = None if col is None else s[facet_col].values[col]
|
|
154
|
+
|
|
155
|
+
fig = plot_bar_chart(
|
|
156
|
+
fig_=fig,
|
|
157
|
+
da0=da,
|
|
158
|
+
row_var_=row_var,
|
|
159
|
+
col_var_=col_var,
|
|
160
|
+
row_=row,
|
|
161
|
+
col_=col,
|
|
162
|
+
add_to_legend_=add_to_legend,
|
|
163
|
+
showlegend_=showlegend,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if row is not None or col is not None:
|
|
167
|
+
detailed_fig = go.Figure()
|
|
168
|
+
|
|
169
|
+
detailed_fig = plot_bar_chart(
|
|
170
|
+
fig_=detailed_fig,
|
|
171
|
+
da0=da,
|
|
172
|
+
row_var_=row_var,
|
|
173
|
+
col_var_=col_var,
|
|
174
|
+
showlegend_=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
detailed_fig.update_layout(
|
|
178
|
+
title=f"Hourly energy balance for {row_var} in {col_var}",
|
|
179
|
+
xaxis_title="Hour (h)",
|
|
180
|
+
yaxis_title="Energy balance (MWh)",
|
|
181
|
+
barmode="relative",
|
|
182
|
+
bargap=0,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
folder = f"{plot_folder}/{plot_name}"
|
|
186
|
+
if not os.path.exists(folder):
|
|
187
|
+
os.makedirs(folder)
|
|
188
|
+
|
|
189
|
+
detailed_fig.write_html(f"{folder}/{col_var}_{row_var}.html")
|
|
190
|
+
fig.update_layout(title="Hourly energy balance", barmode="relative", bargap=0)
|
|
191
|
+
|
|
192
|
+
if not os.path.exists(plot_folder):
|
|
193
|
+
os.makedirs(plot_folder)
|
|
194
|
+
fig.write_html(
|
|
195
|
+
f"{plot_folder}/{plot_name}.html",
|
|
196
|
+
config={"responsive": False},
|
|
197
|
+
)
|
|
198
|
+
return fig
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def plot_stock_level(
|
|
202
|
+
model_parameters,
|
|
203
|
+
model_solution,
|
|
204
|
+
area,
|
|
205
|
+
plot_folder,
|
|
206
|
+
plot_name="stock_level",
|
|
207
|
+
threshold=1e-6,
|
|
208
|
+
):
|
|
209
|
+
s = model_solution
|
|
210
|
+
p = model_parameters
|
|
211
|
+
|
|
212
|
+
# ----------------
|
|
213
|
+
# Data formatting
|
|
214
|
+
# ----------------
|
|
215
|
+
|
|
216
|
+
if not ("storage" in p.keys() and p.storage):
|
|
217
|
+
Warning("No storage in simulation. No storage level graph generated.")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
da = s.operation_storage_level.sel(area=area, drop=True)
|
|
221
|
+
selection = da.max(dim=[dim for dim in da.dims if dim != "storage_tech"]) > threshold
|
|
222
|
+
da = da.sel(storage_tech=da.storage_tech[selection]).sum("year_inv")
|
|
223
|
+
|
|
224
|
+
# ----------------
|
|
225
|
+
# Plot
|
|
226
|
+
# ----------------
|
|
227
|
+
|
|
228
|
+
fig = make_subplots(
|
|
229
|
+
rows=len(da.storage_tech),
|
|
230
|
+
cols=len(da.year_op),
|
|
231
|
+
shared_xaxes=True,
|
|
232
|
+
shared_yaxes=False,
|
|
233
|
+
row_titles=da.storage_tech.values.tolist(),
|
|
234
|
+
column_titles=da.year_op.values.astype("str").tolist(),
|
|
235
|
+
y_title="Storage level (MWh)",
|
|
236
|
+
x_title="Hours (h)",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
for row in range(len(da.storage_tech)):
|
|
240
|
+
storage_tech = da.storage_tech.values[row]
|
|
241
|
+
|
|
242
|
+
for col in range(len(da.year_op)):
|
|
243
|
+
year_op = da.year_op.values[col]
|
|
244
|
+
|
|
245
|
+
x_axis = array_to_datetime(da.hour.values - 1, year_op)
|
|
246
|
+
|
|
247
|
+
fig.add_trace(
|
|
248
|
+
go.Scatter(
|
|
249
|
+
x=x_axis,
|
|
250
|
+
y=da.sel(storage_tech=storage_tech, year_op=year_op),
|
|
251
|
+
mode="lines",
|
|
252
|
+
name=storage_tech,
|
|
253
|
+
showlegend=True,
|
|
254
|
+
),
|
|
255
|
+
row=row + 1,
|
|
256
|
+
col=col + 1,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
fig.update_layout(title="Hourly storage level")
|
|
260
|
+
|
|
261
|
+
if not os.path.exists(plot_folder):
|
|
262
|
+
os.makedirs(plot_folder)
|
|
263
|
+
fig.write_html(f"{plot_folder}/{plot_name}.html")
|
|
264
|
+
return fig
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def plot_capacities(
|
|
268
|
+
model_parameters,
|
|
269
|
+
model_solution,
|
|
270
|
+
area,
|
|
271
|
+
plot_folder,
|
|
272
|
+
plot_name="installed capacities",
|
|
273
|
+
threshold=1e-6,
|
|
274
|
+
):
|
|
275
|
+
# TODO: add year inv, transport management
|
|
276
|
+
s = model_solution
|
|
277
|
+
p = model_parameters
|
|
278
|
+
|
|
279
|
+
# ----------------
|
|
280
|
+
# Data formatting
|
|
281
|
+
# ----------------
|
|
282
|
+
|
|
283
|
+
ds_0 = s[[name for name in s.data_vars if "operation" in name and "capacity" in name]].sum(
|
|
284
|
+
"year_inv"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
ds_0 = ds_0.sel(area=area, drop=True)
|
|
288
|
+
|
|
289
|
+
if model_parameters.transport:
|
|
290
|
+
ds_0 = ds_0.sel(area_to=area, drop=True).sum("area_from")
|
|
291
|
+
|
|
292
|
+
l_ds = []
|
|
293
|
+
|
|
294
|
+
for variable in ds_0:
|
|
295
|
+
ds = ds_0[variable]
|
|
296
|
+
|
|
297
|
+
coord = [coord for coord in ds.coords if "tech" in coord][0]
|
|
298
|
+
|
|
299
|
+
if threshold > 0:
|
|
300
|
+
selection = ds.max(dim=[dim for dim in ds.dims if dim != coord]) > threshold
|
|
301
|
+
ds = ds.sel({coord: selection})
|
|
302
|
+
|
|
303
|
+
capacity_type = "power"
|
|
304
|
+
if "energy" in variable:
|
|
305
|
+
capacity_type = "energy"
|
|
306
|
+
|
|
307
|
+
ds = ds.to_dataset(name=capacity_type)
|
|
308
|
+
|
|
309
|
+
ds = ds.assign(category=xr.DataArray(coord.split("_tech")[0], coords=[ds[coord]]))
|
|
310
|
+
|
|
311
|
+
ds = ds.rename({coord: "tech"})
|
|
312
|
+
|
|
313
|
+
l_ds.append(ds)
|
|
314
|
+
|
|
315
|
+
ds = xr.merge(l_ds)
|
|
316
|
+
|
|
317
|
+
ds = ds.merge(get_main_resource(p), join="inner")
|
|
318
|
+
|
|
319
|
+
resources = p.resource.sel(
|
|
320
|
+
resource=p.resource.isin(ds.groupby("resource").sum("tech").resource)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
demand = p.demand.max(dim=[dim for dim in p.demand.dims if dim not in ["year_op", "resource"]])
|
|
324
|
+
|
|
325
|
+
# ----------------
|
|
326
|
+
# Plot
|
|
327
|
+
# ----------------
|
|
328
|
+
|
|
329
|
+
category_colors = {
|
|
330
|
+
"conversion": "blue",
|
|
331
|
+
"storage": "red",
|
|
332
|
+
"transport": "green"
|
|
333
|
+
# Add more categories if needed
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# DataArray index by (resource, year_op) True if storage installed for each tuple
|
|
337
|
+
boolean_has_storage = (
|
|
338
|
+
resources.where(np.logical_and(ds.energy > 0, ds.resource == resources))
|
|
339
|
+
.notnull()
|
|
340
|
+
.sum("tech")
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
row_titles = p.year_op.values.astype("str").tolist()
|
|
344
|
+
column_titles = resources.values.astype("str").tolist()
|
|
345
|
+
|
|
346
|
+
storage_legend = True
|
|
347
|
+
show_legend_category = {"conversion": True, "storage": True, "transport": True}
|
|
348
|
+
|
|
349
|
+
fig = make_subplots(
|
|
350
|
+
rows=len(p.year_op),
|
|
351
|
+
cols=len(resources),
|
|
352
|
+
shared_xaxes=True,
|
|
353
|
+
shared_yaxes="all",
|
|
354
|
+
row_titles=row_titles,
|
|
355
|
+
column_titles=column_titles,
|
|
356
|
+
specs=[
|
|
357
|
+
[
|
|
358
|
+
{
|
|
359
|
+
"secondary_y": boolean_has_storage.sel(
|
|
360
|
+
year_op=year_op, resource=resource
|
|
361
|
+
).to_numpy()
|
|
362
|
+
}
|
|
363
|
+
for resource in resources
|
|
364
|
+
]
|
|
365
|
+
for year_op in p.year_op
|
|
366
|
+
],
|
|
367
|
+
# x_title="Hours (h)",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
for row in range(len(p.year_op)):
|
|
371
|
+
year_op = p.year_op.values[row]
|
|
372
|
+
|
|
373
|
+
for col in range(len(resources)):
|
|
374
|
+
resource = resources.values[col]
|
|
375
|
+
|
|
376
|
+
ds_plot = ds.sel(year_op=year_op, tech=ds.resource == resource)
|
|
377
|
+
df_plot = ds_plot.to_dataframe().reset_index().sort_values(by=["category", "tech"])
|
|
378
|
+
|
|
379
|
+
x_axis = df_plot[["category", "tech"]].sort_values(by=["category", "tech"]).to_numpy().T
|
|
380
|
+
|
|
381
|
+
for category in df_plot["category"].unique():
|
|
382
|
+
fig.add_trace(
|
|
383
|
+
go.Bar(
|
|
384
|
+
x=x_axis,
|
|
385
|
+
y=df_plot.where(df_plot["category"] == category, np.nan)["power"],
|
|
386
|
+
name=f"{category} power [MW]",
|
|
387
|
+
showlegend=show_legend_category[category],
|
|
388
|
+
marker_color=category_colors[category],
|
|
389
|
+
),
|
|
390
|
+
secondary_y=False,
|
|
391
|
+
row=row + 1,
|
|
392
|
+
col=col + 1,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
show_legend_category[category] = False
|
|
396
|
+
|
|
397
|
+
if boolean_has_storage.sel(resource=resource, year_op=year_op):
|
|
398
|
+
fig.add_trace(
|
|
399
|
+
go.Scatter(
|
|
400
|
+
x=x_axis,
|
|
401
|
+
y=df_plot["energy"],
|
|
402
|
+
name="storage energy [MWh]",
|
|
403
|
+
mode="markers",
|
|
404
|
+
marker=dict(
|
|
405
|
+
size=14,
|
|
406
|
+
color="purple",
|
|
407
|
+
symbol="diamond",
|
|
408
|
+
),
|
|
409
|
+
showlegend=storage_legend,
|
|
410
|
+
),
|
|
411
|
+
secondary_y=True,
|
|
412
|
+
row=row + 1,
|
|
413
|
+
col=col + 1,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
storage_legend = False
|
|
417
|
+
|
|
418
|
+
fig.update_yaxes(
|
|
419
|
+
secondary_y=True,
|
|
420
|
+
row=row + 1,
|
|
421
|
+
col=col + 1,
|
|
422
|
+
color="purple",
|
|
423
|
+
showgrid=False,
|
|
424
|
+
ticks="outside",
|
|
425
|
+
tickwidth=2,
|
|
426
|
+
tickcolor="purple",
|
|
427
|
+
ticklen=8,
|
|
428
|
+
range=[0, df_plot["energy"].max() * 1.2],
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
fig.add_hline(
|
|
432
|
+
y=demand.sel(resource=resource, year_op=year_op).to_numpy(),
|
|
433
|
+
line_width=2,
|
|
434
|
+
line_dash="dash",
|
|
435
|
+
secondary_y=False,
|
|
436
|
+
showlegend=col == 0 and row == 0,
|
|
437
|
+
name="max annual exogenous demand",
|
|
438
|
+
row=row + 1,
|
|
439
|
+
col=col + 1,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
fig.update_yaxes(
|
|
443
|
+
title="Installed power capacities [MW]",
|
|
444
|
+
secondary_y=False,
|
|
445
|
+
col=1,
|
|
446
|
+
color="black",
|
|
447
|
+
showgrid=True,
|
|
448
|
+
ticks="outside",
|
|
449
|
+
tickwidth=2,
|
|
450
|
+
tickcolor="black",
|
|
451
|
+
ticklen=8,
|
|
452
|
+
# range=[0, 10],
|
|
453
|
+
)
|
|
454
|
+
fig.update_yaxes(
|
|
455
|
+
title="Installed energy capacities [MWh]",
|
|
456
|
+
secondary_y=True,
|
|
457
|
+
col=len(resources),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
fig.update_layout(
|
|
461
|
+
title="Installed capacities",
|
|
462
|
+
barmode="stack",
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
fig.for_each_annotation(lambda a: a.update(x=-0.07) if a.text in row_titles else ())
|
|
466
|
+
|
|
467
|
+
if not os.path.exists(plot_folder):
|
|
468
|
+
os.makedirs(plot_folder)
|
|
469
|
+
fig.write_html(f"{plot_folder}/{plot_name}.html")
|
|
470
|
+
return fig
|