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.
Files changed (64) hide show
  1. pommes/__init__.py +0 -0
  2. pommes/common_plots.py +470 -0
  3. pommes/inputs.yaml +650 -0
  4. pommes/io/__init__.py +0 -0
  5. pommes/io/analyse_input_dataset.py +51 -0
  6. pommes/io/build_input_dataset.py +402 -0
  7. pommes/io/read_solution.py +176 -0
  8. pommes/io/save_solution.py +99 -0
  9. pommes/model/__init__.py +0 -0
  10. pommes/model/build_model.py +166 -0
  11. pommes/model/carbon.py +110 -0
  12. pommes/model/combined.py +216 -0
  13. pommes/model/conversion.py +248 -0
  14. pommes/model/net_import.py +129 -0
  15. pommes/model/repurposing.py +74 -0
  16. pommes/model/storage.py +309 -0
  17. pommes/model/transport.py +214 -0
  18. pommes/model/turpe.py +112 -0
  19. pommes/post_process.py +213 -0
  20. pommes/solver/__init__.py +0 -0
  21. pommes/solver/utils_xpress.py +156 -0
  22. pommes/tests/conftest.py +448 -0
  23. pommes/tests/study/test_case/config.yaml +606 -0
  24. pommes/tests/study/test_case/data/availability.csv +11 -0
  25. pommes/tests/study/test_case/data/carbon.csv +5 -0
  26. pommes/tests/study/test_case/data/combined.csv +2 -0
  27. pommes/tests/study/test_case/data/combined_factor.csv +5 -0
  28. pommes/tests/study/test_case/data/combined_operation.csv +3 -0
  29. pommes/tests/study/test_case/data/conversion.csv +7 -0
  30. pommes/tests/study/test_case/data/conversion_factor.csv +11 -0
  31. pommes/tests/study/test_case/data/conversion_operation.csv +5 -0
  32. pommes/tests/study/test_case/data/demand.csv +13 -0
  33. pommes/tests/study/test_case/data/discount.csv +5 -0
  34. pommes/tests/study/test_case/data/eco.csv +2 -0
  35. pommes/tests/study/test_case/data/import_hourly.csv +4 -0
  36. pommes/tests/study/test_case/data/import_yearly.csv +5 -0
  37. pommes/tests/study/test_case/data/load_shedding.csv +5 -0
  38. pommes/tests/study/test_case/data/spillage.csv +5 -0
  39. pommes/tests/study/test_case/data/storage.csv +4 -0
  40. pommes/tests/study/test_case/data/storage_factor.csv +10 -0
  41. pommes/tests/study/test_case/data/transport.csv +7 -0
  42. pommes/tests/study/test_case/data/turpe.csv +21 -0
  43. pommes/tests/study/test_case/data/turpe_calendar.csv +11 -0
  44. pommes/tests/study/test_case/data_source.yaml +41 -0
  45. pommes/tests/study/test_case/pre_process/pre_process.py +15 -0
  46. pommes/tests/study/test_case/pre_process/revert_pre_process.py +7 -0
  47. pommes/tests/study/test_case/pre_process.yaml +16 -0
  48. pommes/tests/study/test_case/run_study.py +59 -0
  49. pommes/tests/test_build_input_dataset.py +38 -0
  50. pommes/tests/test_carbon_net_import.py +90 -0
  51. pommes/tests/test_combined.py +217 -0
  52. pommes/tests/test_conversion.py +358 -0
  53. pommes/tests/test_net_import.py +51 -0
  54. pommes/tests/test_storage.py +243 -0
  55. pommes/tests/test_test_case.py +7 -0
  56. pommes/tests/test_transport.py +252 -0
  57. pommes/tools/draft_multi_index.py +32 -0
  58. pommes/utils.py +313 -0
  59. pommes-0.0.1.dist-info/AUTHORS +6 -0
  60. pommes-0.0.1.dist-info/LICENCE.txt +21 -0
  61. pommes-0.0.1.dist-info/METADATA +214 -0
  62. pommes-0.0.1.dist-info/RECORD +64 -0
  63. pommes-0.0.1.dist-info/WHEEL +5 -0
  64. 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