flixopt 2.2.0b0__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (63) hide show
  1. flixopt/__init__.py +35 -1
  2. flixopt/aggregation.py +60 -81
  3. flixopt/calculation.py +381 -196
  4. flixopt/components.py +1022 -359
  5. flixopt/config.py +553 -191
  6. flixopt/core.py +475 -1315
  7. flixopt/effects.py +477 -214
  8. flixopt/elements.py +591 -344
  9. flixopt/features.py +403 -957
  10. flixopt/flow_system.py +781 -293
  11. flixopt/interface.py +1159 -189
  12. flixopt/io.py +50 -55
  13. flixopt/linear_converters.py +384 -92
  14. flixopt/modeling.py +759 -0
  15. flixopt/network_app.py +789 -0
  16. flixopt/plotting.py +273 -135
  17. flixopt/results.py +639 -383
  18. flixopt/solvers.py +25 -21
  19. flixopt/structure.py +928 -442
  20. flixopt/utils.py +34 -5
  21. flixopt-3.0.0.dist-info/METADATA +209 -0
  22. flixopt-3.0.0.dist-info/RECORD +26 -0
  23. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -49
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/release-notes/_template.txt +0 -32
  37. docs/release-notes/index.md +0 -7
  38. docs/release-notes/v2.0.0.md +0 -93
  39. docs/release-notes/v2.0.1.md +0 -12
  40. docs/release-notes/v2.1.0.md +0 -31
  41. docs/release-notes/v2.2.0.md +0 -55
  42. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  43. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  44. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  45. docs/user-guide/Mathematical Notation/Investment.md +0 -115
  46. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  47. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  48. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  49. docs/user-guide/Mathematical Notation/index.md +0 -22
  50. docs/user-guide/Mathematical Notation/others.md +0 -3
  51. docs/user-guide/index.md +0 -124
  52. flixopt/config.yaml +0 -10
  53. flixopt-2.2.0b0.dist-info/METADATA +0 -146
  54. flixopt-2.2.0b0.dist-info/RECORD +0 -59
  55. flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
  56. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  57. pics/architecture_flixOpt.png +0 -0
  58. pics/flixOpt_plotting.jpg +0 -0
  59. pics/flixopt-icon.svg +0 -1
  60. pics/pics.pptx +0 -0
  61. scripts/gen_ref_pages.py +0 -54
  62. tests/ressources/Zeitreihen2020.csv +0 -35137
  63. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/network_app.py ADDED
