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.
Files changed (42) hide show
  1. flixopt/__init__.py +57 -49
  2. flixopt/carrier.py +159 -0
  3. flixopt/clustering/__init__.py +51 -0
  4. flixopt/clustering/base.py +1746 -0
  5. flixopt/clustering/intercluster_helpers.py +201 -0
  6. flixopt/color_processing.py +372 -0
  7. flixopt/comparison.py +819 -0
  8. flixopt/components.py +848 -270
  9. flixopt/config.py +853 -496
  10. flixopt/core.py +111 -98
  11. flixopt/effects.py +294 -284
  12. flixopt/elements.py +484 -223
  13. flixopt/features.py +220 -118
  14. flixopt/flow_system.py +2026 -389
  15. flixopt/interface.py +504 -286
  16. flixopt/io.py +1718 -55
  17. flixopt/linear_converters.py +291 -230
  18. flixopt/modeling.py +304 -181
  19. flixopt/network_app.py +2 -1
  20. flixopt/optimization.py +788 -0
  21. flixopt/optimize_accessor.py +373 -0
  22. flixopt/plot_result.py +143 -0
  23. flixopt/plotting.py +1177 -1034
  24. flixopt/results.py +1331 -372
  25. flixopt/solvers.py +12 -4
  26. flixopt/statistics_accessor.py +2412 -0
  27. flixopt/stats_accessor.py +75 -0
  28. flixopt/structure.py +954 -120
  29. flixopt/topology_accessor.py +676 -0
  30. flixopt/transform_accessor.py +2277 -0
  31. flixopt/types.py +120 -0
  32. flixopt-6.0.0rc7.dist-info/METADATA +290 -0
  33. flixopt-6.0.0rc7.dist-info/RECORD +36 -0
  34. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
  35. flixopt/aggregation.py +0 -382
  36. flixopt/calculation.py +0 -672
  37. flixopt/commons.py +0 -51
  38. flixopt/utils.py +0 -86
  39. flixopt-3.0.1.dist-info/METADATA +0 -209
  40. flixopt-3.0.1.dist-info/RECORD +0 -26
  41. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  42. {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