flixopt 3.0.1__py3-none-any.whl → 6.0.0rc7__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.
- flixopt/__init__.py +57 -49
- flixopt/carrier.py +159 -0
- flixopt/clustering/__init__.py +51 -0
- flixopt/clustering/base.py +1746 -0
- flixopt/clustering/intercluster_helpers.py +201 -0
- flixopt/color_processing.py +372 -0
- flixopt/comparison.py +819 -0
- flixopt/components.py +848 -270
- flixopt/config.py +853 -496
- flixopt/core.py +111 -98
- flixopt/effects.py +294 -284
- flixopt/elements.py +484 -223
- flixopt/features.py +220 -118
- flixopt/flow_system.py +2026 -389
- flixopt/interface.py +504 -286
- flixopt/io.py +1718 -55
- flixopt/linear_converters.py +291 -230
- flixopt/modeling.py +304 -181
- flixopt/network_app.py +2 -1
- flixopt/optimization.py +788 -0
- flixopt/optimize_accessor.py +373 -0
- flixopt/plot_result.py +143 -0
- flixopt/plotting.py +1177 -1034
- flixopt/results.py +1331 -372
- flixopt/solvers.py +12 -4
- flixopt/statistics_accessor.py +2412 -0
- flixopt/stats_accessor.py +75 -0
- flixopt/structure.py +954 -120
- flixopt/topology_accessor.py +676 -0
- flixopt/transform_accessor.py +2277 -0
- flixopt/types.py +120 -0
- flixopt-6.0.0rc7.dist-info/METADATA +290 -0
- flixopt-6.0.0rc7.dist-info/RECORD +36 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
- flixopt/aggregation.py +0 -382
- flixopt/calculation.py +0 -672
- flixopt/commons.py +0 -51
- flixopt/utils.py +0 -86
- flixopt-3.0.1.dist-info/METADATA +0 -209
- flixopt-3.0.1.dist-info/RECORD +0 -26
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Topology accessor for FlowSystem.
|
|
3
|
+
|
|
4
|
+
This module provides the TopologyAccessor class that enables the
|
|
5
|
+
`flow_system.topology` pattern for network structure inspection and visualization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import pathlib
|
|
12
|
+
import warnings
|
|
13
|
+
from itertools import chain
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
15
|
+
|
|
16
|
+
import plotly.graph_objects as go
|
|
17
|
+
import xarray as xr
|
|
18
|
+
|
|
19
|
+
from .color_processing import ColorType, hex_to_rgba, process_colors
|
|
20
|
+
from .config import CONFIG, DEPRECATION_REMOVAL_VERSION
|
|
21
|
+
from .plot_result import PlotResult
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
import pyvis
|
|
25
|
+
|
|
26
|
+
from .flow_system import FlowSystem
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger('flixopt')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _plot_network(
|
|
32
|
+
node_infos: dict,
|
|
33
|
+
edge_infos: dict,
|
|
34
|
+
path: str | pathlib.Path | None = None,
|
|
35
|
+
controls: bool
|
|
36
|
+
| list[
|
|
37
|
+
Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
|
|
38
|
+
] = True,
|
|
39
|
+
show: bool = False,
|
|
40
|
+
) -> pyvis.network.Network | None:
|
|
41
|
+
"""Visualize network structure using PyVis.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
node_infos: Dictionary of node information.
|
|
45
|
+
edge_infos: Dictionary of edge information.
|
|
46
|
+
path: Path to save HTML visualization.
|
|
47
|
+
controls: UI controls to add. True for all, or list of specific controls.
|
|
48
|
+
show: Whether to open in browser.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Network instance, or None if pyvis not installed.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
from pyvis.network import Network
|
|
55
|
+
except ImportError:
|
|
56
|
+
logger.critical("Plotting the flow system network was not possible. Please install pyvis: 'pip install pyvis'")
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
net = Network(directed=True, height='100%' if controls is False else '800px', font_color='white')
|
|
60
|
+
|
|
61
|
+
for node_id, node in node_infos.items():
|
|
62
|
+
net.add_node(
|
|
63
|
+
node_id,
|
|
64
|
+
label=node['label'],
|
|
65
|
+
shape={'Bus': 'circle', 'Component': 'box'}[node['class']],
|
|
66
|
+
color={'Bus': '#393E46', 'Component': '#00ADB5'}[node['class']],
|
|
67
|
+
title=node['infos'].replace(')', '\n)'),
|
|
68
|
+
font={'size': 14},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for edge in edge_infos.values():
|
|
72
|
+
# Use carrier color if available, otherwise default gray
|
|
73
|
+
edge_color = edge.get('carrier_color', '#222831') or '#222831'
|
|
74
|
+
net.add_edge(
|
|
75
|
+
edge['start'],
|
|
76
|
+
edge['end'],
|
|
77
|
+
label=edge['label'],
|
|
78
|
+
title=edge['infos'].replace(')', '\n)'),
|
|
79
|
+
font={'color': '#4D4D4D', 'size': 14},
|
|
80
|
+
color=edge_color,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
net.barnes_hut(central_gravity=0.8, spring_length=50, spring_strength=0.05, gravity=-10000)
|
|
84
|
+
|
|
85
|
+
if controls:
|
|
86
|
+
net.show_buttons(filter_=controls)
|
|
87
|
+
if not show and not path:
|
|
88
|
+
return net
|
|
89
|
+
elif path:
|
|
90
|
+
path = pathlib.Path(path) if isinstance(path, str) else path
|
|
91
|
+
net.write_html(path.as_posix())
|
|
92
|
+
elif show:
|
|
93
|
+
path = pathlib.Path('network.html')
|
|
94
|
+
net.write_html(path.as_posix())
|
|
95
|
+
|
|
96
|
+
if show:
|
|
97
|
+
try:
|
|
98
|
+
import webbrowser
|
|
99
|
+
|
|
100
|
+
worked = webbrowser.open(f'file://{path.resolve()}', 2)
|
|
101
|
+
if not worked:
|
|
102
|
+
logger.error(f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}')
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(
|
|
105
|
+
f'Showing the network in the Browser went wrong. Open it manually. Its saved under {path}: {e}'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return net
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TopologyAccessor:
|
|
112
|
+
"""
|
|
113
|
+
Accessor for network topology inspection and visualization on FlowSystem.
|
|
114
|
+
|
|
115
|
+
This class provides the topology API for FlowSystem, accessible via
|
|
116
|
+
`flow_system.topology`. It offers methods to inspect the network structure
|
|
117
|
+
and visualize it.
|
|
118
|
+
|
|
119
|
+
Examples:
|
|
120
|
+
Visualize the network:
|
|
121
|
+
|
|
122
|
+
>>> flow_system.topology.plot()
|
|
123
|
+
>>> flow_system.topology.plot(path='my_network.html', show=True)
|
|
124
|
+
|
|
125
|
+
Interactive visualization:
|
|
126
|
+
|
|
127
|
+
>>> flow_system.topology.start_app()
|
|
128
|
+
>>> # ... interact with the visualization ...
|
|
129
|
+
>>> flow_system.topology.stop_app()
|
|
130
|
+
|
|
131
|
+
Get network structure info:
|
|
132
|
+
|
|
133
|
+
>>> nodes, edges = flow_system.topology.infos()
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, flow_system: FlowSystem) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Initialize the accessor with a reference to the FlowSystem.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
flow_system: The FlowSystem to inspect.
|
|
142
|
+
"""
|
|
143
|
+
self._fs = flow_system
|
|
144
|
+
|
|
145
|
+
# Cached color mappings (lazily initialized)
|
|
146
|
+
self._carrier_colors: dict[str, str] | None = None
|
|
147
|
+
self._component_colors: dict[str, str] | None = None
|
|
148
|
+
self._bus_colors: dict[str, str] | None = None
|
|
149
|
+
|
|
150
|
+
# Cached unit mappings (lazily initialized)
|
|
151
|
+
self._carrier_units: dict[str, str] | None = None
|
|
152
|
+
self._effect_units: dict[str, str] | None = None
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def carrier_colors(self) -> dict[str, str]:
|
|
156
|
+
"""Cached mapping of carrier name to hex color.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dict mapping carrier names (lowercase) to hex color strings.
|
|
160
|
+
Only carriers with a color defined are included.
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
>>> fs.topology.carrier_colors
|
|
164
|
+
{'electricity': '#FECB52', 'heat': '#D62728', 'gas': '#1F77B4'}
|
|
165
|
+
"""
|
|
166
|
+
if self._carrier_colors is None:
|
|
167
|
+
self._carrier_colors = {name: carrier.color for name, carrier in self._fs.carriers.items() if carrier.color}
|
|
168
|
+
return self._carrier_colors
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def component_colors(self) -> dict[str, str]:
|
|
172
|
+
"""Cached mapping of component label to hex color.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Dict mapping component labels to hex color strings.
|
|
176
|
+
Only components with a color defined are included.
|
|
177
|
+
|
|
178
|
+
Examples:
|
|
179
|
+
>>> fs.topology.component_colors
|
|
180
|
+
{'Boiler': '#1f77b4', 'CHP': '#ff7f0e', 'HeatPump': '#2ca02c'}
|
|
181
|
+
"""
|
|
182
|
+
if self._component_colors is None:
|
|
183
|
+
self._component_colors = {label: comp.color for label, comp in self._fs.components.items() if comp.color}
|
|
184
|
+
return self._component_colors
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def bus_colors(self) -> dict[str, str]:
|
|
188
|
+
"""Cached mapping of bus label to hex color (from carrier).
|
|
189
|
+
|
|
190
|
+
Bus colors are derived from their associated carrier's color.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dict mapping bus labels to hex color strings.
|
|
194
|
+
Only buses with a carrier that has a color defined are included.
|
|
195
|
+
|
|
196
|
+
Examples:
|
|
197
|
+
>>> fs.topology.bus_colors
|
|
198
|
+
{'ElectricityBus': '#FECB52', 'HeatBus': '#D62728'}
|
|
199
|
+
"""
|
|
200
|
+
if self._bus_colors is None:
|
|
201
|
+
carrier_colors = self.carrier_colors
|
|
202
|
+
self._bus_colors = {}
|
|
203
|
+
for label, bus in self._fs.buses.items():
|
|
204
|
+
if bus.carrier:
|
|
205
|
+
color = carrier_colors.get(bus.carrier.lower())
|
|
206
|
+
if color:
|
|
207
|
+
self._bus_colors[label] = color
|
|
208
|
+
return self._bus_colors
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def carrier_units(self) -> dict[str, str]:
|
|
212
|
+
"""Cached mapping of carrier name to unit string.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dict mapping carrier names (lowercase) to unit strings.
|
|
216
|
+
Carriers without a unit defined return an empty string.
|
|
217
|
+
|
|
218
|
+
Examples:
|
|
219
|
+
>>> fs.topology.carrier_units
|
|
220
|
+
{'electricity': 'kW', 'heat': 'kW', 'gas': 'kW'}
|
|
221
|
+
"""
|
|
222
|
+
if self._carrier_units is None:
|
|
223
|
+
self._carrier_units = {name: carrier.unit or '' for name, carrier in self._fs.carriers.items()}
|
|
224
|
+
return self._carrier_units
|
|
225
|
+
|
|
226
|
+
@property
|
|
227
|
+
def effect_units(self) -> dict[str, str]:
|
|
228
|
+
"""Cached mapping of effect label to unit string.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Dict mapping effect labels to unit strings.
|
|
232
|
+
Effects without a unit defined return an empty string.
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
>>> fs.topology.effect_units
|
|
236
|
+
{'costs': '€', 'CO2': 'kg'}
|
|
237
|
+
"""
|
|
238
|
+
if self._effect_units is None:
|
|
239
|
+
self._effect_units = {effect.label: effect.unit or '' for effect in self._fs.effects.values()}
|
|
240
|
+
return self._effect_units
|
|
241
|
+
|
|
242
|
+
def _invalidate_color_caches(self) -> None:
|
|
243
|
+
"""Reset all color caches so they are rebuilt on next access."""
|
|
244
|
+
self._carrier_colors = None
|
|
245
|
+
self._component_colors = None
|
|
246
|
+
self._bus_colors = None
|
|
247
|
+
|
|
248
|
+
def set_component_color(self, label: str, color: str) -> None:
|
|
249
|
+
"""Set the color for a single component.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
label: Component label.
|
|
253
|
+
color: Color string (hex like '#FF0000', named like 'red', etc.).
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
KeyError: If component with given label doesn't exist.
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
>>> flow_system.topology.set_component_color('Boiler', '#D35400')
|
|
260
|
+
>>> flow_system.topology.set_component_color('CHP', 'darkred')
|
|
261
|
+
"""
|
|
262
|
+
if label not in self._fs.components:
|
|
263
|
+
raise KeyError(f"Component '{label}' not found. Available: {list(self._fs.components.keys())}")
|
|
264
|
+
self._fs.components[label].color = color
|
|
265
|
+
self._invalidate_color_caches()
|
|
266
|
+
|
|
267
|
+
def set_component_colors(
|
|
268
|
+
self,
|
|
269
|
+
colors: dict[str, str | list[str]] | str,
|
|
270
|
+
overwrite: bool = True,
|
|
271
|
+
) -> dict[str, str]:
|
|
272
|
+
"""Set colors for multiple components at once.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
colors: Color configuration:
|
|
276
|
+
- ``str``: Colorscale name for all components (e.g., ``'turbo'``)
|
|
277
|
+
- ``dict``: Component-to-color mapping (``{'Boiler': 'red'}``) or
|
|
278
|
+
colorscale-to-components (``{'Blues': ['Wind1', 'Wind2']}``)
|
|
279
|
+
overwrite: If False, skip components that already have colors.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Mapping of colors that were actually assigned.
|
|
283
|
+
|
|
284
|
+
Examples:
|
|
285
|
+
>>> flow_system.topology.set_component_colors('turbo')
|
|
286
|
+
>>> flow_system.topology.set_component_colors({'Boiler': 'red', 'CHP': '#0000FF'})
|
|
287
|
+
>>> flow_system.topology.set_component_colors({'Blues': ['Wind1', 'Wind2']})
|
|
288
|
+
>>> flow_system.topology.set_component_colors('turbo', overwrite=False)
|
|
289
|
+
"""
|
|
290
|
+
components = self._fs.components
|
|
291
|
+
|
|
292
|
+
# Normalize to {label: color} mapping
|
|
293
|
+
if isinstance(colors, str):
|
|
294
|
+
color_map = process_colors(colors, list(components.keys()))
|
|
295
|
+
else:
|
|
296
|
+
color_map = {}
|
|
297
|
+
for key, value in colors.items():
|
|
298
|
+
if isinstance(value, list):
|
|
299
|
+
# Colorscale -> component list
|
|
300
|
+
missing = [c for c in value if c not in components]
|
|
301
|
+
if missing:
|
|
302
|
+
raise KeyError(f'Components not found: {missing}')
|
|
303
|
+
color_map.update(process_colors(key, value))
|
|
304
|
+
else:
|
|
305
|
+
# Direct assignment
|
|
306
|
+
if key not in components:
|
|
307
|
+
raise KeyError(f"Component '{key}' not found")
|
|
308
|
+
color_map[key] = value
|
|
309
|
+
|
|
310
|
+
# Apply colors (respecting overwrite flag)
|
|
311
|
+
result = {}
|
|
312
|
+
for label, color in color_map.items():
|
|
313
|
+
if overwrite or components[label].color is None:
|
|
314
|
+
components[label].color = color
|
|
315
|
+
result[label] = color
|
|
316
|
+
|
|
317
|
+
self._invalidate_color_caches()
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
def set_carrier_color(self, carrier: str, color: str) -> None:
|
|
321
|
+
"""Set the color for a carrier.
|
|
322
|
+
|
|
323
|
+
This affects bus colors derived from this carrier.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
carrier: Carrier name (case-insensitive).
|
|
327
|
+
color: Color string (hex like '#FF0000', named like 'red', etc.).
|
|
328
|
+
|
|
329
|
+
Examples:
|
|
330
|
+
>>> flow_system.topology.set_carrier_color('electricity', '#FECB52')
|
|
331
|
+
>>> flow_system.topology.set_carrier_color('heat', 'firebrick')
|
|
332
|
+
"""
|
|
333
|
+
carrier_obj = self._fs.get_carrier(carrier)
|
|
334
|
+
if carrier_obj is None:
|
|
335
|
+
raise KeyError(f"Carrier '{carrier}' not found.")
|
|
336
|
+
carrier_obj.color = color
|
|
337
|
+
self._invalidate_color_caches()
|
|
338
|
+
|
|
339
|
+
def infos(self) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, str]]]:
|
|
340
|
+
"""
|
|
341
|
+
Get network topology information as dictionaries.
|
|
342
|
+
|
|
343
|
+
Returns node and edge information suitable for visualization or analysis.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Tuple of (nodes_dict, edges_dict) where:
|
|
347
|
+
- nodes_dict maps node labels to their properties (label, class, infos)
|
|
348
|
+
- edges_dict maps edge labels to their properties (label, start, end, infos)
|
|
349
|
+
|
|
350
|
+
Examples:
|
|
351
|
+
>>> nodes, edges = flow_system.topology.infos()
|
|
352
|
+
>>> print(nodes.keys()) # All component and bus labels
|
|
353
|
+
>>> print(edges.keys()) # All flow labels
|
|
354
|
+
"""
|
|
355
|
+
from .elements import Bus
|
|
356
|
+
|
|
357
|
+
if not self._fs.connected_and_transformed:
|
|
358
|
+
self._fs.connect_and_transform()
|
|
359
|
+
|
|
360
|
+
nodes = {
|
|
361
|
+
node.label_full: {
|
|
362
|
+
'label': node.label,
|
|
363
|
+
'class': 'Bus' if isinstance(node, Bus) else 'Component',
|
|
364
|
+
'infos': node.__str__(),
|
|
365
|
+
}
|
|
366
|
+
for node in chain(self._fs.components.values(), self._fs.buses.values())
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
# Use cached colors for efficient lookup
|
|
370
|
+
flow_carriers = self._fs.flow_carriers
|
|
371
|
+
carrier_colors = self.carrier_colors
|
|
372
|
+
|
|
373
|
+
edges = {}
|
|
374
|
+
for flow in self._fs.flows.values():
|
|
375
|
+
carrier_name = flow_carriers.get(flow.label_full)
|
|
376
|
+
edges[flow.label_full] = {
|
|
377
|
+
'label': flow.label,
|
|
378
|
+
'start': flow.bus if flow.is_input_in_component else flow.component,
|
|
379
|
+
'end': flow.component if flow.is_input_in_component else flow.bus,
|
|
380
|
+
'infos': flow.__str__(),
|
|
381
|
+
'carrier_color': carrier_colors.get(carrier_name) if carrier_name else None,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return nodes, edges
|
|
385
|
+
|
|
386
|
+
def plot(
|
|
387
|
+
self,
|
|
388
|
+
colors: ColorType | None = None,
|
|
389
|
+
show: bool | None = None,
|
|
390
|
+
**plotly_kwargs: Any,
|
|
391
|
+
) -> PlotResult:
|
|
392
|
+
"""
|
|
393
|
+
Visualize the network structure as a Sankey diagram using Plotly.
|
|
394
|
+
|
|
395
|
+
Creates a Sankey diagram showing the topology of the flow system,
|
|
396
|
+
with buses and components as nodes, and flows as links between them.
|
|
397
|
+
All links have equal width since no solution data is used.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
colors: Color specification for nodes (buses).
|
|
401
|
+
- `None`: Uses default color palette based on buses.
|
|
402
|
+
- `str`: Plotly colorscale name (e.g., 'Viridis', 'Blues').
|
|
403
|
+
- `list`: List of colors to cycle through.
|
|
404
|
+
- `dict`: Maps bus labels to specific colors.
|
|
405
|
+
Links inherit colors from their connected bus.
|
|
406
|
+
show: Whether to display the figure in the browser.
|
|
407
|
+
- `None`: Uses default from CONFIG.Plotting.default_show.
|
|
408
|
+
**plotly_kwargs: Additional arguments passed to Plotly layout.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
PlotResult containing the Sankey diagram figure and topology data
|
|
412
|
+
(source, target, value for each link).
|
|
413
|
+
|
|
414
|
+
Examples:
|
|
415
|
+
>>> flow_system.topology.plot()
|
|
416
|
+
>>> flow_system.topology.plot(show=True)
|
|
417
|
+
>>> flow_system.topology.plot(colors='Viridis')
|
|
418
|
+
>>> flow_system.topology.plot(colors={'ElectricityBus': 'gold', 'HeatBus': 'red'})
|
|
419
|
+
|
|
420
|
+
Notes:
|
|
421
|
+
This visualization shows the network structure without optimization results.
|
|
422
|
+
For visualizations that include flow values, use `flow_system.statistics.plot.sankey.flows()`
|
|
423
|
+
after running an optimization.
|
|
424
|
+
|
|
425
|
+
Hover over nodes and links to see detailed element information.
|
|
426
|
+
|
|
427
|
+
See Also:
|
|
428
|
+
- `plot_legacy()`: Previous PyVis-based network visualization.
|
|
429
|
+
- `statistics.plot.sankey.flows()`: Sankey with actual flow values from optimization.
|
|
430
|
+
"""
|
|
431
|
+
if not self._fs.connected_and_transformed:
|
|
432
|
+
self._fs.connect_and_transform()
|
|
433
|
+
|
|
434
|
+
# Build nodes and links from topology
|
|
435
|
+
nodes: set[str] = set()
|
|
436
|
+
links: dict[str, list] = {
|
|
437
|
+
'source': [],
|
|
438
|
+
'target': [],
|
|
439
|
+
'value': [],
|
|
440
|
+
'label': [],
|
|
441
|
+
'customdata': [], # For hover text
|
|
442
|
+
'color': [], # Carrier-based colors
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
# Collect node hover info (format repr for HTML display)
|
|
446
|
+
node_hover: dict[str, str] = {}
|
|
447
|
+
for comp in self._fs.components.values():
|
|
448
|
+
node_hover[comp.label] = repr(comp).replace('\n', '<br>')
|
|
449
|
+
for bus in self._fs.buses.values():
|
|
450
|
+
node_hover[bus.label] = repr(bus).replace('\n', '<br>')
|
|
451
|
+
|
|
452
|
+
# Use cached colors for efficient lookup
|
|
453
|
+
flow_carriers = self._fs.flow_carriers
|
|
454
|
+
carrier_colors = self.carrier_colors
|
|
455
|
+
|
|
456
|
+
for flow in self._fs.flows.values():
|
|
457
|
+
bus_label = flow.bus
|
|
458
|
+
comp_label = flow.component
|
|
459
|
+
|
|
460
|
+
if flow.is_input_in_component:
|
|
461
|
+
source = bus_label
|
|
462
|
+
target = comp_label
|
|
463
|
+
else:
|
|
464
|
+
source = comp_label
|
|
465
|
+
target = bus_label
|
|
466
|
+
|
|
467
|
+
nodes.add(source)
|
|
468
|
+
nodes.add(target)
|
|
469
|
+
links['source'].append(source)
|
|
470
|
+
links['target'].append(target)
|
|
471
|
+
links['value'].append(1) # Equal width for all links (no solution data)
|
|
472
|
+
links['label'].append(flow.label_full)
|
|
473
|
+
links['customdata'].append(repr(flow).replace('\n', '<br>')) # Flow repr for hover
|
|
474
|
+
|
|
475
|
+
# Get carrier color for this flow (subtle/semi-transparent) using cached colors
|
|
476
|
+
carrier_name = flow_carriers.get(flow.label_full)
|
|
477
|
+
color = carrier_colors.get(carrier_name) if carrier_name else None
|
|
478
|
+
links['color'].append(hex_to_rgba(color, alpha=0.4) if color else hex_to_rgba('', alpha=0.4))
|
|
479
|
+
|
|
480
|
+
# Create figure
|
|
481
|
+
node_list = list(nodes)
|
|
482
|
+
node_indices = {n: i for i, n in enumerate(node_list)}
|
|
483
|
+
|
|
484
|
+
# Get colors for buses and components using cached colors
|
|
485
|
+
bus_colors_cached = self.bus_colors
|
|
486
|
+
component_colors_cached = self.component_colors
|
|
487
|
+
|
|
488
|
+
# If user provided colors, process them for buses
|
|
489
|
+
if colors is not None:
|
|
490
|
+
bus_labels = [bus.label for bus in self._fs.buses.values()]
|
|
491
|
+
bus_color_map = process_colors(colors, bus_labels)
|
|
492
|
+
else:
|
|
493
|
+
bus_color_map = bus_colors_cached
|
|
494
|
+
|
|
495
|
+
# Assign colors to nodes: buses get their color, components get their color or neutral gray
|
|
496
|
+
node_colors = []
|
|
497
|
+
for node in node_list:
|
|
498
|
+
if node in bus_color_map:
|
|
499
|
+
node_colors.append(bus_color_map[node])
|
|
500
|
+
elif node in component_colors_cached:
|
|
501
|
+
node_colors.append(component_colors_cached[node])
|
|
502
|
+
else:
|
|
503
|
+
# Fallback - use a neutral gray
|
|
504
|
+
node_colors.append('#808080')
|
|
505
|
+
|
|
506
|
+
# Build hover text for nodes
|
|
507
|
+
node_customdata = [node_hover.get(node, node) for node in node_list]
|
|
508
|
+
|
|
509
|
+
fig = go.Figure(
|
|
510
|
+
data=[
|
|
511
|
+
go.Sankey(
|
|
512
|
+
node=dict(
|
|
513
|
+
pad=15,
|
|
514
|
+
thickness=20,
|
|
515
|
+
line=dict(color='black', width=0.5),
|
|
516
|
+
label=node_list,
|
|
517
|
+
color=node_colors,
|
|
518
|
+
customdata=node_customdata,
|
|
519
|
+
hovertemplate='%{customdata}<extra></extra>',
|
|
520
|
+
),
|
|
521
|
+
link=dict(
|
|
522
|
+
source=[node_indices[s] for s in links['source']],
|
|
523
|
+
target=[node_indices[t] for t in links['target']],
|
|
524
|
+
value=links['value'],
|
|
525
|
+
label=links['label'],
|
|
526
|
+
customdata=links['customdata'],
|
|
527
|
+
hovertemplate='%{customdata}<extra></extra>',
|
|
528
|
+
color=links['color'], # Carrier-based colors
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
title = plotly_kwargs.pop('title', 'Flow System Topology')
|
|
534
|
+
fig.update_layout(title=title, **plotly_kwargs)
|
|
535
|
+
|
|
536
|
+
# Build xarray Dataset with topology data
|
|
537
|
+
data = xr.Dataset(
|
|
538
|
+
{
|
|
539
|
+
'source': ('link', links['source']),
|
|
540
|
+
'target': ('link', links['target']),
|
|
541
|
+
'value': ('link', links['value']),
|
|
542
|
+
},
|
|
543
|
+
coords={'link': links['label']},
|
|
544
|
+
)
|
|
545
|
+
result = PlotResult(data=data, figure=fig)
|
|
546
|
+
|
|
547
|
+
if show is None:
|
|
548
|
+
show = CONFIG.Plotting.default_show
|
|
549
|
+
if show:
|
|
550
|
+
result.show()
|
|
551
|
+
|
|
552
|
+
return result
|
|
553
|
+
|
|
554
|
+
def plot_legacy(
|
|
555
|
+
self,
|
|
556
|
+
path: bool | str | pathlib.Path = 'flow_system.html',
|
|
557
|
+
controls: bool
|
|
558
|
+
| list[
|
|
559
|
+
Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
|
|
560
|
+
] = True,
|
|
561
|
+
show: bool | None = None,
|
|
562
|
+
) -> pyvis.network.Network | None:
|
|
563
|
+
"""
|
|
564
|
+
Visualize the network structure using PyVis, saving it as an interactive HTML file.
|
|
565
|
+
|
|
566
|
+
.. deprecated::
|
|
567
|
+
Use `plot()` instead for the new Plotly-based Sankey visualization.
|
|
568
|
+
This method is kept for backwards compatibility.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
path: Path to save the HTML visualization.
|
|
572
|
+
- `False`: Visualization is created but not saved.
|
|
573
|
+
- `str` or `Path`: Specifies file path (default: 'flow_system.html').
|
|
574
|
+
controls: UI controls to add to the visualization.
|
|
575
|
+
- `True`: Enables all available controls.
|
|
576
|
+
- `List`: Specify controls, e.g., ['nodes', 'layout'].
|
|
577
|
+
- Options: 'nodes', 'edges', 'layout', 'interaction', 'manipulation',
|
|
578
|
+
'physics', 'selection', 'renderer'.
|
|
579
|
+
show: Whether to open the visualization in the web browser.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
The `pyvis.network.Network` instance representing the visualization,
|
|
583
|
+
or `None` if `pyvis` is not installed.
|
|
584
|
+
|
|
585
|
+
Examples:
|
|
586
|
+
>>> flow_system.topology.plot_legacy()
|
|
587
|
+
>>> flow_system.topology.plot_legacy(show=False)
|
|
588
|
+
>>> flow_system.topology.plot_legacy(path='output/network.html', controls=['nodes', 'layout'])
|
|
589
|
+
|
|
590
|
+
Notes:
|
|
591
|
+
This function requires `pyvis`. If not installed, the function prints
|
|
592
|
+
a warning and returns `None`.
|
|
593
|
+
Nodes are styled based on type (circles for buses, boxes for components)
|
|
594
|
+
and annotated with node information.
|
|
595
|
+
"""
|
|
596
|
+
warnings.warn(
|
|
597
|
+
f'This method is deprecated and will be removed in v{DEPRECATION_REMOVAL_VERSION}. '
|
|
598
|
+
'Use flow_system.topology.plot() instead.',
|
|
599
|
+
DeprecationWarning,
|
|
600
|
+
stacklevel=2,
|
|
601
|
+
)
|
|
602
|
+
node_infos, edge_infos = self.infos()
|
|
603
|
+
# Normalize path=False to None for _plot_network compatibility
|
|
604
|
+
normalized_path = None if path is False else path
|
|
605
|
+
return _plot_network(
|
|
606
|
+
node_infos,
|
|
607
|
+
edge_infos,
|
|
608
|
+
normalized_path,
|
|
609
|
+
controls,
|
|
610
|
+
show if show is not None else CONFIG.Plotting.default_show,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
def start_app(self) -> None:
|
|
614
|
+
"""
|
|
615
|
+
Start an interactive network visualization using Dash and Cytoscape.
|
|
616
|
+
|
|
617
|
+
Launches a web-based interactive visualization server that allows
|
|
618
|
+
exploring the network structure dynamically.
|
|
619
|
+
|
|
620
|
+
Raises:
|
|
621
|
+
ImportError: If required dependencies are not installed.
|
|
622
|
+
|
|
623
|
+
Examples:
|
|
624
|
+
>>> flow_system.topology.start_app()
|
|
625
|
+
>>> # ... interact with the visualization in browser ...
|
|
626
|
+
>>> flow_system.topology.stop_app()
|
|
627
|
+
|
|
628
|
+
Notes:
|
|
629
|
+
Requires optional dependencies: dash, dash-cytoscape, dash-daq,
|
|
630
|
+
networkx, flask, werkzeug.
|
|
631
|
+
Install with: `pip install flixopt[network_viz]` or `pip install flixopt[full]`
|
|
632
|
+
"""
|
|
633
|
+
from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR, flow_graph, shownetwork
|
|
634
|
+
|
|
635
|
+
warnings.warn(
|
|
636
|
+
'The network visualization is still experimental and might change in the future.',
|
|
637
|
+
stacklevel=2,
|
|
638
|
+
category=UserWarning,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
642
|
+
raise ImportError(
|
|
643
|
+
f'Network visualization requires optional dependencies. '
|
|
644
|
+
f'Install with: `pip install flixopt[network_viz]`, `pip install flixopt[full]` '
|
|
645
|
+
f'or: `pip install dash dash-cytoscape dash-daq networkx werkzeug`. '
|
|
646
|
+
f'Original error: {VISUALIZATION_ERROR}'
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if not self._fs._connected_and_transformed:
|
|
650
|
+
self._fs._connect_network()
|
|
651
|
+
|
|
652
|
+
if self._fs._network_app is not None:
|
|
653
|
+
logger.warning('The network app is already running. Restarting it.')
|
|
654
|
+
self.stop_app()
|
|
655
|
+
|
|
656
|
+
self._fs._network_app = shownetwork(flow_graph(self._fs))
|
|
657
|
+
|
|
658
|
+
def stop_app(self) -> None:
|
|
659
|
+
"""
|
|
660
|
+
Stop the interactive network visualization server.
|
|
661
|
+
|
|
662
|
+
Examples:
|
|
663
|
+
>>> flow_system.topology.stop_app()
|
|
664
|
+
"""
|
|
665
|
+
if self._fs._network_app is None:
|
|
666
|
+
logger.warning("No network app is currently running. Can't stop it")
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
try:
|
|
670
|
+
logger.info('Stopping network visualization server...')
|
|
671
|
+
self._fs._network_app.server_instance.shutdown()
|
|
672
|
+
logger.info('Network visualization stopped.')
|
|
673
|
+
except Exception as e:
|
|
674
|
+
logger.error(f'Failed to stop the network visualization app: {e}')
|
|
675
|
+
finally:
|
|
676
|
+
self._fs._network_app = None
|