@@ -0,0 +1,789 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import socket
5
+ import threading
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ try:
9
+ import dash_cytoscape as cyto
10
+ import dash_daq as daq
11
+ import networkx as nx
12
+ from dash import Dash, Input, Output, State, callback_context, dcc, html, no_update
13
+ from werkzeug.serving import make_server
14
+
15
+ DASH_CYTOSCAPE_AVAILABLE = True
16
+ VISUALIZATION_ERROR = None
17
+ except ImportError as e:
18
+ DASH_CYTOSCAPE_AVAILABLE = False
19
+ VISUALIZATION_ERROR = str(e)
20
+
21
+ from .components import LinearConverter, Sink, Source, SourceAndSink, Storage
22
+ from .elements import Bus
23
+
24
+ if TYPE_CHECKING:
25
+ from .flow_system import FlowSystem
26
+
27
+ logger = logging.getLogger('flixopt')
28
+
29
+
30
+ # Configuration class for better organization
31
+ class VisualizationConfig:
32
+ """Configuration constants for the visualization"""
33
+
34
+ DEFAULT_COLORS = {
35
+ 'Bus': '#7F8C8D',
36
+ 'Source': '#F1C40F',
37
+ 'Sink': '#F1C40F',
38
+ 'Storage': '#2980B9',
39
+ 'Converter': '#D35400',
40
+ 'Other': '#27AE60',
41
+ }
42
+
43
+ COLOR_PRESETS = {
44
+ 'Default': DEFAULT_COLORS,
45
+ 'Vibrant': {
46
+ 'Bus': '#FF6B6B',
47
+ 'Source': '#4ECDC4',
48
+ 'Sink': '#45B7D1',
49
+ 'Storage': '#96CEB4',
50
+ 'Converter': '#FFEAA7',
51
+ 'Other': '#DDA0DD',
52
+ },
53
+ 'Dark': {
54
+ 'Bus': '#2C3E50',
55
+ 'Source': '#34495E',
56
+ 'Sink': '#7F8C8D',
57
+ 'Storage': '#95A5A6',
58
+ 'Converter': '#BDC3C7',
59
+ 'Other': '#ECF0F1',
60
+ },
61
+ 'Pastel': {
62
+ 'Bus': '#FFB3BA',
63
+ 'Source': '#BAFFC9',
64
+ 'Sink': '#BAE1FF',
65
+ 'Storage': '#FFFFBA',
66
+ 'Converter': '#FFDFBA',
67
+ 'Other': '#E0BBE4',
68
+ },
69
+ }
70
+
71
+ DEFAULT_STYLESHEET = [
72
+ {
73
+ 'selector': 'node',
74
+ 'style': {
75
+ 'content': 'data(label)',
76
+ 'background-color': 'data(color)',
77
+ 'font-size': 10,
78
+ 'color': 'white',
79
+ 'text-valign': 'center',
80
+ 'text-halign': 'center',
81
+ 'width': '90px',
82
+ 'height': '70px',
83
+ 'shape': 'data(shape)',
84
+ 'text-outline-color': 'black',
85
+ 'text-outline-width': 0.5,
86
+ },
87
+ },
88
+ {
89
+ 'selector': '[shape = "custom-source"]',
90
+ 'style': {
91
+ 'shape': 'polygon',
92
+ 'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',
93
+ },
94
+ },
95
+ {
96
+ 'selector': '[shape = "custom-sink"]',
97
+ 'style': {
98
+ 'shape': 'polygon',
99
+ 'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',
100
+ },
101
+ },
102
+ {
103
+ 'selector': 'edge',
104
+ 'style': {
105
+ 'curve-style': 'straight',
106
+ 'width': 2,
107
+ 'line-color': 'gray',
108
+ 'target-arrow-color': 'gray',
109
+ 'target-arrow-shape': 'triangle',
110
+ 'arrow-scale': 2,
111
+ },
112
+ },
113
+ ]
114
+
115
+
116
+ def flow_graph(flow_system: FlowSystem) -> nx.DiGraph:
117
+ """Convert FlowSystem to NetworkX graph - simplified and more robust"""
118
+ if not DASH_CYTOSCAPE_AVAILABLE:
119
+ raise ImportError(
120
+ 'Network visualization requires optional dependencies. '
121
+ 'Install with: pip install flixopt[viz] or '
122
+ 'pip install dash dash-cytoscape networkx werkzeug. '
123
+ f'Original error: {VISUALIZATION_ERROR}'
124
+ )
125
+
126
+ nodes = list(flow_system.components.values()) + list(flow_system.buses.values())
127
+ edges = list(flow_system.flows.values())
128
+
129
+ def get_element_type(element):
130
+ """Determine element type for coloring"""
131
+ if isinstance(element, Bus):
132
+ return 'Bus'
133
+ elif isinstance(element, Source):
134
+ return 'Source'
135
+ elif isinstance(element, (Sink, SourceAndSink)):
136
+ return 'Sink'
137
+ elif isinstance(element, Storage):
138
+ return 'Storage'
139
+ elif isinstance(element, LinearConverter):
140
+ return 'Converter'
141
+ else:
142
+ return 'Other'
143
+
144
+ def get_shape(element):
145
+ """Determine node shape"""
146
+ if isinstance(element, Bus):
147
+ return 'ellipse'
148
+ elif isinstance(element, Source):
149
+ return 'custom-source'
150
+ elif isinstance(element, (Sink, SourceAndSink)):
151
+ return 'custom-sink'
152
+ else:
153
+ return 'rectangle'
154
+
155
+ graph = nx.DiGraph()
156
+
157
+ # Add nodes with attributes
158
+ for node in nodes:
159
+ graph.add_node(
160
+ node.label_full,
161
+ color=VisualizationConfig.DEFAULT_COLORS[get_element_type(node)],
162
+ shape=get_shape(node),
163
+ element_type=get_element_type(node),
164
+ parameters=str(node),
165
+ )
166
+
167
+ # Add edges
168
+ for edge in edges:
169
+ try:
170
+ graph.add_edge(
171
+ u_of_edge=edge.bus if edge.is_input_in_component else edge.component,
172
+ v_of_edge=edge.component if edge.is_input_in_component else edge.bus,
173
+ label=edge.label_full,
174
+ parameters=edge.__str__().replace(')', '\n)'),
175
+ )
176
+ except Exception as e:
177
+ logger.error(f'Failed to add edge {edge}: {e}')
178
+
179
+ return graph
180
+
181
+
182
+ def make_cytoscape_elements(graph: nx.DiGraph) -> list[dict[str, Any]]:
183
+ """Convert NetworkX graph to Cytoscape elements"""
184
+ elements = []
185
+
186
+ # Add nodes
187
+ for node_id in graph.nodes():
188
+ node_data = graph.nodes[node_id]
189
+ elements.append(
190
+ {
191
+ 'data': {
192
+ 'id': node_id,
193
+ 'label': node_id,
194
+ 'color': node_data.get('color', '#7F8C8D'),
195
+ 'shape': node_data.get('shape', 'rectangle'),
196
+ 'element_type': node_data.get('element_type', 'Other'),
197
+ 'parameters': node_data.get('parameters', ''),
198
+ }
199
+ }
200
+ )
201
+
202
+ # Add edges
203
+ for u, v in graph.edges():
204
+ edge_data = graph.edges[u, v]
205
+ elements.append(
206
+ {
207
+ 'data': {
208
+ 'source': u,
209
+ 'target': v,
210
+ 'id': f'{u}-{v}',
211
+ 'label': edge_data.get('label', ''),
212
+ 'parameters': edge_data.get('parameters', ''),
213
+ }
214
+ }
215
+ )
216
+
217
+ return elements
218
+
219
+
220
+ def create_color_picker_input(label: str, input_id: str, default_color: str):
221
+ """Create a compact color picker with DAQ ColorPicker"""
222
+ return html.Div(
223
+ [
224
+ html.Label(
225
+ label, style={'color': 'white', 'font-size': '12px', 'margin-bottom': '5px', 'display': 'block'}
226
+ ),
227
+ daq.ColorPicker(
228
+ id=input_id,
229
+ label='',
230
+ value={'hex': default_color},
231
+ size=200,
232
+ theme={'dark': True},
233
+ style={'margin-bottom': '10px'},
234
+ ),
235
+ ]
236
+ )
237
+
238
+
239
+ def create_style_section(title: str, children: list):
240
+ """Create a collapsible section for organizing controls"""
241
+ return html.Div(
242
+ [
243
+ html.H4(
244
+ title,
245
+ style={
246
+ 'color': 'white',
247
+ 'margin-bottom': '10px',
248
+ 'border-bottom': '2px solid #3498DB',
249
+ 'padding-bottom': '5px',
250
+ },
251
+ ),
252
+ html.Div(children, style={'margin-bottom': '20px'}),
253
+ ]
254
+ )
255
+
256
+
257
+ def create_sidebar():
258
+ """Create the main sidebar with improved organization"""
259
+ return html.Div(
260
+ [
261
+ html.Div(
262
+ [
263
+ html.H3(
264
+ 'Style Controls',
265
+ style={
266
+ 'color': 'white',
267
+ 'margin-bottom': '20px',
268
+ 'text-align': 'center',
269
+ 'border-bottom': '3px solid #9B59B6',
270
+ 'padding-bottom': '10px',
271
+ },
272
+ ),
273
+ # Layout Section
274
+ create_style_section(
275
+ 'Layout',
276
+ [
277
+ dcc.Dropdown(
278
+ id='layout-dropdown',
279
+ options=[
280
+ {'label': 'Klay (horizontal)', 'value': 'klay'},
281
+ {'label': 'Dagre (vertical)', 'value': 'dagre'},
282
+ {'label': 'Breadthfirst', 'value': 'breadthfirst'},
283
+ {'label': 'Cose (force-directed)', 'value': 'cose'},
284
+ {'label': 'Grid', 'value': 'grid'},
285
+ {'label': 'Circle', 'value': 'circle'},
286
+ ],
287
+ value='klay',
288
+ clearable=False,
289
+ style={'width': '100%'},
290
+ ),
291
+ ],
292
+ ),
293
+ # Color Scheme Section
294
+ create_style_section(
295
+ 'Color Scheme',
296
+ [
297
+ dcc.Dropdown(
298
+ id='color-scheme-dropdown',
299
+ options=[{'label': k, 'value': k} for k in VisualizationConfig.COLOR_PRESETS.keys()],
300
+ value='Default',
301
+ style={'width': '100%', 'margin-bottom': '10px'},
302
+ ),
303
+ ],
304
+ ),
305
+ # Color Pickers Section
306
+ create_style_section(
307
+ 'Custom Colors',
308
+ [
309
+ create_color_picker_input('Bus', 'bus-color-picker', '#7F8C8D'),
310
+ create_color_picker_input('Source', 'source-color-picker', '#F1C40F'),
311
+ create_color_picker_input('Sink', 'sink-color-picker', '#F1C40F'),
312
+ create_color_picker_input('Storage', 'storage-color-picker', '#2980B9'),
313
+ create_color_picker_input('Converter', 'converter-color-picker', '#D35400'),
314
+ create_color_picker_input('Edge', 'edge-color-picker', '#808080'),
315
+ ],
316
+ ),
317
+ # Node Settings
318
+ create_style_section(
319
+ 'Node Settings',
320
+ [
321
+ html.Label('Size', style={'color': 'white', 'font-size': '12px'}),
322
+ dcc.Slider(
323
+ id='node-size-slider',
324
+ min=50,
325
+ max=150,
326
+ step=10,
327
+ value=90,
328
+ marks={
329
+ i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
330
+ for i in range(50, 151, 25)
331
+ },
332
+ tooltip={'placement': 'bottom', 'always_visible': True},
333
+ ),
334
+ html.Br(),
335
+ html.Label('Font Size', style={'color': 'white', 'font-size': '12px'}),
336
+ dcc.Slider(
337
+ id='font-size-slider',
338
+ min=8,
339
+ max=20,
340
+ step=1,
341
+ value=10,
342
+ marks={
343
+ i: {'label': str(i), 'style': {'color': 'white', 'font-size': '10px'}}
344
+ for i in range(8, 21, 2)
345
+ },
346
+ tooltip={'placement': 'bottom', 'always_visible': True},
347
+ ),
348
+ ],
349
+ ),
350
+ # Reset Button
351
+ html.Div(
352
+ [
353
+ html.Button(
354
+ 'Reset to Defaults',
355
+ id='reset-btn',
356
+ n_clicks=0,
357
+ style={
358
+ 'width': '100%',
359
+ 'background-color': '#E74C3C',
360
+ 'color': 'white',
361
+ 'border': 'none',
362
+ 'padding': '10px',
363
+ 'border-radius': '5px',
364
+ 'cursor': 'pointer',
365
+ 'margin-top': '20px',
366
+ },
367
+ ),
368
+ ]
369
+ ),
370
+ ],
371
+ id='sidebar-content',
372
+ style={
373
+ 'width': '280px',
374
+ 'height': '100vh',
375
+ 'background-color': '#2C3E50',
376
+ 'padding': '20px',
377
+ 'position': 'fixed',
378
+ 'left': '0',
379
+ 'top': '0',
380
+ 'overflow-y': 'auto',
381
+ 'border-right': '3px solid #34495E',
382
+ 'box-shadow': '2px 0 5px rgba(0,0,0,0.1)',
383
+ 'z-index': '999',
384
+ 'transform': 'translateX(-100%)',
385
+ 'transition': 'transform 0.3s ease',
386
+ },
387
+ )
388
+ ]
389
+ )
390
+
391
+
392
+ def shownetwork(graph: nx.DiGraph):
393
+ """Main function to create and run the network visualization"""
394
+ if not DASH_CYTOSCAPE_AVAILABLE:
395
+ raise ImportError(f'Required packages not available: {VISUALIZATION_ERROR}')
396
+
397
+ app = Dash(__name__, suppress_callback_exceptions=True)
398
+
399
+ # Load extra layouts
400
+ cyto.load_extra_layouts()
401
+
402
+ # Create initial elements
403
+ elements = make_cytoscape_elements(graph)
404
+
405
+ # App Layout
406
+ app.layout = html.Div(
407
+ [
408
+ # Toggle button
409
+ html.Button(
410
+ '☰',
411
+ id='toggle-sidebar',
412
+ n_clicks=0,
413
+ style={
414
+ 'position': 'fixed',
415
+ 'top': '20px',
416
+ 'left': '20px',
417
+ 'z-index': '1000',
418
+ 'background-color': '#3498DB',
419
+ 'color': 'white',
420
+ 'border': 'none',
421
+ 'padding': '10px 15px',
422
+ 'border-radius': '5px',
423
+ 'cursor': 'pointer',
424
+ 'font-size': '18px',
425
+ 'box-shadow': '0 2px 5px rgba(0,0,0,0.3)',
426
+ },
427
+ ),
428
+ # Data storage
429
+ dcc.Store(id='elements-store', data=elements),
430
+ # Sidebar
431
+ create_sidebar(),
432
+ # Main content
433
+ html.Div(
434
+ [
435
+ # Header
436
+ html.Div(
437
+ [
438
+ html.H2(
439
+ 'Network Visualization', style={'color': 'white', 'margin': '0', 'text-align': 'center'}
440
+ ),
441
+ html.Button(
442
+ 'Export PNG',
443
+ id='export-btn',
444
+ n_clicks=0,
445
+ style={
446
+ 'position': 'absolute',
447
+ 'right': '20px',
448
+ 'top': '15px',
449
+ 'background-color': '#27AE60',
450
+ 'color': 'white',
451
+ 'border': 'none',
452
+ 'padding': '10px 20px',
453
+ 'border-radius': '5px',
454
+ 'cursor': 'pointer',
455
+ },
456
+ ),
457
+ ],
458
+ style={
459
+ 'background-color': '#34495E',
460
+ 'padding': '15px 20px',
461
+ 'position': 'relative',
462
+ 'border-bottom': '2px solid #3498DB',
463
+ },
464
+ ),
465
+ # Cytoscape graph
466
+ cyto.Cytoscape(
467
+ id='cytoscape',
468
+ layout={'name': 'klay'},
469
+ style={'width': '100%', 'height': '70vh'},
470
+ elements=elements,
471
+ stylesheet=VisualizationConfig.DEFAULT_STYLESHEET,
472
+ ),
473
+ # Info panel
474
+ html.Div(
475
+ [
476
+ html.H4(
477
+ 'Element Information',
478
+ style={
479
+ 'color': 'white',
480
+ 'margin': '0 0 10px 0',
481
+ 'border-bottom': '2px solid #3498DB',
482
+ 'padding-bottom': '5px',
483
+ },
484
+ ),
485
+ html.Div(
486
+ id='info-panel',
487
+ children=[
488
+ html.P(
489
+ 'Click on a node or edge to see details.',
490
+ style={'color': '#95A5A6', 'font-style': 'italic'},
491
+ )
492
+ ],
493
+ ),
494
+ ],
495
+ style={
496
+ 'background-color': '#2C3E50',
497
+ 'padding': '15px',
498
+ 'height': '25vh',
499
+ 'overflow-y': 'auto',
500
+ 'border-top': '2px solid #34495E',
501
+ },
502
+ ),
503
+ ],
504
+ id='main-content',
505
+ style={
506
+ 'margin-left': '0',
507
+ 'background-color': '#1A252F',
508
+ 'min-height': '100vh',
509
+ 'transition': 'margin-left 0.3s ease',
510
+ },
511
+ ),
512
+ ]
513
+ )
514
+
515
+ # Callbacks
516
+ @app.callback(
517
+ [Output('sidebar-content', 'style'), Output('main-content', 'style')], [Input('toggle-sidebar', 'n_clicks')]
518
+ )
519
+ def toggle_sidebar(n_clicks):
520
+ is_open = (n_clicks or 0) % 2 == 1
521
+ sidebar_transform = 'translateX(0)' if is_open else 'translateX(-100%)'
522
+ main_margin = '280px' if is_open else '0'
523
+
524
+ sidebar_style = {
525
+ 'width': '280px',
526
+ 'height': '100vh',
527
+ 'background-color': '#2C3E50',
528
+ 'padding': '20px',
529
+ 'position': 'fixed',
530
+ 'left': '0',
531
+ 'top': '0',
532
+ 'overflow-y': 'auto',
533
+ 'border-right': '3px solid #34495E',
534
+ 'box-shadow': '2px 0 5px rgba(0,0,0,0.1)',
535
+ 'z-index': '999',
536
+ 'transform': sidebar_transform,
537
+ 'transition': 'transform 0.3s ease',
538
+ }
539
+
540
+ main_style = {
541
+ 'margin-left': main_margin,
542
+ 'background-color': '#1A252F',
543
+ 'min-height': '100vh',
544
+ 'transition': 'margin-left 0.3s ease',
545
+ }
546
+
547
+ return sidebar_style, main_style
548
+
549
+ # Combined callback to handle both color scheme changes and reset
550
+ @app.callback(
551
+ [
552
+ Output('bus-color-picker', 'value'),
553
+ Output('source-color-picker', 'value'),
554
+ Output('sink-color-picker', 'value'),
555
+ Output('storage-color-picker', 'value'),
556
+ Output('converter-color-picker', 'value'),
557
+ ],
558
+ [Input('color-scheme-dropdown', 'value'), Input('reset-btn', 'n_clicks')],
559
+ )
560
+ def update_color_pickers(color_scheme, reset_clicks):
561
+ """Update color pickers when color scheme changes or reset is clicked"""
562
+ ctx = callback_context
563
+
564
+ # Determine which input triggered the callback
565
+ if ctx.triggered:
566
+ trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]
567
+ if trigger_id == 'reset-btn' and reset_clicks and reset_clicks > 0:
568
+ # Reset was clicked, use default colors
569
+ colors = VisualizationConfig.DEFAULT_COLORS
570
+ else:
571
+ # Color scheme changed
572
+ colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme, VisualizationConfig.DEFAULT_COLORS)
573
+ else:
574
+ # Initial load
575
+ colors = VisualizationConfig.COLOR_PRESETS.get(color_scheme, VisualizationConfig.DEFAULT_COLORS)
576
+
577
+ return (
578
+ {'hex': colors['Bus']},
579
+ {'hex': colors['Source']},
580
+ {'hex': colors['Sink']},
581
+ {'hex': colors['Storage']},
582
+ {'hex': colors['Converter']},
583
+ )
584
+
585
+ # Updated main visualization callback - simplified logic
586
+ @app.callback(
587
+ [Output('cytoscape', 'elements'), Output('cytoscape', 'stylesheet')],
588
+ [
589
+ Input('bus-color-picker', 'value'),
590
+ Input('source-color-picker', 'value'),
591
+ Input('sink-color-picker', 'value'),
592
+ Input('storage-color-picker', 'value'),
593
+ Input('converter-color-picker', 'value'),
594
+ Input('edge-color-picker', 'value'),
595
+ Input('node-size-slider', 'value'),
596
+ Input('font-size-slider', 'value'),
597
+ ],
598
+ [State('elements-store', 'data')],
599
+ )
600
+ def update_visualization(
601
+ bus_color,
602
+ source_color,
603
+ sink_color,
604
+ storage_color,
605
+ converter_color,
606
+ edge_color,
607
+ node_size,
608
+ font_size,
609
+ stored_elements,
610
+ ):
611
+ """Update visualization based on current color picker values"""
612
+ if not stored_elements:
613
+ return no_update, no_update
614
+
615
+ # Use colors from pickers (which are now synced with scheme selection)
616
+ default_colors = VisualizationConfig.DEFAULT_COLORS
617
+ colors = {
618
+ 'Bus': bus_color.get('hex') if bus_color else default_colors['Bus'],
619
+ 'Source': source_color.get('hex') if source_color else default_colors['Source'],
620
+ 'Sink': sink_color.get('hex') if sink_color else default_colors['Sink'],
621
+ 'Storage': storage_color.get('hex') if storage_color else default_colors['Storage'],
622
+ 'Converter': converter_color.get('hex') if converter_color else default_colors['Converter'],
623
+ 'Other': default_colors['Other'],
624
+ }
625
+
626
+ # Update element colors
627
+ updated_elements = []
628
+ for element in stored_elements:
629
+ if 'data' in element and 'element_type' in element['data']:
630
+ element_copy = element.copy()
631
+ element_copy['data'] = element['data'].copy()
632
+ element_type = element_copy['data']['element_type']
633
+ if element_type in colors:
634
+ element_copy['data']['color'] = colors[element_type]
635
+ updated_elements.append(element_copy)
636
+ else:
637
+ updated_elements.append(element)
638
+
639
+ # Create stylesheet
640
+ edge_color_hex = edge_color.get('hex') if edge_color else 'gray'
641
+ stylesheet = [
642
+ {
643
+ 'selector': 'node',
644
+ 'style': {
645
+ 'content': 'data(label)',
646
+ 'background-color': 'data(color)',
647
+ 'font-size': font_size or 10,
648
+ 'color': 'white',
649
+ 'text-valign': 'center',
650
+ 'text-halign': 'center',
651
+ 'width': f'{node_size or 90}px',
652
+ 'height': f'{int((node_size or 90) * 0.8)}px',
653
+ 'shape': 'data(shape)',
654
+ 'text-outline-color': 'black',
655
+ 'text-outline-width': 0.5,
656
+ },
657
+ },
658
+ {
659
+ 'selector': '[shape = "custom-source"]',
660
+ 'style': {
661
+ 'shape': 'polygon',
662
+ 'shape-polygon-points': '-0.5 0.5, 0.5 0.5, 1 -0.5, -1 -0.5',
663
+ },
664
+ },
665
+ {
666
+ 'selector': '[shape = "custom-sink"]',
667
+ 'style': {
668
+ 'shape': 'polygon',
669
+ 'shape-polygon-points': '-0.5 -0.5, 0.5 -0.5, 1 0.5, -1 0.5',
670
+ },
671
+ },
672
+ {
673
+ 'selector': 'edge',
674
+ 'style': {
675
+ 'curve-style': 'straight',
676
+ 'width': 2,
677
+ 'line-color': edge_color_hex,
678
+ 'target-arrow-color': edge_color_hex,
679
+ 'target-arrow-shape': 'triangle',
680
+ 'arrow-scale': 2,
681
+ },
682
+ },
683
+ ]
684
+
685
+ return updated_elements, stylesheet
686
+
687
+ @app.callback(
688
+ Output('info-panel', 'children'), [Input('cytoscape', 'tapNodeData'), Input('cytoscape', 'tapEdgeData')]
689
+ )
690
+ def display_element_info(node_data, edge_data):
691
+ ctx = callback_context
692
+ if not ctx.triggered:
693
+ return [
694
+ html.P('Click on a node or edge to see details.', style={'color': '#95A5A6', 'font-style': 'italic'})
695
+ ]
696
+
697
+ # Determine what was clicked
698
+ if ctx.triggered[0]['prop_id'] == 'cytoscape.tapNodeData' and node_data:
699
+ return [
700
+ html.H5(
701
+ f'Node: {node_data.get("label", "Unknown")}', style={'color': 'white', 'margin-bottom': '10px'}
702
+ ),
703
+ html.P(f'Type: {node_data.get("element_type", "Unknown")}', style={'color': '#BDC3C7'}),
704
+ html.Pre(
705
+ node_data.get('parameters', 'No parameters'),
706
+ style={'color': '#BDC3C7', 'font-size': '11px', 'white-space': 'pre-wrap'},
707
+ ),
708
+ ]
709
+ elif ctx.triggered[0]['prop_id'] == 'cytoscape.tapEdgeData' and edge_data:
710
+ return [
711
+ html.H5(
712
+ f'Edge: {edge_data.get("label", "Unknown")}', style={'color': 'white', 'margin-bottom': '10px'}
713
+ ),
714
+ html.P(f'{edge_data.get("source", "")} → {edge_data.get("target", "")}', style={'color': '#E67E22'}),
715
+ html.Pre(
716
+ edge_data.get('parameters', 'No parameters'),
717
+ style={'color': '#BDC3C7', 'font-size': '11px', 'white-space': 'pre-wrap'},
718
+ ),
719
+ ]
720
+
721
+ return [html.P('Click on a node or edge to see details.', style={'color': '#95A5A6', 'font-style': 'italic'})]
722
+
723
+ @app.callback(Output('cytoscape', 'layout'), Input('layout-dropdown', 'value'))
724
+ def update_layout(selected_layout):
725
+ return {'name': selected_layout}
726
+
727
+ # Reset callback for non-color-picker controls
728
+ @app.callback(
729
+ [
730
+ Output('color-scheme-dropdown', 'value'),
731
+ Output('edge-color-picker', 'value'),
732
+ Output('node-size-slider', 'value'),
733
+ Output('font-size-slider', 'value'),
734
+ Output('layout-dropdown', 'value'),
735
+ ],
736
+ [Input('reset-btn', 'n_clicks')],
737
+ )
738
+ def reset_controls(n_clicks):
739
+ """Reset all controls to defaults (color pickers handled separately)"""
740
+ if n_clicks and n_clicks > 0:
741
+ return (
742
+ 'Default', # color scheme (will trigger color picker updates)
743
+ {'hex': '#808080'}, # edge color
744
+ 90, # node size
745
+ 10, # font size
746
+ 'klay', # layout
747
+ )
748
+ return no_update
749
+
750
+ # Export functionality
751
+ app.clientside_callback(
752
+ """
753
+ function(n_clicks) {
754
+ if (n_clicks > 0 && window.cy) {
755
+ var png64 = window.cy.png({scale: 3, full: true});
756
+ var a = document.createElement('a');
757
+ a.href = png64;
758
+ a.download = 'network_visualization.png';
759
+ a.click();
760
+ }
761
+ return 'Export PNG';
762
+ }
763
+ """,
764
+ Output('export-btn', 'children'),
765
+ Input('export-btn', 'n_clicks'),
766
+ )
767
+
768
+ # Start server
769
+ def find_free_port(start_port=8050, end_port=8100):
770
+ for port in range(start_port, end_port):
771
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
772
+ if s.connect_ex(('localhost', port)) != 0:
773
+ return port
774
+ raise Exception('No free port found')
775
+
776
+ port = find_free_port()
777
+ server = make_server('127.0.0.1', port, app.server)
778
+
779
+ # Start server in background thread
780
+ server_thread = threading.Thread(target=server.serve_forever, daemon=True)
781
+ server_thread.start()
782
+
783
+ print(f'Network visualization started on http://127.0.0.1:{port}/')
784
+
785
+ # Store server reference for cleanup
786
+ app.server_instance = server
787
+ app.port = port
788
+
789
+ return app