Q2NSViz 0.1.0
Q2NS trace visualizer
Loading...
Searching...
No Matches
window.py
Go to the documentation of this file.
1# -----------------------------------------------------------------------------
2# Q2NSViz - Quantum Network Trace Visualizer
3# Copyright (c) 2026 QuantumInternet.it
4#
5# This program is released under the MIT License - see LICENSE for details.
6# -----------------------------------------------------------------------------
7
8import bisect
9import logging
10import os
11import sys
12from importlib import resources
13
14from PyQt6.QtCore import QSize, Qt, QTimer
15from PyQt6.QtGui import QColor, QFontDatabase, QIcon, QKeySequence, QPainter, QPixmap, QShortcut
16from PyQt6.QtWidgets import (
17 QApplication,
18 QFileDialog,
19 QFrame,
20 QHBoxLayout,
21 QLabel,
22 QMainWindow,
23 QMessageBox,
24 QPushButton,
25 QSizePolicy,
26 QSpacerItem,
27 QVBoxLayout,
28 QWidget,
29)
30
31from ..logic import EventFileParser, SimulationStateManager
32from . import theme as _theme
33from .canvas import NetworkCanvas
34from .panels import ControlPanel, InfoPanel
35from .theme import Theme
36
37logger = logging.getLogger(__name__)
38
39
40def _example_traces_dir() -> str:
41 """Locate the bundled example traces, which ship as package data.
42
43 Empty string when they do not resolve to a real directory, as a non-filesystem
44 import (a zipapp) would.
45 """
46 try:
47 traces = resources.files("q2nsviz") / "example_traces"
48 if traces.is_dir():
49 return str(traces)
50 except (ModuleNotFoundError, OSError):
51 pass
52 return ""
53
54
55def _primary_modifier_label() -> str:
56 """Name of the primary shortcut modifier as it appears on this platform's keyboard.
57
58 Qt maps the ``Ctrl`` of a ``QKeySequence`` onto the Command key on macOS, so the
59 key the user actually presses differs from the sequence's spelling. Shortcut
60 hints are labelled from this helper rather than hard-coding either form.
61 """
62 return "\u2318" if sys.platform == "darwin" else "Ctrl"
63
64
65def _tinted_pixmap(src: QPixmap, color: QColor) -> QPixmap:
66 """Return a copy of *src* recolored to *color*, preserving its alpha silhouette."""
67 out = QPixmap(src.size())
68 out.fill(Qt.GlobalColor.transparent)
69 painter = QPainter(out)
70 painter.drawPixmap(0, 0, src)
71 painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
72 painter.fillRect(out.rect(), color)
73 painter.end()
74 return out
75
76
78 """Owns the simulation clock, playback timer, and all time-navigation logic.
79
80 Decoupled from ``QuantumVisualizerWindow`` so that playback concerns are
81 separated from widget construction. The controller reads
82 ``state_manager.time_array`` and ``state_manager.t_max`` and drives the
83 ``on_update`` callback after every time advance.
84
85 Call ``connect_signals()`` once after construction to wire ``ControlPanel``
86 signals, and ``setup_for_file()`` each time a new trace is loaded.
87
88 @param state_manager Shared ``SimulationStateManager`` instance.
89 @param control_panel ``ControlPanel`` providing slider, speed, and loop widgets.
90 @param on_update Callback invoked (with no arguments) after every time step.
91 @ingroup q2nsviz_app
92 """
93
94 def __init__(self, state_manager: SimulationStateManager, control_panel: ControlPanel, on_update):
95 self.state_manager = state_manager
96 self.control_panel = control_panel
97 self._on_update = on_update
98 self.current_sim_time_ns: int = 0
99 self.is_playing: bool = False
100 self._timer = QTimer()
101 self._timer.setInterval(16)
102 self._timer.timeout.connect(self._tick_tick)
103
105 """Wire all ``ControlPanel`` signals to the appropriate playback methods."""
106 cp = self.control_panel
107 cp.play_clicked.connect(self.startstart)
108 cp.pause_clicked.connect(self.pausepause)
109 cp.step_forward.connect(self.step_forwardstep_forward)
110 cp.step_backward.connect(self.step_backwardstep_backward)
111 cp.time_changed.connect(self._on_time_changed_on_time_changed)
112 cp.speed_changed.connect(self._on_speed_changed_on_speed_changed)
113
114 def setup_for_file(self):
115 """Reset to time zero and configure the slider for a newly loaded file.
116
117 Called immediately after ``SimulationStateManager.load_events()``. Does not
118 invoke the update callback; the caller is responsible for triggering a
119 visualization refresh.
120 """
122 self.is_playing = False
123 self._timer.stop()
124 self.control_panel.set_playing(False)
125 if self.state_manager.time_array:
126 self.control_panel.time_slider.setMaximum(len(self.state_manager.time_array) - 1)
127 self.control_panel.time_slider.blockSignals(True)
128 self.control_panel.time_slider.setValue(0)
129 self.control_panel.time_slider.blockSignals(False)
130
131 def reset(self):
132 """Stop playback, return to t = 0, and restore the slider to its default range."""
133 self.is_playing = False
134 self._timer.stop()
135 self.control_panel.set_playing(False)
136 self.current_sim_time_ns = 0
137 slider = self.control_panel.time_slider
138 slider.blockSignals(True)
139 slider.setValue(0)
140 slider.setMaximum(100)
141 slider.blockSignals(False)
142 self.control_panel.update_time_label(0.0, 0, 0)
143
144 def start(self):
145 """Begin timer-driven playback."""
146 self.is_playing = True
147 self._timer.start()
148
149 def pause(self):
150 """Suspend timer-driven playback."""
151 self.is_playing = False
152 self._timer.stop()
153
154 def step_forward(self):
155 """Advance to the next keyframe and invoke the update callback."""
156 if not self.state_manager.time_array:
157 return
158 ta = self.state_manager.time_array
159 idx = bisect.bisect_right(ta, self.current_sim_time_ns)
160 if idx < len(ta):
161 self.current_sim_time_ns = ta[idx]
162 self._sync_slider()
163 self._on_update()
164
165 def step_backward(self):
166 """Retreat to the previous keyframe and invoke the update callback."""
167 if not self.state_manager.time_array:
168 return
169 ta = self.state_manager.time_array
170 idx = bisect.bisect_left(ta, self.current_sim_time_ns) - 1
171 self.current_sim_time_ns = ta[idx] if idx >= 0 else 0
172 self._sync_slider()
173 self._on_update()
174
175 def jump_to_start(self):
176 """Jump to t = 0 and invoke the update callback."""
177 if not self.state_manager.time_array:
178 return
179 self.current_sim_time_ns = 0
180 self._sync_slider()
181 self._on_update()
182
183 def jump_to_end(self):
184 """Jump to the last event timestamp and invoke the update callback."""
185 if not self.state_manager.time_array:
186 return
187 self.current_sim_time_ns = self.state_manager.t_max
188 self._sync_slider()
189 self._on_update()
190
191 def _on_time_changed(self, value: int):
192 """Handle time-scrubber drag: snap to the keyframe at the given slider index."""
193 if self.state_manager.time_array:
194 value = max(0, min(value, len(self.state_manager.time_array) - 1))
195 self.current_sim_time_ns = self.state_manager.time_array[value]
196 self._on_update()
197
198 def _on_speed_changed(self, value: int):
199 """Recompute and display the speed label from the slider position.
200
201 The mapping is logarithmic: slider range [1, 100] maps to playback
202 multipliers [1x, ~600x] via ``mult = 10 ** (2.778 * (value - 1) / 99)``.
203 At value = 1 the exponent is 0 (1x); at value = 100 it is 2.778 (~600x).
204 """
205 exp = 2.778 * (value - 1) / 99
206 mult = 10**exp
207 if mult >= 10:
208 label = f"{mult:.0f}\u00d7"
209 elif mult >= 2:
210 label = f"{mult:.1f}\u00d7"
211 else:
212 label = f"{mult:.2f}\u00d7"
213 self.control_panel.update_speed_label(label)
214
215 def _sync_slider(self):
216 """Set the time-scrubber position to reflect ``current_sim_time_ns``.
217
218 Blocks slider signals during the update so that ``time_changed`` is
219 not re-emitted and does not cause a recursive update.
220 """
221 if not self.state_manager.time_array:
222 return
223 idx = bisect.bisect_right(self.state_manager.time_array, self.current_sim_time_ns) - 1
224 idx = max(0, min(idx, len(self.state_manager.time_array) - 1))
225 slider = self.control_panel.time_slider
226 slider.blockSignals(True)
227 slider.setValue(idx)
228 slider.blockSignals(False)
229
230 def _tick(self):
231 """Advance playback by one wall-clock frame and invoke the update callback.
232
233 Each 16 ms tick advances ``current_sim_time_ns`` by an amount proportional
234 to the speed-slider multiplier (same log scale as ``_on_speed_changed``).
235 Stops or loops at the final event timestamp depending on the loop-button state.
236 """
237 if not self.is_playing or not self.state_manager.time_array:
238 return
239 speed = self.control_panel.speed_slider.value()
240 exp = 2.778 * (speed - 1) / 99
241 target_ticks = 3600 / (10**exp)
242 advance_ns = max(1, int(self.state_manager.t_max / target_ticks))
243 self.current_sim_time_ns = min(self.current_sim_time_ns + advance_ns, self.state_manager.t_max)
244 if self.current_sim_time_ns >= self.state_manager.t_max:
245 if self.control_panel.loop_btn.isChecked():
246 self.current_sim_time_ns = 0
247 else:
248 self.pausepause()
249 self.control_panel.set_playing(False)
250 self._sync_slider()
251 self._on_update()
252
253
254class QuantumVisualizerWindow(QMainWindow):
255 """Top-level application window.
256
257 Owns the ``SimulationStateManager``, the ``NetworkCanvas``, the
258 ``ControlPanel``, and the ``InfoPanel``. Coordinates playback via a
259 ``PlaybackController`` and propagates time-step changes to all child
260 widgets through ``_update_visualization()``.
261
262 The single source of truth for the current simulation time is
263 ``playback.current_sim_time_ns``; all other widgets derive their
264 displayed state from that value on each ``_update_visualization()`` call.
265 @ingroup q2nsviz_app
266 """
267
268 def __init__(self):
269 super().__init__()
271 self._last_filename: str | None = None
272
273 self.setWindowTitle("Q2NSViz - Quantum Network Trace Visualizer")
274 self.resize(1600, 900)
275 self._center_on_screen()
276 self._setup_ui()
277 # PlaybackController is created after _setup_ui so that control_panel exists.
279 self.playback.connect_signals()
280 self.playback._on_speed_changed(self.control_panel.speed_slider.value())
281 self.setStyleSheet(f"background-color: {Theme.BG_DARK.name()};")
282 self.statusBar().setStyleSheet(f"""
283 QStatusBar {{
284 background-color: {Theme.BG_MEDIUM.name()};
285 color: {Theme.TEXT_SECONDARY.name()};
286 border-top: 1px solid {Theme.BORDER.name()};
287 padding: 4px 8px;
288 font-size: 12px;
289 }}
290 """)
291 self.statusBar().showMessage("Ready \u2014 Load a simulation file to begin")
292 self._setup_shortcuts()
293
294 def _center_on_screen(self):
295 screen = QApplication.primaryScreen()
296 if screen is None:
297 return
298 geo = screen.availableGeometry()
299 x = (geo.width() - self.width()) // 2
300 y = (geo.height() - self.height()) // 2
301 self.move(geo.left() + x, geo.top() + y)
302
303 def _setup_ui(self):
304 central = QWidget()
305 self.setCentralWidget(central)
306 main_layout = QVBoxLayout(central)
307 main_layout.setContentsMargins(10, 10, 10, 10)
308 main_layout.setSpacing(10)
309 top_bar = self._create_top_bar()
310 self._top_bar = top_bar
311 main_layout.addWidget(top_bar)
312
313 content = QHBoxLayout()
314 content.setSpacing(10)
316 content.addWidget(self.canvas, 2)
317
318 right_panel = QVBoxLayout()
319 right_panel.setSpacing(10)
321 right_panel.addWidget(self.info_panel, 1)
322 content.addLayout(right_panel, 1)
323 main_layout.addLayout(content, 1)
324
326 main_layout.addWidget(self.control_panel)
327
328 def _build_topbar_stylesheet(self) -> str:
329 return f"""
330 QFrame {{
331 background-color: {Theme.BG_TOPBAR.name()};
332 border-radius: 12px;
333 padding: 12px;
334 }}
335 QPushButton {{
336 background-color: {Theme.PRIMARY.name()};
337 color: white;
338 border: none;
339 padding: 10px 20px;
340 border-radius: 6px;
341 font-weight: 500;
342 font-size: 13px;
343 }}
344 QPushButton:hover {{
345 background-color: {Theme.PRIMARY_HOVER.name()};
346 }}
347 QPushButton:pressed {{
348 background-color: {Theme.PRIMARY_DARK.name()};
349 }}
350 QPushButton#secondary {{
351 background-color: transparent;
352 color: {Theme.TEXT_PRIMARY.name()};
353 border: 1px solid {Theme.BORDER.name()};
354 }}
355 QPushButton#secondary:hover {{
356 background-color: {Theme.BG_LIGHT.name()};
357 border-color: {Theme.PRIMARY_HOVER.name()};
358 }}
359 QPushButton#secondary:checked {{
360 background-color: {Theme.PRIMARY.name()};
361 border-color: {Theme.PRIMARY.name()};
362 color: white;
363 }}
364 QLabel {{
365 color: {Theme.TEXT_PRIMARY.name()};
366 font-size: 18px;
367 font-weight: 600;
368 }}
369 """
370
371 def _create_top_bar(self) -> QWidget:
372 bar = QFrame()
373 bar.setStyleSheet(self._build_topbar_stylesheet())
374 layout = QHBoxLayout(bar)
375 layout.setContentsMargins(8, 6, 8, 6)
376
377 # --- Left side: logo + title ---
378 _assets_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
379 logo_path = os.path.join(_assets_dir, "logo-qnattynet.png")
380 self._logo_label = QLabel()
381 self._logo_label.setStyleSheet("background: transparent; padding: 3px 6px;")
382 pix = QPixmap(logo_path)
383 self._logo_pixmap = (
384 pix.scaled(
385 QSize(500, 36),
386 aspectRatioMode=Qt.AspectRatioMode.KeepAspectRatio,
387 transformMode=Qt.TransformationMode.SmoothTransformation,
388 )
389 if not pix.isNull()
390 else QPixmap()
391 )
392 self._apply_logo()
393 layout.addWidget(self._logo_label, 0, Qt.AlignmentFlag.AlignVCenter)
394 layout.addSpacing(12)
395
396 self._title_label = QLabel("Q2NSViz \u2014 Quantum Network Trace Visualizer")
397 self._title_label.setStyleSheet(
398 f"color: {Theme.TEXT_PRIMARY.name()}; font-size: 16px; font-weight: 600; background: transparent;"
399 )
400 layout.addWidget(self._title_label)
401 layout.addSpacing(16)
402
403 layout.addStretch()
404
405 # --- Right side ---
406 self._dark_btn = QPushButton("Dark")
407 self._dark_btn.setObjectName("secondary")
408 self._dark_btn.setToolTip("Switch to dark / light mode")
409 self._dark_btn.clicked.connect(self._toggle_dark_toggle_dark)
410 layout.addWidget(self._dark_btn)
411
412 self._legend_btn = QPushButton("Legend")
413 self._legend_btn.setObjectName("secondary")
414 self._legend_btn.setToolTip("Toggle legend (L)")
415 self._legend_btn.setCheckable(True)
416 self._legend_btn.setChecked(True)
417 self._legend_btn.clicked.connect(self._toggle_legend_toggle_legend)
418 layout.addWidget(self._legend_btn)
419
420 help_btn = QPushButton("Shortcuts")
421 help_btn.setObjectName("secondary")
422 help_btn.setToolTip("Keyboard shortcuts")
423 help_btn.clicked.connect(self._show_shortcuts_show_shortcuts)
424 layout.addWidget(help_btn)
425
426 layout.addItem(QSpacerItem(20, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum))
427
428 reload_btn = QPushButton("\u21ba\u00a0Reload")
429 reload_btn.setObjectName("secondary")
430 reload_btn.setToolTip("Reload the last opened trace file")
431 reload_btn.clicked.connect(self._reload_file_reload_file)
432 layout.addWidget(reload_btn)
433
434 reset_btn = QPushButton("\u2715\u00a0Reset")
435 reset_btn.setObjectName("secondary")
436 reset_btn.setToolTip("Reset simulation to the start")
437 reset_btn.clicked.connect(self._reset_simulation_reset_simulation)
438 layout.addWidget(reset_btn)
439
440 export_btn = QPushButton("Export\u2026")
441 export_btn.setObjectName("secondary")
442 export_btn.setToolTip("Save the current canvas as an image")
443 export_btn.clicked.connect(self._export_canvas_export_canvas)
444 layout.addWidget(export_btn)
445
446 layout.addItem(QSpacerItem(20, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum))
447
448 load_btn = QPushButton("Load Simulation")
449 load_btn.setToolTip(f"Open a trace file ({_primary_modifier_label()}+O)")
450 load_btn.clicked.connect(self._load_file_load_file)
451 layout.addWidget(load_btn)
452
453 return bar
454
455 def _setup_shortcuts(self):
456 QShortcut(QKeySequence("Space"), self).activated.connect(self.control_panel.play_btn.click)
457 QShortcut(QKeySequence("Right"), self).activated.connect(self.playback.step_forward)
458 QShortcut(QKeySequence("Left"), self).activated.connect(self.playback.step_backward)
459 QShortcut(QKeySequence("Ctrl+O"), self).activated.connect(self._load_file_load_file)
460 QShortcut(QKeySequence("Home"), self).activated.connect(self.playback.jump_to_start)
461 QShortcut(QKeySequence("End"), self).activated.connect(self.playback.jump_to_end)
462 QShortcut(QKeySequence("L"), self).activated.connect(self._toggle_legend_toggle_legend)
463 QShortcut(QKeySequence("N"), self).activated.connect(self._toggle_node_labels_toggle_node_labels)
464
465 def _load_file(self):
466 filename, _ = QFileDialog.getOpenFileName(
467 self, "Open Simulation File", _example_traces_dir(), "JSON Files (*.json *.ndjson);;All Files (*)"
468 )
469 if filename:
470 self.load_file(filename)
471
472 def load_file(self, filename: str):
473 """Load a Q2NS simulation trace from *filename* and initialize playback.
474
475 Calls ``EventFileParser.load_from_file()``, resets the state manager,
476 and repaints all child widgets at simulation time zero. Any parse errors are
477 counted and displayed in the status bar without aborting the load.
478
479 @param filename Absolute path to the trace file.
480 @warning Displays a ``QMessageBox.critical`` dialog on
481 ``OSError`` or any unexpected exception.
482 """
483 self._last_filename = filename
484 try:
485 events, parse_errors = EventFileParser.load_from_file(filename)
486 self.state_manager.load_events(events)
487 self.playback.setup_for_file()
488 self.canvas.reset()
490 status = f"Loaded: {os.path.basename(filename)} — {len(events)} events"
491 if parse_errors:
492 status += f" | {len(parse_errors)} line(s) skipped (parse errors)"
493 logger.warning("Parse errors in %s: %s", filename, parse_errors)
494 self.statusBar().showMessage(status)
495 except Exception as exc:
496 QMessageBox.critical(self, "Load Error", f"Failed to load file:\n{exc}")
497 logger.exception("Failed to load file: %s", filename)
498
499 def _reset_simulation(self):
500 """Stop playback, clear all loaded state, and return the UI to its initial state."""
501 self.playback.reset()
502 self.state_manager.reset()
503 self.canvas.reset()
504 if self.info_panel.chart_canvas:
505 self.info_panel.chart_canvas.clear()
506 self.info_panel.update_log([], 0)
507 self.info_panel.entangle_table.setRowCount(0)
508 self.info_panel.stats_table.setRowCount(0)
509 self.statusBar().showMessage("Ready \u2014 Load a simulation file to begin")
510
511 def _update_visualization(self):
512 """Query the replay engine once and dispatch the state to all child widgets.
513
514 Called after any change to ``current_sim_time_ns``. Performs the
515 single per-frame ``snapshot_at()`` query and pushes the resulting
516 ``Snapshot`` to the network canvas and the information panel; derives
517 the display step index via binary search for the status bar readout.
518
519 Safe to call on an empty trace: every widget then renders its empty state.
520 """
521 t_ns = self.playback.current_sim_time_ns
522 snap = self.state_manager.snapshot_at(t_ns)
523 self.canvas.set_snapshot(snap)
524 t_us = t_ns / 1000
525 ta = self.state_manager.time_array
526 display_step = bisect.bisect_right(ta, t_ns)
527 total_steps = len(ta)
528 self.control_panel.update_time_label(t_us, display_step, total_steps)
529 self.info_panel.update_log(self.state_manager.log_events, self.state_manager.log_count_at(t_ns))
530 self.info_panel.update_entanglement(snap.entangled_states)
531 live_qubits = len(snap.live_qubit_labels)
532 self.info_panel.update_stats(
533 len(snap.nodes),
534 len(self.state_manager.events),
535 self.state_manager.t_max / 1000 if self.state_manager.t_max else 0,
536 qubits=live_qubits,
537 entangled=len(snap.entangled_states),
538 measured=len(snap.measured_qubits),
539 lost=len(snap.lost_qubits),
540 discarded=len(snap.discarded_qubits),
541 )
542 self.info_panel.update_charts(self.state_manager, snap)
543
544 self.statusBar().showMessage(
545 f"Time: {t_us:.3f}\u202f\u03bcs | Step: {display_step}/{total_steps} | "
546 f"Qubits: {live_qubits} | Entangled States: {len(snap.entangled_states)}"
547 )
548
549 def _toggle_dark(self):
550 """Switch between light and dark color palettes for this session."""
551 is_now_dark = _theme.toggle_dark()
552 self._dark_btn.setText("Light" if is_now_dark else "Dark")
553 self.apply_theme()
554 if self.info_panel.chart_canvas and self.state_manager.time_array:
555 snap = self.state_manager.snapshot_at(self.playback.current_sim_time_ns)
556 self.info_panel.update_charts(self.state_manager, snap)
557
558 def _toggle_legend(self):
559 """Show or hide the canvas legend; syncs the toolbar button state."""
560 visible = self.canvas.toggle_legend()
561 self._legend_btn.setChecked(visible)
562
563 def _toggle_node_labels(self):
564 """Show or hide node labels on the canvas."""
565 self.canvas.toggle_node_labels()
566
567 def _export_canvas(self):
568 """Save the current canvas view as an image file."""
569 path, _ = QFileDialog.getSaveFileName(
570 self, "Export Canvas", "canvas.png", "PNG Image (*.png);;JPEG Image (*.jpg);;All Files (*)"
571 )
572 if path:
573 self.canvas.grab().save(path)
574
575 def _reload_file(self):
576 """Reload the last opened trace file."""
577 if self._last_filename:
578 self.load_file(self._last_filename)
579
580 def _show_shortcuts(self):
581 """Display a summary of keyboard shortcuts with styled key badges."""
582 _kb = (
583 "background:#e8eaed; border:1px solid #c0c3c8;"
584 " border-bottom:2px solid #a0a3a8; border-radius:4px;"
585 " padding:2px 8px; font-family:monospace; font-size:11px;"
586 )
587 html = (
588 f"<table cellpadding='5' cellspacing='0' style='font-size:13px;'>"
589 f"<tr><td colspan='2' style='padding-top:0;padding-bottom:4px;'><b>Playback</b></td></tr>"
590 f"<tr><td align='right'><span style='{_kb}'>Space</span></td>"
591 f" <td style='padding-left:14px;'>Play / Pause</td></tr>"
592 f"<tr><td align='right'>"
593 f" <span style='{_kb}'>&#8592;</span>&nbsp;"
594 f" <span style='{_kb}'>&#8594;</span></td>"
595 f" <td style='padding-left:14px;'>Step backward / forward</td></tr>"
596 f"<tr><td align='right'>"
597 f" <span style='{_kb}'>Home</span>&nbsp;"
598 f" <span style='{_kb}'>End</span></td>"
599 f" <td style='padding-left:14px;'>Jump to start / end</td></tr>"
600 f"<tr><td colspan='2' style='padding-top:10px;padding-bottom:4px;'><b>File</b></td></tr>"
601 f"<tr><td align='right'>"
602 f" <span style='{_kb}'>{_primary_modifier_label()}</span>&nbsp;+&nbsp;"
603 f" <span style='{_kb}'>O</span></td>"
604 f" <td style='padding-left:14px;'>Open trace file</td></tr>"
605 f"<tr><td colspan='2' style='padding-top:10px;padding-bottom:4px;'><b>Display</b></td></tr>"
606 f"<tr><td align='right'><span style='{_kb}'>L</span></td>"
607 f" <td style='padding-left:14px;'>Toggle legend</td></tr>"
608 f"<tr><td align='right'><span style='{_kb}'>N</span></td>"
609 f" <td style='padding-left:14px;'>Toggle node labels</td></tr>"
610 f"</table>"
611 )
612 QMessageBox.information(self, "Keyboard Shortcuts", html)
613
614 def _apply_logo(self):
615 """Set the top-bar logo, tinted to a light tone in dark mode for legibility."""
616 if self._logo_pixmap.isNull():
617 return
618 if _theme.is_dark():
619 self._logo_label.setPixmap(_tinted_pixmap(self._logo_pixmap, QColor(Theme.TEXT_PRIMARY)))
620 else:
621 self._logo_label.setPixmap(self._logo_pixmap)
622
623 def apply_theme(self):
624 """Re-apply the active color palette to the window and all child widgets.
625
626 Call after ``toggle_dark()`` to propagate palette changes throughout
627 the application. Updates stylesheets and triggers a canvas repaint.
628 """
629 self.setStyleSheet(f"background-color: {Theme.BG_DARK.name()};")
630 self.statusBar().setStyleSheet(f"""
631 QStatusBar {{
632 background-color: {Theme.BG_MEDIUM.name()};
633 color: {Theme.TEXT_SECONDARY.name()};
634 border-top: 1px solid {Theme.BORDER.name()};
635 padding: 4px 8px;
636 font-size: 12px;
637 }}
638 """)
639 if hasattr(self, "_top_bar"):
640 self._top_bar.setStyleSheet(self._build_topbar_stylesheet())
641 for _btn in self._top_bar.findChildren(QPushButton):
642 self._top_bar.style().unpolish(_btn)
643 self._top_bar.style().polish(_btn)
644 _btn.update()
645 if hasattr(self, "_title_label"):
646 self._title_label.setStyleSheet(
647 f"color: {Theme.TEXT_PRIMARY.name()}; font-size: 16px; font-weight: 600; background: transparent;"
648 )
649 if hasattr(self, "_logo_label"):
650 self._apply_logo()
653 self.canvas.apply_theme()
654
655
656def main(initial_file: str | None = None):
657 """Launch the Quantum Network Visualizer application."""
658 app = QApplication(sys.argv)
659 app.setApplicationName("Q2NSViz")
660 app.setApplicationDisplayName("Q2NSViz \u2014 Quantum Network Trace Visualizer")
661 app.setStyle("Fusion")
662 app.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont))
663
664 _icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "logo-qnattynet.png")
665 if os.path.isfile(_icon_path):
666 app.setWindowIcon(QIcon(_icon_path))
667
668 window = QuantumVisualizerWindow()
669 window.show()
670
671 if initial_file:
672 window.load_file(initial_file)
673
674 return app.exec()
675
676
677__all__ = ["PlaybackController", "QuantumVisualizerWindow", "main"]
Replays a Q2NS simulation event stream and exposes the quantum state.
Definition logic.py:255
Custom widget that renders the network topology and quantum state.
Definition canvas.py:32
Transport controls, time scrubber, and speed slider.
Definition panels.py:32
Tabbed panel with event log, statistics, entangled states, and charts.
Definition panels.py:203
Owns the simulation clock, playback timer, and all time-navigation logic.
Definition window.py:77
connect_signals(self)
Wire all ControlPanel signals to the appropriate playback methods.
Definition window.py:104
pause(self)
Suspend timer-driven playback.
Definition window.py:149
jump_to_start(self)
Jump to t = 0 and invoke the update callback.
Definition window.py:175
start(self)
Begin timer-driven playback.
Definition window.py:144
step_backward(self)
Retreat to the previous keyframe and invoke the update callback.
Definition window.py:165
step_forward(self)
Advance to the next keyframe and invoke the update callback.
Definition window.py:154
reset(self)
Stop playback, return to t = 0, and restore the slider to its default range.
Definition window.py:131
jump_to_end(self)
Jump to the last event timestamp and invoke the update callback.
Definition window.py:183
setup_for_file(self)
Reset to time zero and configure the slider for a newly loaded file.
Definition window.py:114
Top-level application window.
Definition window.py:254
load_file(self, str filename)
Load a Q2NS simulation trace from filename and initialize playback.
Definition window.py:472
apply_theme(self)
Re-apply the active color palette to the window and all child widgets.
Definition window.py:623