flixopt 2.1.4__py3-none-any.whl → 2.1.6__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.

flixopt/aggregation.py CHANGED
@@ -34,7 +34,6 @@ from .structure import (
34
34
  if TYPE_CHECKING:
35
35
  import plotly.graph_objects as go
36
36
 
37
- warnings.filterwarnings('ignore', category=DeprecationWarning)
38
37
  logger = logging.getLogger('flixopt')
39
38
 
40
39
 
flixopt/components.py CHANGED
@@ -3,6 +3,7 @@ This module contains the basic components of the flixopt framework.
3
3
  """
4
4
 
5
5
  import logging
6
+ import warnings
6
7
  from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Set, Tuple, Union
7
8
 
8
9
  import linopy
@@ -586,52 +587,160 @@ class SourceAndSink(Component):
586
587
  def __init__(
587
588
  self,
588
589
  label: str,
589
- source: Flow,
590
- sink: Flow,
591
- prevent_simultaneous_sink_and_source: bool = True,
590
+ inputs: List[Flow] = None,
591
+ outputs: List[Flow] = None,
592
+ prevent_simultaneous_flow_rates: bool = True,
592
593
  meta_data: Optional[Dict] = None,
594
+ **kwargs,
593
595
  ):
594
596
  """
595
597
  Args:
596
598
  label: The label of the Element. Used to identify it in the FlowSystem
597
- source: output-flow of this component
598
- sink: input-flow of this component
599
- prevent_simultaneous_sink_and_source: If True, inflow and outflow can not be active simultaniously.
599
+ outputs: output-flows of this component
600
+ inputs: input-flows of this component
601
+ prevent_simultaneous_flow_rates: If True, inflow and outflow can not be active simultaniously.
600
602
  meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
601
603
  """
604
+ source = kwargs.pop('source', None)
605
+ sink = kwargs.pop('sink', None)
606
+ prevent_simultaneous_sink_and_source = kwargs.pop('prevent_simultaneous_sink_and_source', None)
607
+ if source is not None:
608
+ warnings.deprecated(
609
+ 'The use of the source argument is deprecated. Use the outputs argument instead.',
610
+ stacklevel=2,
611
+ )
612
+ if outputs is not None:
613
+ raise ValueError('Either source or outputs can be specified, but not both.')
614
+ outputs = [source]
615
+
616
+ if sink is not None:
617
+ warnings.deprecated(
618
+ 'The use of the sink argument is deprecated. Use the outputs argument instead.',
619
+ stacklevel=2,
620
+ )
621
+ if inputs is not None:
622
+ raise ValueError('Either sink or outputs can be specified, but not both.')
623
+ inputs = [sink]
624
+
625
+ if prevent_simultaneous_sink_and_source is not None:
626
+ warnings.deprecated(
627
+ 'The use of the prevent_simultaneous_sink_and_source argument is deprecated. Use the prevent_simultaneous_flow_rates argument instead.',
628
+ stacklevel=2,
629
+ )
630
+ prevent_simultaneous_flow_rates = prevent_simultaneous_sink_and_source
631
+
602
632
  super().__init__(
603
633
  label,
604
- inputs=[sink],
605
- outputs=[source],
606
- prevent_simultaneous_flows=[sink, source] if prevent_simultaneous_sink_and_source is True else None,
634
+ inputs=inputs,
635
+ outputs=outputs,
636
+ prevent_simultaneous_flows=inputs + outputs if prevent_simultaneous_flow_rates is True else None,
607
637
  meta_data=meta_data,
608
638
  )
609
- self.source = source
610
- self.sink = sink
611
- self.prevent_simultaneous_sink_and_source = prevent_simultaneous_sink_and_source
639
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
640
+
641
+ @property
642
+ def source(self) -> Flow:
643
+ warnings.warn(
644
+ 'The source property is deprecated. Use the outputs property instead.',
645
+ DeprecationWarning,
646
+ stacklevel=2,
647
+ )
648
+ return self.outputs[0]
649
+
650
+ @property
651
+ def sink(self) -> Flow:
652
+ warnings.warn(
653
+ 'The sink property is deprecated. Use the outputs property instead.',
654
+ DeprecationWarning,
655
+ stacklevel=2,
656
+ )
657
+ return self.inputs[0]
658
+
659
+ @property
660
+ def prevent_simultaneous_sink_and_source(self) -> bool:
661
+ warnings.warn(
662
+ 'The prevent_simultaneous_sink_and_source property is deprecated. Use the prevent_simultaneous_flow_rates property instead.',
663
+ DeprecationWarning,
664
+ stacklevel=2,
665
+ )
666
+ return self.prevent_simultaneous_flow_rates
612
667
 
613
668
 
614
669
  @register_class_for_io
615
670
  class Source(Component):
616
- def __init__(self, label: str, source: Flow, meta_data: Optional[Dict] = None):
671
+ def __init__(
672
+ self,
673
+ label: str,
674
+ outputs: List[Flow] = None,
675
+ meta_data: Optional[Dict] = None,
676
+ prevent_simultaneous_flow_rates: bool = False,
677
+ **kwargs
678
+ ):
617
679
  """
618
680
  Args:
619
681
  label: The label of the Element. Used to identify it in the FlowSystem
620
- source: output-flow of source
682
+ outputs: output-flows of source
621
683
  meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
622
684
  """
623
- super().__init__(label, outputs=[source], meta_data=meta_data)
624
- self.source = source
685
+ source = kwargs.pop('source', None)
686
+ if source is not None:
687
+ warnings.warn(
688
+ 'The use of the source argument is deprecated. Use the outputs argument instead.',
689
+ DeprecationWarning,
690
+ stacklevel=2,
691
+ )
692
+ if outputs is not None:
693
+ raise ValueError('Either source or outputs can be specified, but not both.')
694
+ outputs = [source]
695
+
696
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
697
+ super().__init__(label, outputs=outputs, meta_data=meta_data, prevent_simultaneous_flows=outputs if prevent_simultaneous_flow_rates else None)
698
+
699
+ @property
700
+ def source(self) -> Flow:
701
+ warnings.warn(
702
+ 'The source property is deprecated. Use the outputs property instead.',
703
+ DeprecationWarning,
704
+ stacklevel=2,
705
+ )
706
+ return self.outputs[0]
625
707
 
626
708
 
627
709
  @register_class_for_io
628
710
  class Sink(Component):
629
- def __init__(self, label: str, sink: Flow, meta_data: Optional[Dict] = None):
711
+ def __init__(
712
+ self,
713
+ label: str,
714
+ inputs: List[Flow] = None,
715
+ meta_data: Optional[Dict] = None,
716
+ prevent_simultaneous_flow_rates: bool = False,
717
+ **kwargs
718
+ ):
630
719
  """
631
720
  Args:
632
721
  label: The label of the Element. Used to identify it in the FlowSystem
633
- meta_data: used to store more information about the element. Is not used internally, but saved in the results
634
- sink: input-flow of sink
722
+ inputs: output-flows of source
723
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
635
724
  """
636
- super().__init__(label, inputs=[sink], meta_data=meta_data)
637
- self.sink = sink
725
+ sink = kwargs.pop('sink', None)
726
+ if sink is not None:
727
+ warnings.warn(
728
+ 'The use of the sink argument is deprecated. Use the outputs argument instead.',
729
+ DeprecationWarning,
730
+ stacklevel=2,
731
+ )
732
+ if inputs is not None:
733
+ raise ValueError('Either sink or outputs can be specified, but not both.')
734
+ inputs = [sink]
735
+
736
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
737
+ super().__init__(label, inputs=inputs, meta_data=meta_data, prevent_simultaneous_flows=inputs if prevent_simultaneous_flow_rates else None)
738
+
739
+ @property
740
+ def sink(self) -> Flow:
741
+ warnings.warn(
742
+ 'The sink property is deprecated. Use the outputs property instead.',
743
+ DeprecationWarning,
744
+ stacklevel=2,
745
+ )
746
+ return self.inputs[0]
flixopt/flow_system.py CHANGED
@@ -61,6 +61,8 @@ class FlowSystem:
61
61
 
62
62
  self._connected = False
63
63
 
64
+ self._network_app = None
65
+
64
66
  @classmethod
65
67
  def from_dataset(cls, ds: xr.Dataset):
66
68
  timesteps_extra = pd.DatetimeIndex(ds.attrs['timesteps_extra'], name='time')
@@ -241,6 +243,57 @@ class FlowSystem:
241
243
  node_infos, edge_infos = self.network_infos()
242
244
  return plotting.plot_network(node_infos, edge_infos, path, controls, show)
243
245
 
246
+ def start_network_app(self):
247
+ """Visualizes the network structure of a FlowSystem using Dash, Cytoscape, and networkx.
248
+ Requires optional dependencies: dash, dash-cytoscape, networkx, werkzeug.
249
+ """
250
+ from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR, flow_graph, shownetwork
251
+
252
+ warnings.warn(
253
+ 'The network visualization is still experimental and might change in the future.',
254
+ stacklevel=2,
255
+ category=UserWarning,
256
+ )
257
+
258
+ if not DASH_CYTOSCAPE_AVAILABLE:
259
+ raise ImportError(
260
+ f"Network visualization requires optional dependencies. "
261
+ f"Install with: pip install flixopt[viz], flixopt[full] or pip install dash dash_cytoscape networkx werkzeug. "
262
+ f"Original error: {VISUALIZATION_ERROR}"
263
+ )
264
+
265
+ if not self._connected:
266
+ self._connect_network()
267
+
268
+ if self._network_app is not None:
269
+ logger.warning('The network app is already running. Restarting it.')
270
+ self.stop_network_app()
271
+
272
+ self._network_app = shownetwork(flow_graph(self))
273
+
274
+ def stop_network_app(self):
275
+ """Stop the network visualization server."""
276
+ from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR
277
+ if not DASH_CYTOSCAPE_AVAILABLE:
278
+ raise ImportError(
279
+ f'Network visualization requires optional dependencies. '
280
+ f'Install with: pip install flixopt[viz]. '
281
+ f'Original error: {VISUALIZATION_ERROR}'
282
+ )
283
+
284
+ if self._network_app is None:
285
+ logger.warning('No network app is currently running. Cant stop it')
286
+ return
287
+
288
+ try:
289
+ logger.info('Stopping network visualization server...')
290
+ self._network_app.server_instance.shutdown()
291
+ logger.info('Network visualization stopped.')
292
+ except Exception as e:
293
+ logger.error(f'Failed to stop the network visualization app: {e}')
294
+ finally:
295
+ self._network_app = None
296
+
244
297
  def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]:
245
298
  if not self._connected:
246
299
  self._connect_network()
flixopt/network_app.py ADDED
@@ -0,0 +1,612 @@
1
+ import json
2
+ import logging
3
+ import socket
4
+ import threading
5
+ from typing import Any, Dict, List
6
+
7
+ try:
8
+ import dash_cytoscape as cyto
9
+ import dash_daq as daq
10
+ import networkx
11
+ from dash import Dash, Input, Output, State, callback_context, dcc, html, no_update
12
+ from werkzeug.serving import make_server
13
+
14
+ DASH_CYTOSCAPE_AVAILABLE = True
15
+ VISUALIZATION_ERROR = None
16
+ except ImportError as e:
17
+ DASH_CYTOSCAPE_AVAILABLE = False
18
+ VISUALIZATION_ERROR = str(e)
19
+
20
+ from .components import LinearConverter, Sink, Source, SourceAndSink, Storage
21
+ from .elements import Bus, Component, Flow
22
+ from .flow_system import FlowSystem
23
+
24
+ logger = logging.getLogger('flixopt')
25
+
26
+ # Configuration class for better organization
27
+ class VisualizationConfig:
28
+ """Configuration constants for the visualization"""
29
+
30
+ DEFAULT_COLORS = {
31
+ 'Bus': '#7F8C8D',
32
+ 'Source': '#F1C40F',
33
+ 'Sink': '#F1C40F',
34
+ 'Storage': '#2980B9',
35
+ 'Converter': '#D35400',
36
+ 'Other': '#27AE60',
37
+ }
38
+
39
+ COLOR_PRESETS = {
40
+ 'Default': DEFAULT_COLORS,
41
+ 'Vibrant': {
42
+ 'Bus': '#FF6B6B', 'Source': '#4ECDC4', 'Sink': '#45B7D1',
43
+ 'Storage': '#96CEB4', 'Converter': '#FFEAA7', 'Other': '#DDA0DD',
44
+ },
45
+ 'Dark': {
46
+ 'Bus': '#2C3E50', 'Source': '#34495E', 'Sink': '#7F8C8D',
47
+ 'Storage': '#95A5A6', 'Converter': '#BDC3C7', 'Other': '#ECF0F1',
48
+ },
49
+ 'Pastel': {
50
+ 'Bus': '#FFB3BA', 'Source': '#BAFFC9', 'Sink': '#BAE1FF',
51
+ 'Storage': '#FFFFBA', 'Converter': '#FFDFBA', 'Other': '#E0BBE4',
52
+ },
53
+ }
54
+
55
+ DEFAULT_STYLESHEET = [
56
+ {
57
+ 'selector': 'node',
58
+ 'style': {
59
+ 'content': 'data(label)',
60
+ 'background-color': 'data(color)',
61
+ 'font-size': 10,
62
+ 'color': 'white',
63
+ 'text-valign': 'center',
64
+ 'text-halign': 'center',
65
+ 'width': '90px',
66
+ 'height': '70px',
67
+ 'shape': 'data(shape)',
68
+ 'text-outline-color': 'black',
69
+ 'text-outline-width': 0.5,
70
+ },
71
+ },
72
+ {
73
+ 'selector': '[shape = "custom-source"]',
74
+ 'style': {
75
+ 'shape': 'polygon',
76
+ 'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',
77
+ },
78
+ },
79
+ {
80
+ 'selector': '[shape = "custom-sink"]',
81
+ 'style': {
82
+ 'shape': 'polygon',
83
+ 'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',
84
+ },
85
+ },
86
+ {
87
+ 'selector': 'edge',
88
+ 'style': {
89
+ 'curve-style': 'straight',
90
+ 'width': 2,
91
+ 'line-color': 'gray',
92
+ 'target-arrow-color': 'gray',
93
+ 'target-arrow-shape': 'triangle',
94
+ 'arrow-scale': 2,
95
+ },
96
+ },
97
+ ]
98
+
99
+ def flow_graph(flow_system: FlowSystem) -> networkx.DiGraph:
100
+ """Convert FlowSystem to NetworkX graph - simplified and more robust"""
101
+ nodes = list(flow_system.components.values()) + list(flow_system.buses.values())
102
+ edges = list(flow_system.flows.values())
103
+
104
+ def get_element_type(element):
105
+ """Determine element type for coloring"""
106
+ if isinstance(element, Bus):
107
+ return 'Bus'
108
+ elif isinstance(element, Source):
109
+ return 'Source'
110
+ elif isinstance(element, (Sink, SourceAndSink)):
111
+ return 'Sink'
112
+ elif isinstance(element, Storage):
113
+ return 'Storage'
114
+ elif isinstance(element, LinearConverter):
115
+ return 'Converter'
116
+ else:
117
+ return 'Other'
118
+
119
+ def get_shape(element):
120
+ """Determine node shape"""
121
+ if isinstance(element, Bus):
122
+ return 'ellipse'
123
+ elif isinstance(element, Source):
124
+ return 'custom-source'
125
+ elif isinstance(element, (Sink, SourceAndSink)):
126
+ return 'custom-sink'
127
+ else:
128
+ return 'rectangle'
129
+
130
+ graph = networkx.DiGraph()
131
+
132
+ # Add nodes with attributes
133
+ for node in nodes:
134
+ graph.add_node(
135
+ node.label_full,
136
+ color=VisualizationConfig.DEFAULT_COLORS[get_element_type(node)],
137
+ shape=get_shape(node),
138
+ element_type=get_element_type(node),
139
+ parameters=str(node),
140
+ )
141
+
142
+ # Add edges
143
+ for edge in edges:
144
+ try:
145
+ graph.add_edge(
146
+ u_of_edge=edge.bus if edge.is_input_in_component else edge.component,
147
+ v_of_edge=edge.component if edge.is_input_in_component else edge.bus,
148
+ label=edge.label_full,
149
+ parameters=edge.__str__().replace(')', '\n)'),
150
+ )
151
+ except Exception as e:
152
+ logger.error(f"Failed to add edge {edge}: {e}")
153
+
154
+ return graph
155
+
156
+ def make_cytoscape_elements(graph: networkx.DiGraph) -> List[Dict[str, Any]]:
157
+ """Convert NetworkX graph to Cytoscape elements"""
158
+ elements = []
159
+
160
+ # Add nodes
161
+ for node_id in graph.nodes():
162
+ node_data = graph.nodes[node_id]
163
+ elements.append({
164
+ 'data': {
165
+ 'id': node_id,
166
+ 'label': node_id,
167
+ 'color': node_data.get('color', '#7F8C8D'),
168
+ 'shape': node_data.get('shape', 'rectangle'),
169
+ 'element_type': node_data.get('element_type', 'Other'),
170
+ 'parameters': node_data.get('parameters', ''),
171
+ }
172
+ })
173
+
174
+ # Add edges
175
+ for u, v in graph.edges():
176
+ edge_data = graph.edges[u, v]
177
+ elements.append({
178
+ 'data': {
179
+ 'source': u,
180
+ 'target': v,
181
+ 'id': f'{u}-{v}',
182
+ 'label': edge_data.get('label', ''),
183
+ 'parameters': edge_data.get('parameters', ''),
184
+ }
185
+ })
186
+
187
+ return elements
188
+
189
+ def create_color_picker_input(label: str, input_id: str, default_color: str):
190
+ """Create a compact color picker with DAQ ColorPicker"""
191
+ return html.Div([
192
+ html.Label(label, style={
193
+ 'color': 'white', 'font-size': '12px', 'margin-bottom': '5px',
194
+ 'display': 'block'
195
+ }),
196
+ daq.ColorPicker(
197
+ id=input_id,
198
+ label="",
199
+ value={'hex': default_color},
200
+ size=200,
201
+ theme={'dark': True},
202
+ style={'margin-bottom': '10px'}
203
+ ),
204
+ ])
205
+
206
+ def create_style_section(title: str, children: List):
207
+ """Create a collapsible section for organizing controls"""
208
+ return html.Div([
209
+ html.H4(title, style={
210
+ 'color': 'white', 'margin-bottom': '10px',
211
+ 'border-bottom': '2px solid #3498DB', 'padding-bottom': '5px',
212
+ }),
213
+ html.Div(children, style={'margin-bottom': '20px'}),
214
+ ])
215
+
216
+ def create_sidebar():
217
+ """Create the main sidebar with improved organization"""
218
+ return html.Div([
219
+ html.Div([
220
+ html.H3('Style Controls', style={
221
+ 'color': 'white', 'margin-bottom': '20px', 'text-align': 'center',
222
+ 'border-bottom': '3px solid #9B59B6', 'padding-bottom': '10px',
223
+ }),
224
+
225
+ # Layout Section
226
+ create_style_section('Layout', [
227
+ dcc.Dropdown(
228
+ id='layout-dropdown',
229
+ options=[
230
+ {'label': 'Klay (horizontal)', 'value': 'klay'},
231
+ {'label': 'Dagre (vertical)', 'value': 'dagre'},
232
+ {'label': 'Breadthfirst', 'value': 'breadthfirst'},
233
+ {'label': 'Cose (force-directed)', 'value': 'cose'},
234
+ {'label': 'Grid', 'value': 'grid'},
235
+ {'label': 'Circle', 'value': 'circle'},
236
+ ],
237
+ value='klay',
238
+ clearable=False,
239
+ style={'width': '100%'},
240
+ ),
241
+ ]),
242
+
243
+ # Color Scheme Section
244
+ create_style_section('Color Scheme', [
245
+ dcc.Dropdown(
246
+ id='color-scheme-dropdown',
247
+ options=[{'label': k, 'value': k} for k in VisualizationConfig.COLOR_PRESETS.keys()],
248
+ value='Default',
249
+ style={'width': '100%', 'margin-bottom': '10px'},
250
+ ),
251
+ ]),
252
+
253
+ # Color Pickers Section
254
+ create_style_section('Custom Colors', [
255
+ create_color_picker_input('Bus', 'bus-color-picker', '#7F8C8D'),
256
+ create_color_picker_input('Source', 'source-color-picker', '#F1C40F'),
257
+ create_color_picker_input('Sink', 'sink-color-picker', '#F1C40F'),
258
+ create_color_picker_input('Storage', 'storage-color-picker', '#2980B9'),
259
+ create_color_picker_input('Converter', 'converter-color-picker', '#D35400'),
260
+ create_color_picker_input('Edge', 'edge-color-picker', '#808080'),
261
+ ]),
262
+
263
+ # Node Settings
264
+ create_style_section('Node Settings', [
265
+ html.Label('Size', style={'color': 'white', 'font-size': '12px'}),
266
+ dcc.Slider(
267
+ id='node-size-slider', min=50, max=150, step=10, value=90,
268
+ marks={i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
269
+ for i in range(50, 151, 25)},
270
+ tooltip={'placement': 'bottom', 'always_visible': True},
271
+ ),
272
+ html.Br(),
273
+ html.Label('Font Size', style={'color': 'white', 'font-size': '12px'}),
274
+ dcc.Slider(
275
+ id='font-size-slider', min=8, max=20, step=1, value=10,
276
+ marks={i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
277
+ for i in range(8, 21, 2)},
278
+ tooltip={'placement': 'bottom', 'always_visible': True},
279
+ ),
280
+ ]),
281
+
282
+ # Reset Button
283
+ html.Div([
284
+ html.Button('Reset to Defaults', id='reset-btn', n_clicks=0, style={
285
+ 'width': '100%', 'background-color': '#E74C3C', 'color': 'white',
286
+ 'border': 'none', 'padding': '10px', 'border-radius': '5px',
287
+ 'cursor': 'pointer', 'margin-top': '20px',
288
+ }),
289
+ ]),
290
+ ], id='sidebar-content', style={
291
+ 'width': '280px', 'height': '100vh', 'background-color': '#2C3E50',
292
+ 'padding': '20px', 'position': 'fixed', 'left': '0', 'top': '0',
293
+ 'overflow-y': 'auto', 'border-right': '3px solid #34495E',
294
+ 'box-shadow': '2px 0 5px rgba(0,0,0,0.1)', 'z-index': '999',
295
+ 'transform': 'translateX(-100%)', 'transition': 'transform 0.3s ease',
296
+ })
297
+ ])
298
+
299
+ def shownetwork(graph: networkx.DiGraph):
300
+ """Main function to create and run the network visualization"""
301
+ if not DASH_CYTOSCAPE_AVAILABLE:
302
+ raise ImportError(f"Required packages not available: {VISUALIZATION_ERROR}")
303
+
304
+ app = Dash(__name__, suppress_callback_exceptions=True)
305
+
306
+ # Load extra layouts
307
+ cyto.load_extra_layouts()
308
+
309
+ # Create initial elements
310
+ elements = make_cytoscape_elements(graph)
311
+
312
+ # App Layout
313
+ app.layout = html.Div([
314
+ # Toggle button
315
+ html.Button('☰', id='toggle-sidebar', n_clicks=0, style={
316
+ 'position': 'fixed', 'top': '20px', 'left': '20px', 'z-index': '1000',
317
+ 'background-color': '#3498DB', 'color': 'white', 'border': 'none',
318
+ 'padding': '10px 15px', 'border-radius': '5px', 'cursor': 'pointer',
319
+ 'font-size': '18px', 'box-shadow': '0 2px 5px rgba(0,0,0,0.3)',
320
+ }),
321
+
322
+ # Data storage
323
+ dcc.Store(id='elements-store', data=elements),
324
+
325
+ # Sidebar
326
+ create_sidebar(),
327
+
328
+ # Main content
329
+ html.Div([
330
+ # Header
331
+ html.Div([
332
+ html.H2('Network Visualization', style={
333
+ 'color': 'white', 'margin': '0', 'text-align': 'center'
334
+ }),
335
+ html.Button('Export PNG', id='export-btn', n_clicks=0, style={
336
+ 'position': 'absolute', 'right': '20px', 'top': '15px',
337
+ 'background-color': '#27AE60', 'color': 'white', 'border': 'none',
338
+ 'padding': '10px 20px', 'border-radius': '5px', 'cursor': 'pointer',
339
+ }),
340
+ ], style={
341
+ 'background-color': '#34495E', 'padding': '15px 20px',
342
+ 'position': 'relative', 'border-bottom': '2px solid #3498DB',
343
+ }),
344
+
345
+ # Cytoscape graph
346
+ cyto.Cytoscape(
347
+ id='cytoscape',
348
+ layout={'name': 'klay'},
349
+ style={'width': '100%', 'height': '70vh'},
350
+ elements=elements,
351
+ stylesheet=VisualizationConfig.DEFAULT_STYLESHEET,
352
+ ),
353
+
354
+ # Info panel
355
+ html.Div([
356
+ html.H4('Element Information', style={
357
+ 'color': 'white', 'margin': '0 0 10px 0',
358
+ 'border-bottom': '2px solid #3498DB', 'padding-bottom': '5px',
359
+ }),
360
+ html.Div(id='info-panel', children=[
361
+ html.P('Click on a node or edge to see details.',
362
+ style={'color': '#95A5A6', 'font-style': 'italic'})
363
+ ]),
364
+ ], style={
365
+ 'background-color': '#2C3E50', 'padding': '15px',
366
+ 'height': '25vh', 'overflow-y': 'auto',
367
+ 'border-top': '2px solid #34495E',
368
+ }),
369
+ ], id='main-content', style={
370
+ 'margin-left': '0', 'background-color': '#1A252F',
371
+ 'min-height': '100vh', 'transition': 'margin-left 0.3s ease',
372
+ }),
373
+ ])
374
+
375
+ # Callbacks
376
+ @app.callback(
377
+ [Output('sidebar-content', 'style'), Output('main-content', 'style')],
378
+ [Input('toggle-sidebar', 'n_clicks')]
379
+ )
380
+ def toggle_sidebar(n_clicks):
381
+ is_open = (n_clicks or 0) % 2 == 1
382
+ sidebar_transform = 'translateX(0)' if is_open else 'translateX(-100%)'
383
+ main_margin = '280px' if is_open else '0'
384
+
385
+ sidebar_style = {
386
+ 'width': '280px', 'height': '100vh', 'background-color': '#2C3E50',
387
+ 'padding': '20px', 'position': 'fixed', 'left': '0', 'top': '0',
388
+ 'overflow-y': 'auto', 'border-right': '3px solid #34495E',
389
+ 'box-shadow': '2px 0 5px rgba(0,0,0,0.1)', 'z-index': '999',
390
+ 'transform': sidebar_transform, 'transition': 'transform 0.3s ease',
391
+ }
392
+
393
+ main_style = {
394
+ 'margin-left': main_margin, 'background-color': '#1A252F',
395
+ 'min-height': '100vh', 'transition': 'margin-left 0.3s ease',
396
+ }
397
+
398
+ return sidebar_style, main_style
399
+
400
+ @app.callback(
401
+ [Output('cytoscape', 'elements'), Output('cytoscape', 'stylesheet')],
402
+ [
403
+ Input('color-scheme-dropdown', 'value'),
404
+ Input('bus-color-picker', 'value'),
405
+ Input('source-color-picker', 'value'),
406
+ Input('sink-color-picker', 'value'),
407
+ Input('storage-color-picker', 'value'),
408
+ Input('converter-color-picker', 'value'),
409
+ Input('edge-color-picker', 'value'),
410
+ Input('node-size-slider', 'value'),
411
+ Input('font-size-slider', 'value'),
412
+ ],
413
+ [State('elements-store', 'data')]
414
+ )
415
+ def update_visualization(color_scheme, bus_color, source_color, sink_color,
416
+ storage_color, converter_color, edge_color,
417
+ node_size, font_size, stored_elements):
418
+ if not stored_elements:
419
+ return no_update, no_update
420
+
421
+ # Determine colors to use
422
+ if any(picker for picker in [bus_color, source_color, sink_color,
423
+ storage_color, converter_color, edge_color]):
424
+ # Use custom colors from pickers
425
+ colors = {
426
+ 'Bus': bus_color.get('hex') if bus_color else '#7F8C8D',
427
+ 'Source': source_color.get('hex') if source_color else '#F1C40F',
428
+ 'Sink': sink_color.get('hex') if sink_color else '#F1C40F',
429
+ 'Storage': storage_color.get('hex') if storage_color else '#2980B9',
430
+ 'Converter': converter_color.get('hex') if converter_color else '#D35400',
431
+ 'Other': '#27AE60',
432
+ }
433
+ else:
434
+ # Use preset scheme
435
+ colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme,
436
+ VisualizationConfig.DEFAULT_COLORS)
437
+
438
+ # Update element colors
439
+ updated_elements = []
440
+ for element in stored_elements:
441
+ if 'data' in element and 'element_type' in element['data']:
442
+ element_copy = element.copy()
443
+ element_copy['data'] = element['data'].copy()
444
+ element_type = element_copy['data']['element_type']
445
+ if element_type in colors:
446
+ element_copy['data']['color'] = colors[element_type]
447
+ updated_elements.append(element_copy)
448
+ else:
449
+ updated_elements.append(element)
450
+
451
+ # Create stylesheet
452
+ edge_color_hex = edge_color.get('hex') if edge_color else 'gray'
453
+ stylesheet = [
454
+ {
455
+ 'selector': 'node',
456
+ 'style': {
457
+ 'content': 'data(label)',
458
+ 'background-color': 'data(color)',
459
+ 'font-size': font_size or 10,
460
+ 'color': 'white',
461
+ 'text-valign': 'center',
462
+ 'text-halign': 'center',
463
+ 'width': f'{node_size or 90}px',
464
+ 'height': f'{int((node_size or 90) * 0.8)}px',
465
+ 'shape': 'data(shape)',
466
+ 'text-outline-color': 'black',
467
+ 'text-outline-width': 0.5,
468
+ },
469
+ },
470
+ {
471
+ 'selector': '[shape = "custom-source"]',
472
+ 'style': {
473
+ 'shape': 'polygon',
474
+ 'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',
475
+ },
476
+ },
477
+ {
478
+ 'selector': '[shape = "custom-sink"]',
479
+ 'style': {
480
+ 'shape': 'polygon',
481
+ 'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',
482
+ },
483
+ },
484
+ {
485
+ 'selector': 'edge',
486
+ 'style': {
487
+ 'curve-style': 'straight',
488
+ 'width': 2,
489
+ 'line-color': edge_color_hex,
490
+ 'target-arrow-color': edge_color_hex,
491
+ 'target-arrow-shape': 'triangle',
492
+ 'arrow-scale': 2,
493
+ },
494
+ },
495
+ ]
496
+
497
+ return updated_elements, stylesheet
498
+
499
+ @app.callback(
500
+ Output('info-panel', 'children'),
501
+ [Input('cytoscape', 'tapNodeData'), Input('cytoscape', 'tapEdgeData')]
502
+ )
503
+ def display_element_info(node_data, edge_data):
504
+ ctx = callback_context
505
+ if not ctx.triggered:
506
+ return [html.P('Click on a node or edge to see details.',
507
+ style={'color': '#95A5A6', 'font-style': 'italic'})]
508
+
509
+ # Determine what was clicked
510
+ if ctx.triggered[0]['prop_id'] == 'cytoscape.tapNodeData' and node_data:
511
+ return [
512
+ html.H5(f"Node: {node_data.get('label', 'Unknown')}",
513
+ style={'color': 'white', 'margin-bottom': '10px'}),
514
+ html.P(f"Type: {node_data.get('element_type', 'Unknown')}",
515
+ style={'color': '#BDC3C7'}),
516
+ html.Pre(node_data.get('parameters', 'No parameters'),
517
+ style={'color': '#BDC3C7', 'font-size': '11px',
518
+ 'white-space': 'pre-wrap'})
519
+ ]
520
+ elif ctx.triggered[0]['prop_id'] == 'cytoscape.tapEdgeData' and edge_data:
521
+ return [
522
+ html.H5(f"Edge: {edge_data.get('label', 'Unknown')}",
523
+ style={'color': 'white', 'margin-bottom': '10px'}),
524
+ html.P(f"{edge_data.get('source', '')} → {edge_data.get('target', '')}",
525
+ style={'color': '#E67E22'}),
526
+ html.Pre(edge_data.get('parameters', 'No parameters'),
527
+ style={'color': '#BDC3C7', 'font-size': '11px',
528
+ 'white-space': 'pre-wrap'})
529
+ ]
530
+
531
+ return [html.P('Click on a node or edge to see details.',
532
+ style={'color': '#95A5A6', 'font-style': 'italic'})]
533
+
534
+ @app.callback(
535
+ Output('cytoscape', 'layout'),
536
+ Input('layout-dropdown', 'value')
537
+ )
538
+ def update_layout(selected_layout):
539
+ return {'name': selected_layout}
540
+
541
+ # Reset callback
542
+ @app.callback(
543
+ [
544
+ Output('color-scheme-dropdown', 'value'),
545
+ Output('bus-color-picker', 'value'),
546
+ Output('source-color-picker', 'value'),
547
+ Output('sink-color-picker', 'value'),
548
+ Output('storage-color-picker', 'value'),
549
+ Output('converter-color-picker', 'value'),
550
+ Output('edge-color-picker', 'value'),
551
+ Output('node-size-slider', 'value'),
552
+ Output('font-size-slider', 'value'),
553
+ Output('layout-dropdown', 'value'),
554
+ ],
555
+ [Input('reset-btn', 'n_clicks')]
556
+ )
557
+ def reset_controls(n_clicks):
558
+ if n_clicks and n_clicks > 0:
559
+ return (
560
+ 'Default', # color scheme
561
+ {'hex': '#7F8C8D'}, # bus
562
+ {'hex': '#F1C40F'}, # source
563
+ {'hex': '#F1C40F'}, # sink
564
+ {'hex': '#2980B9'}, # storage
565
+ {'hex': '#D35400'}, # converter
566
+ {'hex': '#808080'}, # edge
567
+ 90, # node size
568
+ 10, # font size
569
+ 'klay', # layout
570
+ )
571
+ return no_update
572
+
573
+ # Export functionality
574
+ app.clientside_callback(
575
+ """
576
+ function(n_clicks) {
577
+ if (n_clicks > 0 && window.cy) {
578
+ var png64 = window.cy.png({scale: 3, full: true});
579
+ var a = document.createElement('a');
580
+ a.href = png64;
581
+ a.download = 'network_visualization.png';
582
+ a.click();
583
+ }
584
+ return 'Export PNG';
585
+ }
586
+ """,
587
+ Output('export-btn', 'children'),
588
+ Input('export-btn', 'n_clicks'),
589
+ )
590
+
591
+ # Start server
592
+ def find_free_port(start_port=8050, end_port=8100):
593
+ for port in range(start_port, end_port):
594
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
595
+ if s.connect_ex(('localhost', port)) != 0:
596
+ return port
597
+ raise Exception('No free port found')
598
+
599
+ port = find_free_port()
600
+ server = make_server('127.0.0.1', port, app.server)
601
+
602
+ # Start server in background thread
603
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
604
+ server_thread.start()
605
+
606
+ print(f'Network visualization started on http://127.0.0.1:{port}/')
607
+
608
+ # Store server reference for cleanup
609
+ app.server_instance = server
610
+ app.port = port
611
+
612
+ return app
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 2.1.4
3
+ Version: 2.1.6
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -18,13 +18,12 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Intended Audience :: Developers
19
19
  Classifier: Intended Audience :: Science/Research
20
20
  Classifier: Topic :: Scientific/Engineering
21
- Classifier: License :: OSI Approved :: MIT License
22
21
  Requires-Python: >=3.10
23
22
  Description-Content-Type: text/markdown
24
23
  License-File: LICENSE
25
24
  Requires-Dist: numpy<3,>=1.21.5
26
25
  Requires-Dist: pandas<3,>=2.0.0
27
- Requires-Dist: linopy<0.6.0,>=0.5.1
26
+ Requires-Dist: linopy<0.5.6,>=0.5.1
28
27
  Requires-Dist: netcdf4<2,>=1.6.1
29
28
  Requires-Dist: PyYAML<7,>=6.0.0
30
29
  Requires-Dist: rich>=13.0.0
@@ -32,6 +31,22 @@ Requires-Dist: tomli>=2.0.1; python_version < "3.11"
32
31
  Requires-Dist: highspy>=1.5.3
33
32
  Requires-Dist: matplotlib<4.0.0,>=3.5.2
34
33
  Requires-Dist: plotly<6.0.0,>=5.15.0
34
+ Provides-Extra: network-viz
35
+ Requires-Dist: dash>=3.0.0; extra == "network-viz"
36
+ Requires-Dist: dash-cytoscape>=1.0.0; extra == "network-viz"
37
+ Requires-Dist: dash-daq>=0.6.0; extra == "network-viz"
38
+ Requires-Dist: networkx>=3.0.0; extra == "network-viz"
39
+ Requires-Dist: werkzeug>=3.0.0; extra == "network-viz"
40
+ Provides-Extra: full
41
+ Requires-Dist: pyvis==0.3.1; extra == "full"
42
+ Requires-Dist: tsam<3.0.0,>=2.3.1; extra == "full"
43
+ Requires-Dist: scipy<2.0.0,>=1.15.1; extra == "full"
44
+ Requires-Dist: gurobipy>=10.0.0; extra == "full"
45
+ Requires-Dist: dash>=3.0.0; extra == "full"
46
+ Requires-Dist: dash-cytoscape>=1.0.0; extra == "full"
47
+ Requires-Dist: dash-daq>=0.6.0; extra == "full"
48
+ Requires-Dist: networkx>=3.0.0; extra == "full"
49
+ Requires-Dist: werkzeug>=3.0.0; extra == "full"
35
50
  Provides-Extra: dev
36
51
  Requires-Dist: pytest>=7.0.0; extra == "dev"
37
52
  Requires-Dist: ruff>=0.9.0; extra == "dev"
@@ -39,12 +54,11 @@ Requires-Dist: pyvis==0.3.1; extra == "dev"
39
54
  Requires-Dist: tsam<3.0.0,>=2.3.1; extra == "dev"
40
55
  Requires-Dist: scipy<2.0.0,>=1.15.1; extra == "dev"
41
56
  Requires-Dist: gurobipy>=10.0.0; extra == "dev"
42
- Provides-Extra: full
43
- Requires-Dist: pyvis==0.3.1; extra == "full"
44
- Requires-Dist: tsam<3.0.0,>=2.3.1; extra == "full"
45
- Requires-Dist: scipy<2.0.0,>=1.15.1; extra == "full"
46
- Requires-Dist: streamlit<2.0.0,>=1.44.0; extra == "full"
47
- Requires-Dist: gurobipy>=10.0.0; extra == "full"
57
+ Requires-Dist: dash>=3.0.0; extra == "dev"
58
+ Requires-Dist: dash-cytoscape>=1.0.0; extra == "dev"
59
+ Requires-Dist: dash-daq>=0.6.0; extra == "dev"
60
+ Requires-Dist: networkx>=3.0.0; extra == "dev"
61
+ Requires-Dist: werkzeug>=3.0.0; extra == "dev"
48
62
  Provides-Extra: docs
49
63
  Requires-Dist: mkdocs-material<10,>=9.0.0; extra == "docs"
50
64
  Requires-Dist: mkdocstrings-python>=1.0.0; extra == "docs"
@@ -19,26 +19,27 @@ docs/user-guide/Mathematical Notation/Storage.md,sha256=PeNzk77i-81VX8I5r3zen3ka
19
19
  docs/user-guide/Mathematical Notation/index.md,sha256=gkglBsoARhgvppXN9PgdJF33sCSnwGY7MtKDtCC32bE,1255
20
20
  docs/user-guide/Mathematical Notation/others.md,sha256=wOUsfspAoSNTMlTNipeQ8ohoVVX2S-eI3dmlzqqrbR8,47
21
21
  flixopt/__init__.py,sha256=F49OK5QLUnMGmsaKQ-G0dXsVuKr9Ow_pjM4KMSNZ918,614
22
- flixopt/aggregation.py,sha256=UaAYh34C4XhDgiSs4lm31XEMLr4YO5BzLKUAx4NQuyI,17002
22
+ flixopt/aggregation.py,sha256=xgQu2U5YEbtdDAEMjWiuP9uo_KjhzC95VNmY4ZcSX3I,16939
23
23
  flixopt/calculation.py,sha256=1Hs9dc6eqdJoHT6Dd3NlwdRORFO2vKdKx38o95FPxJE,20016
24
24
  flixopt/commons.py,sha256=ZNlUN1z-h9OGHPo-s-n5OLlJaoPZKVGcAdRyGKpMk4M,1256
25
- flixopt/components.py,sha256=7vI0IzWBLj81IErPllWHbMMqrrPlplYOPcH3aHvDbeY,29124
25
+ flixopt/components.py,sha256=4M1WR4JasMXiOUo4JpJfgPuMY5H0RbazhTAXAf8IWI0,33177
26
26
  flixopt/config.py,sha256=Kt8QYk7hX5qHcQUtfgjM862C6SQr4K2lDvtk_LLER8Y,9085
27
27
  flixopt/config.yaml,sha256=imzAnnhcJhIfKNTTXFB5Td7Pvk5ARn5j720k-oGGRug,392
28
28
  flixopt/core.py,sha256=dBdAzA3khIe64aVGpPj3G5PzOr7RdGdSymnV3xWgaR8,38083
29
29
  flixopt/effects.py,sha256=TKpUfUo0wbX5y5HS9U8HcDNOiygg0R7k9V3TM0G6uL4,16650
30
30
  flixopt/elements.py,sha256=9P2uB3twwADf48Gx1xCluE-ZJCkzw0X7tYrtKEQOjk8,26932
31
31
  flixopt/features.py,sha256=sEtdj7BpaYS9a0XdhRUtdDFXWLdaGABRXdi5JOoLPb0,43919
32
- flixopt/flow_system.py,sha256=4D2u2ucLig0GbC7ksCuWXuZPZdkgDzPafv-GhjAxRyk,17479
32
+ flixopt/flow_system.py,sha256=x7wgbBhftIarB7w1lNzSkDY6b1IYzg6O7rexsAym1es,19606
33
33
  flixopt/interface.py,sha256=uXf6Z29OfHpIRsS1-oZZ6SSuy8FLe13FjtqzHPqzzQE,12088
34
34
  flixopt/io.py,sha256=2QKdtu2-mkzSGBIqHtUcF9UaG32nq9qcIRxZghf1hLw,11284
35
35
  flixopt/linear_converters.py,sha256=ej5V_ML_3m1k9HbDnuey6pHEpQtguYkxBXHxWyE9sq0,10936
36
+ flixopt/network_app.py,sha256=Xyb4iLm13BFlIPhkL6RE2x8n5jgAbXMiN8mTMr-_-nM,23459
36
37
  flixopt/plotting.py,sha256=wUwBSQxxwy1uui-mi2hgj6h__O6EvxCnocIbX0ewpMk,54111
37
38
  flixopt/results.py,sha256=GKSZmz0GCuJwspTC8Ot6MOKinvy_mhnDXCafb_f7uVY,35161
38
39
  flixopt/solvers.py,sha256=k1bSoiXec3asWED70-erXkgtpn2C8KRBfSZj0FLviSM,2436
39
40
  flixopt/structure.py,sha256=QS0IFBHzdEMKYTgd6uosudhoDD4X0JcdF7LlS-XRacs,26295
40
41
  flixopt/utils.py,sha256=f-_vFDvvG27-c_VMpzkv3lb79Yny4rvoSmemushbzhU,1687
41
- flixopt-2.1.4.dist-info/licenses/LICENSE,sha256=HKsZnbrM_3Rvnr_u9cWSG90cBsj5_slaqI_z_qcxnGI,1118
42
+ flixopt-2.1.6.dist-info/licenses/LICENSE,sha256=HKsZnbrM_3Rvnr_u9cWSG90cBsj5_slaqI_z_qcxnGI,1118
42
43
  pics/architecture_flixOpt-pre2.0.0.png,sha256=9RWSA3vys588aadr2437zor-_-xBTQNQ0bAf8xGcu5g,70605
43
44
  pics/architecture_flixOpt.png,sha256=KjN1bJwESbkHmTW7UsJ7dZyiKZlTO7Dx20dg8KlR1HU,260219
44
45
  pics/flixOpt_plotting.jpg,sha256=zn7ZPAtXm5eRTxtOj86e4-PPhHpCar1jqGh7vMBgQGY,518862
@@ -47,7 +48,7 @@ pics/pics.pptx,sha256=ImWeGGvjtWJ6BGruipsnZYmWtHj5sWdbw1NSFePbkC8,683344
47
48
  scripts/extract_release_notes.py,sha256=0v8B9c6VXz55PJ0I08W7FdxGp7jEY5NOkrqMY6MNdYU,1249
48
49
  scripts/gen_ref_pages.py,sha256=AYRtXyz78x5I_Hn0oRtGVbTxgLLj2QNyRX6vWRefPjc,1960
49
50
  tests/ressources/Zeitreihen2020.csv,sha256=kbsDTKZS0iUsNZAS7m3DohzZI_OHHWe44s3GwLvcTLw,1918412
50
- flixopt-2.1.4.dist-info/METADATA,sha256=48o6Nah3Lr1g2NSvKWQmJbAWpMgrxMEYjTMG_smnYIU,7343
51
- flixopt-2.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
- flixopt-2.1.4.dist-info/top_level.txt,sha256=DEuo4R1z7GmEp5R3pjbQEJbaPRjKHFvNX2ceiBnVOL0,32
53
- flixopt-2.1.4.dist-info/RECORD,,
51
+ flixopt-2.1.6.dist-info/METADATA,sha256=drw11yScM5qaROW3pGcs_6S55msL0DiC3uzzppHDwpQ,8019
52
+ flixopt-2.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ flixopt-2.1.6.dist-info/top_level.txt,sha256=DEuo4R1z7GmEp5R3pjbQEJbaPRjKHFvNX2ceiBnVOL0,32
54
+ flixopt-2.1.6.dist-info/RECORD,,