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.
- docs/examples/00-Minimal Example.md +5 -0
- docs/examples/01-Basic Example.md +5 -0
- docs/examples/02-Complex Example.md +10 -0
- docs/examples/03-Calculation Modes.md +5 -0
- docs/examples/index.md +5 -0
- docs/faq/contribute.md +49 -0
- docs/faq/index.md +3 -0
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +1 -0
- docs/javascripts/mathjax.js +18 -0
- docs/release-notes/_template.txt +32 -0
- docs/release-notes/index.md +7 -0
- docs/release-notes/v2.0.0.md +93 -0
- docs/release-notes/v2.0.1.md +12 -0
- docs/user-guide/Mathematical Notation/Bus.md +33 -0
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
- docs/user-guide/Mathematical Notation/Flow.md +26 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
- docs/user-guide/Mathematical Notation/Storage.md +44 -0
- docs/user-guide/Mathematical Notation/index.md +22 -0
- docs/user-guide/Mathematical Notation/others.md +3 -0
- docs/user-guide/index.md +124 -0
- {flixOpt → flixopt}/__init__.py +5 -2
- {flixOpt → flixopt}/aggregation.py +113 -140
- flixopt/calculation.py +455 -0
- {flixOpt → flixopt}/commons.py +7 -4
- flixopt/components.py +630 -0
- {flixOpt → flixopt}/config.py +9 -8
- {flixOpt → flixopt}/config.yaml +3 -3
- flixopt/core.py +970 -0
- flixopt/effects.py +386 -0
- flixopt/elements.py +534 -0
- flixopt/features.py +1042 -0
- flixopt/flow_system.py +409 -0
- flixopt/interface.py +265 -0
- flixopt/io.py +308 -0
- flixopt/linear_converters.py +331 -0
- flixopt/plotting.py +1340 -0
- flixopt/results.py +898 -0
- flixopt/solvers.py +77 -0
- flixopt/structure.py +630 -0
- flixopt/utils.py +62 -0
- flixopt-2.0.1.dist-info/METADATA +145 -0
- flixopt-2.0.1.dist-info/RECORD +57 -0
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
- flixopt-2.0.1.dist-info/top_level.txt +6 -0
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixopt-icon.svg +1 -0
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +54 -0
- site/release-notes/_template.txt +32 -0
- flixOpt/calculation.py +0 -629
- flixOpt/components.py +0 -614
- flixOpt/core.py +0 -182
- flixOpt/effects.py +0 -410
- flixOpt/elements.py +0 -489
- flixOpt/features.py +0 -942
- flixOpt/flow_system.py +0 -351
- flixOpt/interface.py +0 -203
- flixOpt/linear_converters.py +0 -325
- flixOpt/math_modeling.py +0 -1145
- flixOpt/plotting.py +0 -712
- flixOpt/results.py +0 -563
- flixOpt/solvers.py +0 -21
- flixOpt/structure.py +0 -733
- flixOpt/utils.py +0 -134
- flixopt-1.0.12.dist-info/METADATA +0 -174
- flixopt-1.0.12.dist-info/RECORD +0 -29
- flixopt-1.0.12.dist-info/top_level.txt +0 -3
- {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
|
-
)
|