flixopt 1.0.12__py3-none-any.whl → 2.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.

Potentially problematic release.


This version of flixopt might be problematic. Click here for more details.

Files changed (73) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixOpt/plotting.py DELETED
@@ -1,712 +0,0 @@
1
- """
2
- This module contains the plotting functionality of the flixOpt framework.
3
- It provides high level functions to plot data with plotly and matplotlib.
4
- It's meant to be used in results.py, but is designed to be used by the end user as well.
5
- """
6
-
7
- import logging
8
- import pathlib
9
- from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
10
-
11
- import matplotlib.pyplot as plt
12
- import numpy as np
13
- import pandas as pd
14
- import plotly.express as px
15
- import plotly.graph_objects as go
16
- import plotly.offline
17
-
18
- if TYPE_CHECKING:
19
- import pyvis
20
-
21
- logger = logging.getLogger('flixOpt')
22
-
23
-
24
- def with_plotly(
25
- data: pd.DataFrame,
26
- mode: Literal['bar', 'line', 'area'] = 'area',
27
- colors: Union[List[str], str] = 'viridis',
28
- title: str = '',
29
- ylabel: str = '',
30
- fig: Optional[go.Figure] = None,
31
- show: bool = False,
32
- save: bool = False,
33
- path: Union[str, pathlib.Path] = 'temp-plot.html',
34
- ) -> go.Figure:
35
- """
36
- Plot a DataFrame with Plotly, using either stacked bars or stepped lines.
37
-
38
- Parameters
39
- ----------
40
- data : pd.DataFrame
41
- A DataFrame containing the data to plot, where the index represents
42
- time (e.g., hours), and each column represents a separate data series.
43
- mode : {'bar', 'line'}, default='bar'
44
- The plotting mode. Use 'bar' for stacked bar charts or 'line' for
45
- stepped lines.
46
- colors : List[str], str, default='viridis'
47
- A List of colors (as str) or a name of a colorscale (e.g., 'viridis', 'plasma') to use for
48
- coloring the data series.
49
- title: str
50
- The title of the plot.
51
- ylabel: str
52
- The label for the y-axis.
53
- fig : go.Figure, optional
54
- A Plotly figure object to plot on. If not provided, a new figure
55
- will be created.
56
- show: bool
57
- Wether to show the figure after creation. (This includes saving the figure)
58
- save: bool
59
- Wether to save the figure after creation (without showing)
60
- path: Union[str, pathlib.Path]
61
- Path to save the figure.
62
-
63
- Returns
64
- -------
65
- go.Figure
66
- A Plotly figure object containing the generated plot.
67
-
68
- Notes
69
- -----
70
- - If `mode` is 'bar', bars are stacked for each data series.
71
- - If `mode` is 'line', a stepped line is drawn for each data series.
72
- - The legend is positioned below the plot for a cleaner layout when many
73
- data series are present.
74
-
75
- Examples
76
- --------
77
- >>> fig = with_plotly(data, mode='bar', colorscale='plasma')
78
- >>> fig.show()
79
- """
80
- assert mode in ['bar', 'line', 'area'], f"'mode' must be one of {['bar', 'line', 'area']}"
81
- if data.empty:
82
- return go.Figure()
83
- if isinstance(colors, str):
84
- colorscale = px.colors.get_colorscale(colors)
85
- colors = px.colors.sample_colorscale(
86
- colorscale,
87
- [i / (len(data.columns) - 1) for i in range(len(data.columns))] if len(data.columns) > 1 else [0],
88
- )
89
-
90
- assert len(colors) == len(data.columns), (
91
- f'The number of colors does not match the provided data columns. {len(colors)=}; {len(colors)=}'
92
- )
93
- fig = fig if fig is not None else go.Figure()
94
-
95
- if mode == 'bar':
96
- for i, column in enumerate(data.columns):
97
- fig.add_trace(
98
- go.Bar(
99
- x=data.index,
100
- y=data[column],
101
- name=column,
102
- marker=dict(color=colors[i]),
103
- )
104
- )
105
-
106
- fig.update_layout(
107
- barmode='relative' if mode == 'bar' else None,
108
- bargap=0, # No space between bars
109
- bargroupgap=0, # No space between groups of bars
110
- )
111
- elif mode == 'line':
112
- for i, column in enumerate(data.columns):
113
- fig.add_trace(
114
- go.Scatter(
115
- x=data.index,
116
- y=data[column],
117
- mode='lines',
118
- name=column,
119
- line=dict(shape='hv', color=colors[i]),
120
- )
121
- )
122
- elif mode == 'area':
123
- data[(data > -1e-5) & (data < 1e-5)] = 0 # Preventing issues with plotting
124
- # Split columns into positive, negative, and mixed categories
125
- positive_columns = list(data.columns[(data >= 0).all()])
126
- negative_columns = list(data.columns[(data <= 0).all()])
127
- mixed_columns = list(set(data.columns) - set(positive_columns + negative_columns))
128
- if mixed_columns:
129
- logger.warning(
130
- f'Data for plotting stacked lines contains columns with both positive and negative values:'
131
- f' {mixed_columns}. These can not be stacked, and are printed as simple lines'
132
- )
133
-
134
- colors_stacked = {column: colors[i] for i, column in enumerate(data.columns)}
135
-
136
- for column in positive_columns + negative_columns:
137
- fig.add_trace(
138
- go.Scatter(
139
- x=data.index,
140
- y=data[column],
141
- mode='lines',
142
- name=column,
143
- line=dict(shape='hv', color=colors_stacked[column]),
144
- fill='tonexty',
145
- stackgroup='pos' if column in positive_columns else 'neg',
146
- )
147
- )
148
-
149
- for column in mixed_columns:
150
- fig.add_trace(
151
- go.Scatter(
152
- x=data.index,
153
- y=data[column],
154
- mode='lines',
155
- name=column,
156
- line=dict(shape='hv', color=colors_stacked[column], dash='dash'),
157
- )
158
- )
159
-
160
- # Update layout for better aesthetics
161
- fig.update_layout(
162
- title=title,
163
- yaxis=dict(
164
- title=ylabel,
165
- showgrid=True, # Enable grid lines on the y-axis
166
- gridcolor='lightgrey', # Customize grid line color
167
- gridwidth=0.5, # Customize grid line width
168
- ),
169
- xaxis=dict(
170
- title='Time in h',
171
- showgrid=True, # Enable grid lines on the x-axis
172
- gridcolor='lightgrey', # Customize grid line color
173
- gridwidth=0.5, # Customize grid line width
174
- ),
175
- plot_bgcolor='rgba(0,0,0,0)', # Transparent background
176
- paper_bgcolor='rgba(0,0,0,0)', # Transparent paper background
177
- font=dict(size=14), # Increase font size for better readability
178
- legend=dict(
179
- orientation='h', # Horizontal legend
180
- yanchor='bottom',
181
- y=-0.3, # Adjusts how far below the plot it appears
182
- xanchor='center',
183
- x=0.5,
184
- title_text=None, # Removes legend title for a cleaner look
185
- ),
186
- )
187
-
188
- if isinstance(path, pathlib.Path):
189
- path = path.as_posix()
190
- if show:
191
- plotly.offline.plot(fig, filename=path)
192
- elif save: # If show, the file is saved anyway
193
- fig.write_html(path)
194
- return fig
195
-
196
-
197
- def with_matplotlib(
198
- data: pd.DataFrame,
199
- mode: Literal['bar', 'line'] = 'bar',
200
- colors: Union[List[str], str] = 'viridis',
201
- figsize: Tuple[int, int] = (12, 6),
202
- fig: Optional[plt.Figure] = None,
203
- ax: Optional[plt.Axes] = None,
204
- show: bool = False,
205
- path: Optional[Union[str, pathlib.Path]] = None,
206
- ) -> Tuple[plt.Figure, plt.Axes]:
207
- """
208
- Plot a DataFrame with Matplotlib using stacked bars or stepped lines.
209
-
210
- Parameters
211
- ----------
212
- data : pd.DataFrame
213
- A DataFrame containing the data to plot. The index should represent
214
- time (e.g., hours), and each column represents a separate data series.
215
- mode : {'bar', 'line'}, default='bar'
216
- Plotting mode. Use 'bar' for stacked bar charts or 'line' for stepped lines.
217
- colors : List[str], str, default='viridis'
218
- A List of colors (as str) or a name of a colorscale (e.g., 'viridis', 'plasma') to use for
219
- coloring the data series.
220
- figsize: Tuple[int, int], optional
221
- Specify the size of the figure
222
- fig : plt.Figure, optional
223
- A Matplotlib figure object to plot on. If not provided, a new figure
224
- will be created.
225
- ax : plt.Axes, optional
226
- A Matplotlib axes object to plot on. If not provided, a new axes
227
- will be created.
228
- show: bool
229
- Wether to show the figure after creation.
230
- path: Union[str, pathlib.Path]
231
- Path to save the figure to.
232
-
233
- Returns
234
- -------
235
- Tuple[plt.Figure, plt.Axes]
236
- A tuple containing the Matplotlib figure and axes objects used for the plot.
237
-
238
- Notes
239
- -----
240
- - If `mode` is 'bar', bars are stacked for both positive and negative values.
241
- Negative values are stacked separately without extra labels in the legend.
242
- - If `mode` is 'line', stepped lines are drawn for each data series.
243
- - The legend is placed below the plot to accommodate multiple data series.
244
-
245
- Examples
246
- --------
247
- >>> fig, ax = with_matplotlib(data, mode='bar', colorscale='plasma')
248
- >>> plt.show()
249
- """
250
- assert mode in ['bar', 'line'], f"'mode' must be one of {['bar', 'line']} for matplotlib"
251
-
252
- if fig is None or ax is None:
253
- fig, ax = plt.subplots(figsize=figsize)
254
-
255
- if isinstance(colors, str):
256
- cmap = plt.get_cmap(colors, len(data.columns))
257
- colors = [cmap(i) for i in range(len(data.columns))]
258
- assert len(colors) == len(data.columns), (
259
- f'The number of colors does not match the provided data columns. {len(colors)=}; {len(colors)=}'
260
- )
261
-
262
- if mode == 'bar':
263
- cumulative_positive = np.zeros(len(data))
264
- cumulative_negative = np.zeros(len(data))
265
- width = data.index.to_series().diff().dropna().min() # Minimum time difference
266
-
267
- for i, column in enumerate(data.columns):
268
- positive_values = np.clip(data[column], 0, None) # Keep only positive values
269
- negative_values = np.clip(data[column], None, 0) # Keep only negative values
270
- # Plot positive bars
271
- ax.bar(
272
- data.index,
273
- positive_values,
274
- bottom=cumulative_positive,
275
- color=colors[i],
276
- label=column,
277
- width=width,
278
- align='center',
279
- )
280
- cumulative_positive += positive_values.values
281
- # Plot negative bars
282
- ax.bar(
283
- data.index,
284
- negative_values,
285
- bottom=cumulative_negative,
286
- color=colors[i],
287
- label='', # No label for negative bars
288
- width=width,
289
- align='center',
290
- )
291
- cumulative_negative += negative_values.values
292
-
293
- elif mode == 'line':
294
- for i, column in enumerate(data.columns):
295
- ax.step(data.index, data[column], where='post', color=colors[i], label=column)
296
-
297
- # Aesthetics
298
- ax.set_xlabel('Time in h', fontsize=14)
299
- ax.grid(color='lightgrey', linestyle='-', linewidth=0.5)
300
- ax.legend(
301
- loc='upper center', # Place legend at the bottom center
302
- bbox_to_anchor=(0.5, -0.15), # Adjust the position to fit below plot
303
- ncol=5,
304
- frameon=False, # Remove box around legend
305
- )
306
- fig.tight_layout()
307
-
308
- if show:
309
- plt.show()
310
- if path is not None:
311
- fig.savefig(path, dpi=300)
312
-
313
- return fig, ax
314
-
315
-
316
- def heat_map_matplotlib(
317
- data: pd.DataFrame,
318
- color_map: str = 'viridis',
319
- figsize: Tuple[float, float] = (12, 6),
320
- show: bool = False,
321
- path: Optional[Union[str, pathlib.Path]] = None,
322
- ) -> Tuple[plt.Figure, plt.Axes]:
323
- """
324
- Plots a DataFrame as a heatmap using Matplotlib. The columns of the DataFrame will be displayed on the x-axis,
325
- the index will be displayed on the y-axis, and the values will represent the 'heat' intensity in the plot.
326
-
327
- Parameters
328
- ----------
329
- data : pd.DataFrame
330
- A DataFrame containing the data to be visualized. The index will be used for the y-axis, and columns will be used for the x-axis.
331
- The values in the DataFrame will be represented as colors in the heatmap.
332
- color_map : str, optional
333
- The colormap to use for the heatmap. Default is 'viridis'. Matplotlib supports various colormaps like 'plasma', 'inferno', 'cividis', etc.
334
- figsize : tuple of float, optional
335
- The size of the figure to create. Default is (12, 6), which results in a width of 12 inches and a height of 6 inches.
336
- show: bool
337
- Wether to show the figure after creation.
338
- path: Union[str, pathlib.Path]
339
- Path to save the figure to.
340
-
341
- Returns
342
- -------
343
- tuple of (plt.Figure, plt.Axes)
344
- A tuple containing the Matplotlib `Figure` and `Axes` objects. The `Figure` contains the overall plot, while the `Axes` is the area
345
- where the heatmap is drawn. These can be used for further customization or saving the plot to a file.
346
-
347
- Notes
348
- -----
349
- - The y-axis is flipped so that the first row of the DataFrame is displayed at the top of the plot.
350
- - The color scale is normalized based on the minimum and maximum values in the DataFrame.
351
- - The x-axis labels (periods) are placed at the top of the plot.
352
- - The colorbar is added horizontally at the bottom of the plot, with a label.
353
- """
354
-
355
- # Get the min and max values for color normalization
356
- color_bar_min, color_bar_max = data.min().min(), data.max().max()
357
-
358
- # Create the heatmap plot
359
- fig, ax = plt.subplots(figsize=figsize)
360
- ax.pcolormesh(data.values, cmap=color_map)
361
- ax.invert_yaxis() # Flip the y-axis to start at the top
362
-
363
- # Adjust ticks and labels for x and y axes
364
- ax.set_xticks(np.arange(len(data.columns)) + 0.5)
365
- ax.set_xticklabels(data.columns, ha='center')
366
- ax.set_yticks(np.arange(len(data.index)) + 0.5)
367
- ax.set_yticklabels(data.index, va='center')
368
-
369
- # Add labels to the axes
370
- ax.set_xlabel('Period', ha='center')
371
- ax.set_ylabel('Step', va='center')
372
-
373
- # Position x-axis labels at the top
374
- ax.xaxis.set_label_position('top')
375
- ax.xaxis.set_ticks_position('top')
376
-
377
- # Add the colorbar
378
- sm1 = plt.cm.ScalarMappable(cmap=color_map, norm=plt.Normalize(vmin=color_bar_min, vmax=color_bar_max))
379
- sm1._A = []
380
- fig.colorbar(sm1, ax=ax, pad=0.12, aspect=15, fraction=0.2, orientation='horizontal')
381
-
382
- fig.tight_layout()
383
- if show:
384
- plt.show()
385
- if path is not None:
386
- fig.savefig(path, dpi=300)
387
-
388
- return fig, ax
389
-
390
-
391
- def heat_map_plotly(
392
- data: pd.DataFrame,
393
- color_map: str = 'viridis',
394
- title: str = '',
395
- xlabel: str = 'Periods',
396
- ylabel: str = 'Step',
397
- categorical_labels: bool = True,
398
- show: bool = False,
399
- save: bool = False,
400
- path: Union[str, pathlib.Path] = 'temp-plot.html',
401
- ) -> go.Figure:
402
- """
403
- Plots a DataFrame as a heatmap using Plotly. The columns of the DataFrame will be mapped to the x-axis,
404
- and the index will be displayed on the y-axis. The values in the DataFrame will represent the 'heat' in the plot.
405
-
406
- Parameters
407
- ----------
408
- data : pd.DataFrame
409
- A DataFrame with the data to be visualized. The index will be used for the y-axis, and columns will be used for the x-axis.
410
- The values in the DataFrame will be represented as colors in the heatmap.
411
- color_map : str, optional
412
- The color scale to use for the heatmap. Default is 'viridis'. Plotly supports various color scales like 'Cividis', 'Inferno', etc.
413
- categorical_labels : bool, optional
414
- If True, the x and y axes are treated as categorical data (i.e., the index and columns will not be interpreted as continuous data).
415
- Default is True. If False, the axes are treated as continuous, which may be useful for time series or numeric data.
416
- show: bool
417
- Wether to show the figure after creation. (This includes saving the figure)
418
- save: bool
419
- Wether to save the figure after creation (without showing)
420
- path: Union[str, pathlib.Path]
421
- Path to save the figure.
422
-
423
- Returns
424
- -------
425
- go.Figure
426
- A Plotly figure object containing the heatmap. This can be further customized and saved
427
- or displayed using `fig.show()`.
428
-
429
- Notes
430
- -----
431
- The color bar is automatically scaled to the minimum and maximum values in the data.
432
- The y-axis is reversed to display the first row at the top.
433
- """
434
-
435
- color_bar_min, color_bar_max = data.min().min(), data.max().max() # Min and max values for color scaling
436
- # Define the figure
437
- fig = go.Figure(
438
- data=go.Heatmap(
439
- z=data.values,
440
- x=data.columns,
441
- y=data.index,
442
- colorscale=color_map,
443
- zmin=color_bar_min,
444
- zmax=color_bar_max,
445
- colorbar=dict(
446
- title=dict(text='Color Bar Label', side='right'),
447
- orientation='h',
448
- xref='container',
449
- yref='container',
450
- len=0.8, # Color bar length relative to plot
451
- x=0.5,
452
- y=0.1,
453
- ),
454
- )
455
- )
456
-
457
- # Set axis labels and style
458
- fig.update_layout(
459
- title=title,
460
- xaxis=dict(title=xlabel, side='top', type='category' if categorical_labels else None),
461
- yaxis=dict(title=ylabel, autorange='reversed', type='category' if categorical_labels else None),
462
- )
463
-
464
- if isinstance(path, pathlib.Path):
465
- path = path.as_posix()
466
- if show:
467
- plotly.offline.plot(fig, filename=path)
468
- elif save: # If show, the file is saved anyway
469
- fig.write_html(path)
470
-
471
- return fig
472
-
473
-
474
- def reshape_to_2d(data_1d: np.ndarray, nr_of_steps_per_column: int) -> np.ndarray:
475
- """
476
- Reshapes a 1D numpy array into a 2D array suitable for plotting as a colormap.
477
-
478
- The reshaped array will have the number of rows corresponding to the steps per column
479
- (e.g., 24 hours per day) and columns representing time periods (e.g., days or months).
480
-
481
- Parameters
482
- ----------
483
- data_1d : np.ndarray
484
- A 1D numpy array with the data to reshape.
485
-
486
- nr_of_steps_per_column : int
487
- The number of steps (rows) per column in the resulting 2D array. For example,
488
- this could be 24 (for hours) or 31 (for days in a month).
489
-
490
- Returns
491
- -------
492
- np.ndarray
493
- The reshaped 2D array. Each internal array corresponds to one column, with the specified number of steps.
494
- Each column might represents a time period (e.g., day, month, etc.).
495
- """
496
-
497
- # Step 1: Ensure the input is a 1D array.
498
- if data_1d.ndim != 1:
499
- raise ValueError('Input must be a 1D array')
500
-
501
- # Step 2: Convert data to float type to allow NaN padding
502
- if data_1d.dtype != np.float64:
503
- data_1d = data_1d.astype(np.float64)
504
-
505
- # Step 3: Calculate the number of columns required
506
- total_steps = len(data_1d)
507
- cols = len(data_1d) // nr_of_steps_per_column # Base number of columns
508
-
509
- # If there's a remainder, add an extra column to hold the remaining values
510
- if total_steps % nr_of_steps_per_column != 0:
511
- cols += 1
512
-
513
- # Step 4: Pad the 1D data to match the required number of rows and columns
514
- padded_data = np.pad(
515
- data_1d, (0, cols * nr_of_steps_per_column - total_steps), mode='constant', constant_values=np.nan
516
- )
517
-
518
- # Step 5: Reshape the padded data into a 2D array
519
- data_2d = padded_data.reshape(cols, nr_of_steps_per_column)
520
-
521
- return data_2d.T
522
-
523
-
524
- def heat_map_data_from_df(
525
- df: pd.DataFrame,
526
- periods: Literal['YS', 'MS', 'W', 'D', 'h', '15min', 'min'],
527
- steps_per_period: Literal['W', 'D', 'h', '15min', 'min'],
528
- fill: Optional[Literal['ffill', 'bfill']] = None,
529
- ) -> pd.DataFrame:
530
- """
531
- Reshapes a DataFrame with a DateTime index into a 2D array for heatmap plotting,
532
- based on a specified sample rate.
533
- If a non-valid combination of periods and steps per period is used, falls back to numerical indices
534
-
535
- Parameters
536
- ----------
537
- df : pd.DataFrame
538
- A DataFrame with a DateTime index containing the data to reshape.
539
- periods : str
540
- The time interval of each period (columns of the heatmap),
541
- such as 'YS' (year start), 'W' (weekly), 'D' (daily), 'h' (hourly) etc.
542
- steps_per_period : str
543
- The time interval within each period (rows in the heatmap),
544
- such as 'YS' (year start), 'W' (weekly), 'D' (daily), 'h' (hourly) etc.
545
- fill : str, optional
546
- Method to fill missing values: 'ffill' for forward fill or 'bfill' for backward fill.
547
-
548
- Returns
549
- -------
550
- pd.DataFrame
551
- A DataFrame suitable for heatmap plotting, with rows representing steps within each period
552
- and columns representing each period.
553
- """
554
- assert pd.api.types.is_datetime64_any_dtype(df.index), (
555
- 'The index of the Dataframe must be datetime to transfrom it properly for a heatmap plot'
556
- )
557
-
558
- # Define formats for different combinations of `periods` and `steps_per_period`
559
- formats = {
560
- ('YS', 'W'): ('%Y', '%W'),
561
- ('YS', 'D'): ('%Y', '%j'), # day of year
562
- ('YS', 'h'): ('%Y', '%j %H:00'),
563
- ('MS', 'D'): ('%Y-%m', '%d'), # day of month
564
- ('MS', 'h'): ('%Y-%m', '%d %H:00'),
565
- ('W', 'D'): ('%Y-w%W', '%w_%A'), # week and day of week (with prefix for proper sorting)
566
- ('W', 'h'): ('%Y-w%W', '%w_%A %H:00'),
567
- ('D', 'h'): ('%Y-%m-%d', '%H:00'), # Day and hour
568
- ('D', '15min'): ('%Y-%m-%d', '%H:%MM'), # Day and hour
569
- ('h', '15min'): ('%Y-%m-%d %H:00', '%M'), # minute of hour
570
- ('h', 'min'): ('%Y-%m-%d %H:00', '%M'), # minute of hour
571
- }
572
-
573
- minimum_time_diff_in_min = df.index.to_series().diff().min().total_seconds() / 60 # Smallest time_diff in minutes
574
- time_intervals = {'min': 1, '15min': 15, 'h': 60, 'D': 24 * 60, 'W': 7 * 24 * 60}
575
- if time_intervals[steps_per_period] > minimum_time_diff_in_min:
576
- time_intervals[steps_per_period]
577
- logger.warning(
578
- f'To compute the heatmap, the data was aggregated from {minimum_time_diff_in_min:.2f} min to '
579
- f'{time_intervals[steps_per_period]:.2f} min. Mean values are displayed.'
580
- )
581
-
582
- # Select the format based on the `periods` and `steps_per_period` combination
583
- format_pair = (periods, steps_per_period)
584
- assert format_pair in formats, f'{format_pair} is not a valid format. Choose from {list(formats.keys())}'
585
- period_format, step_format = formats[format_pair]
586
-
587
- df = df.sort_index() # Ensure DataFrame is sorted by time index
588
-
589
- resampled_data = df.resample(steps_per_period).mean() # Resample and fill any gaps with NaN
590
-
591
- if fill == 'ffill': # Apply fill method if specified
592
- resampled_data = resampled_data.ffill()
593
- elif fill == 'bfill':
594
- resampled_data = resampled_data.bfill()
595
-
596
- resampled_data['period'] = resampled_data.index.strftime(period_format)
597
- resampled_data['step'] = resampled_data.index.strftime(step_format)
598
- if '%w_%A' in step_format: # SHift index of strings to ensure proper sorting
599
- resampled_data['step'] = resampled_data['step'].apply(
600
- lambda x: x.replace('0_Sunday', '7_Sunday') if '0_Sunday' in x else x
601
- )
602
-
603
- # Pivot the table so periods are columns and steps are indices
604
- df_pivoted = resampled_data.pivot(columns='period', index='step', values=df.columns[0])
605
-
606
- return df_pivoted
607
-
608
-
609
- def visualize_network(
610
- node_infos: dict,
611
- edge_infos: dict,
612
- path: Optional[Union[str, pathlib.Path]] = None,
613
- controls: Union[
614
- bool,
615
- List[Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']],
616
- ] = True,
617
- show: bool = True,
618
- ) -> Optional['pyvis.network.Network']:
619
- """
620
- Visualizes the network structure of a FlowSystem using PyVis, using info-dictionaries.
621
-
622
- Parameters:
623
- - path (Union[bool, str, pathlib.Path], default='results/network.html'):
624
- Path to save the HTML visualization.
625
- - `False`: Visualization is created but not saved.
626
- - `str` or `Path`: Specifies file path (default: 'results/network.html').
627
-
628
- - controls (Union[bool, List[str]], default=True):
629
- UI controls to add to the visualization.
630
- - `True`: Enables all available controls.
631
- - `List`: Specify controls, e.g., ['nodes', 'layout'].
632
- - Options: 'nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer'.
633
- You can play with these and generate a Dictionary from it that can be applied to the network returned by this function.
634
- network.set_options()
635
- https://pyvis.readthedocs.io/en/latest/tutorial.html
636
-
637
- - show (bool, default=True):
638
- Whether to open the visualization in the web browser.
639
- The calculation must be saved to show it. If no path is given, it defaults to 'network.html'.
640
-
641
- Returns:
642
- - Optional[pyvis.network.Network]: The `Network` instance representing the visualization, or `None` if `pyvis` is not installed.
643
-
644
- Usage:
645
- - Visualize and open the network with default options:
646
- >>> self.visualize_network()
647
-
648
- - Save the visualization without opening:
649
- >>> self.visualize_network(show=False)
650
-
651
- - Visualize with custom controls and path:
652
- >>> self.visualize_network(path='output/custom_network.html', controls=['nodes', 'layout'])
653
-
654
- Notes:
655
- - This function requires `pyvis`. If not installed, the function prints a warning and returns `None`.
656
- - Nodes are styled based on type (e.g., circles for buses, boxes for components) and annotated with node information.
657
- """
658
- try:
659
- from pyvis.network import Network
660
- except ImportError:
661
- logger.warning("Please install pyvis to visualize the network: 'pip install pyvis'")
662
- return None
663
-
664
- net = Network(directed=True, height='100%' if controls is False else '800px', font_color='white')
665
-
666
- for node_id, node in node_infos.items():
667
- net.add_node(
668
- node_id,
669
- label=node['label'],
670
- shape={'Bus': 'circle', 'Component': 'box'}[node['class']],
671
- color={'Bus': '#393E46', 'Component': '#00ADB5'}[node['class']],
672
- title=node['infos'].replace(')', '\n)'),
673
- font={'size': 14},
674
- )
675
-
676
- for edge in edge_infos.values():
677
- net.add_edge(
678
- edge['start'],
679
- edge['end'],
680
- label=edge['label'],
681
- title=edge['infos'].replace(')', '\n)'),
682
- font={'color': '#4D4D4D', 'size': 14},
683
- color='#222831',
684
- )
685
-
686
- # Enhanced physics settings
687
- net.barnes_hut(central_gravity=0.8, spring_length=50, spring_strength=0.05, gravity=-10000)
688
-
689
- if controls:
690
- net.show_buttons(filter_=controls) # Adds UI buttons to control physics settings
691
- if not show and not path:
692
- return net
693
- elif path:
694
- path = pathlib.Path(path) if isinstance(path, str) else path
695
- net.write_html(path.as_posix())
696
- elif show:
697
- path = pathlib.Path('network.html')
698
- net.write_html(path.as_posix())
699
-
700
- if show:
701
- try:
702
- import webbrowser
703
-
704
- worked = webbrowser.open(f'file://{path.resolve()}', 2)
705
- if not worked:
706
- logger.warning(
707
- f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}'
708
- )
709
- except Exception as e:
710
- logger.warning(
711
- f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}: {e}'
712
- )