33 """Transport controls, time scrubber, and speed slider.
35 @ingroup q2nsviz_panels
38 play_clicked = pyqtSignal()
39 pause_clicked = pyqtSignal()
40 step_forward = pyqtSignal()
41 step_backward = pyqtSignal()
42 time_changed = pyqtSignal(int)
43 speed_changed = pyqtSignal(int)
47 self.setObjectName(
"ControlPanel")
51 def _apply_stylesheet(self):
52 self.setStyleSheet(f
"""
53 QFrame#ControlPanel {{
54 background-color: {Theme.BG_MEDIUM.name()};
59 background-color: {Theme.PRIMARY.name()};
69 background-color: {Theme.PRIMARY_HOVER.name()};
71 QPushButton:pressed {{
72 background-color: {Theme.PRIMARY_DARK.name()};
74 QPushButton#secondary {{
75 background-color: {Theme.BG_MEDIUM.name()};
76 color: {Theme.TEXT_PRIMARY.name()};
77 border: 1px solid {Theme.BORDER.name()};
79 QPushButton#secondary:hover {{
80 background-color: {Theme.BG_LIGHT.name()};
81 border-color: {Theme.PRIMARY.name()};
83 QPushButton#secondary:checked {{
84 background-color: {Theme.BG_LIGHT.name()};
85 border-color: {Theme.PRIMARY.name()};
86 color: {Theme.PRIMARY.name()};
89 background-color: transparent;
90 color: {Theme.TEXT_SECONDARY.name()};
93 QSlider::groove:horizontal {{
94 background: {Theme.BG_DARK.name()};
97 border: 1px solid {Theme.BORDER.name()};
99 QSlider::handle:horizontal {{
100 background: {Theme.PRIMARY.name()};
105 border: 2px solid {Theme.BG_MEDIUM.name()};
107 QSlider::handle:horizontal:hover {{
108 background: {Theme.PRIMARY_HOVER.name()};
110 QSlider::sub-page:horizontal {{
111 background: {Theme.PRIMARY.name()};
118 layout = QHBoxLayout(self)
119 layout.setContentsMargins(16, 12, 16, 12)
120 layout.setSpacing(20)
122 controls_box = QVBoxLayout()
123 controls_box.setSpacing(8)
125 self.
prev_btn.setObjectName(
"secondary")
126 self.
prev_btn.setToolTip(
"Step backward (←)")
129 self.
play_btn.setToolTip(
"Play / Pause (Space)")
132 self.
next_btn.setObjectName(
"secondary")
133 self.
next_btn.setToolTip(
"Step forward (→)")
135 controls_box.addWidget(self.
play_btn)
136 controls_box.addWidget(self.
prev_btn)
137 controls_box.addWidget(self.
next_btn)
139 self.
loop_btn.setObjectName(
"secondary")
140 self.
loop_btn.setToolTip(
"Loop playback when reaching the end")
142 controls_box.addWidget(self.
loop_btn)
143 layout.addLayout(controls_box)
145 sliders_box = QVBoxLayout()
146 sliders_box.setSpacing(12)
148 time_row = QHBoxLayout()
149 time_row.addWidget(QLabel(
"Time:"))
158 sliders_box.addLayout(time_row)
160 speed_row = QHBoxLayout()
161 speed_row.addWidget(QLabel(
"Speed:"))
171 sliders_box.addLayout(speed_row)
173 layout.addLayout(sliders_box, 1)
175 def _toggle_play(self):
176 """Toggle the play/pause button state and emit the corresponding signal."""
187 self.
play_btn.setText(
"Pause" if playing
else "Play")
190 self.
time_label.setText(f
"{time_us:.3f} μs ({step}/{total})")
196 """Re-apply the active color palette to all styled elements.
198 Call after ``toggle_dark()`` to update the control panel's appearance.
204 """Tabbed panel with event log, statistics, entangled states, and charts.
206 @ingroup q2nsviz_panels
211 self.setObjectName(
"InfoPanel")
212 self.
_save_btn: QPushButton |
None =
None
215 self.
_log_source: list[dict[str, Any]] |
None =
None
220 def _apply_stylesheet(self):
221 self.setStyleSheet(f
"""
223 background-color: {Theme.BG_MEDIUM.name()};
229 background-color: transparent;
230 color: {Theme.TEXT_PRIMARY.name()};
234 background-color: {Theme.BG_DARK.name()};
235 color: {Theme.TEXT_PRIMARY.name()};
236 border: 1px solid {Theme.BORDER.name()};
239 font-family: 'Courier New', monospace;
243 background-color: {Theme.BG_DARK.name()};
244 alternate-background-color: {Theme.BG_MEDIUM.name()};
245 color: {Theme.TEXT_PRIMARY.name()};
246 border: 1px solid {Theme.BORDER.name()};
248 gridline-color: {Theme.BORDER.name()};
250 QTableWidget::item {{
252 border-bottom: 1px solid {Theme.BORDER.name()};
254 QTableWidget::item:selected {{
255 background-color: {Theme.PRIMARY.name()};
258 QHeaderView::section {{
259 background-color: {Theme.BG_DARK.name()};
260 color: {Theme.TEXT_SECONDARY.name()};
269 background-color: {Theme.BG_LIGHT.name()};
270 color: {Theme.TEXT_SECONDARY.name()};
272 border: 1px solid {Theme.BORDER.name()};
274 border-top-left-radius: 6px;
275 border-top-right-radius: 6px;
278 QTabBar::tab:selected {{
279 background-color: {Theme.BG_DARK.name()};
280 color: {Theme.TEXT_PRIMARY.name()};
283 QTabBar::tab:hover {{
284 background-color: {Theme.BG_MEDIUM.name()};
290 background-color: {Theme.BG_DARK.name()};
291 color: {Theme.TEXT_PRIMARY.name()};
292 border: 1px solid {Theme.BORDER.name()};
298 border-color: {Theme.PRIMARY.name()};
304 layout = QVBoxLayout(self)
308 log_widget = QWidget()
309 log_layout = QVBoxLayout(log_widget)
313 self.
tabs.addTab(log_widget,
"Event Log")
315 stats_widget = QWidget()
316 stats_layout = QVBoxLayout(stats_widget)
319 self.
stats_table.setHorizontalHeaderLabels([
"Property",
"Value"])
320 self.
stats_table.horizontalHeader().setStretchLastSection(
True)
321 self.
stats_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
322 self.
stats_table.verticalHeader().setVisible(
False)
324 self.
stats_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
326 self.
tabs.addTab(stats_widget,
"Statistics")
328 entangle_widget = QWidget()
329 entangle_layout = QVBoxLayout(entangle_widget)
332 self.
entangle_table.setHorizontalHeaderLabels([
"State",
"Qubits"])
333 self.
entangle_table.horizontalHeader().setStretchLastSection(
True)
334 self.
entangle_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
337 self.
entangle_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
339 self.
tabs.addTab(entangle_widget,
"Entangled States")
341 if MATPLOTLIB_AVAILABLE:
342 charts_widget = QWidget()
343 charts_layout = QVBoxLayout(charts_widget)
346 save_btn = QPushButton(
"Save chart as PNG/PDF\u2026")
348 save_btn.setStyleSheet(f
"""
350 background-color: {Theme.BG_DARK.name()};
351 color: {Theme.TEXT_PRIMARY.name()};
352 border: 1px solid {Theme.BORDER.name()};
358 border-color: {Theme.PRIMARY.name()};
362 charts_layout.addWidget(save_btn)
363 self.
tabs.addTab(charts_widget,
"Charts")
367 layout.addWidget(self.
tabs)
372 def _save_chart(self):
373 """Open a save-file dialog and export the chart figure to an image file."""
376 path, _ = QFileDialog.getSaveFileName(
380 "PNG Image (*.png);;PDF Document (*.pdf);;SVG Vector (*.svg);;All Files (*)",
383 self.
chart_canvas.fig.savefig(path, dpi=300, bbox_inches=
"tight")
385 def _on_tab_changed(self, _index: int):
386 """Re-render the chart when the chart tab is brought into view.
388 Lazy redraw: the chart is updated only when the tab becomes visible,
389 avoiding unnecessary Matplotlib renders during log or entanglement updates.
399 def _format_log_line(event: dict[str, Any]) -> str:
400 t_us = event.get(
"t_ns", 0) / 1000
401 node = event.get(
"node",
"")
402 prefix = f
"[{t_us:8.3f} μs]"
403 return f
"{prefix} [{node}] {event.get('text', '')}" if node
else f
"{prefix} {event.get('text', '')}"
405 def update_log(self, log_events: list[dict[str, Any]], visible: int):
406 """Show the first *visible* entries of the trace's ``traceText`` log.
408 The log only grows as the clock advances, so stepping forward appends
409 the new lines instead of re-rendering the whole document; the view is
410 rebuilt only when the user scrubs backward or loads a new trace.
412 @param log_events ``SimulationStateManager.log_events`` -- every
413 ``traceText`` event of the trace, in time order.
414 @param visible Number of leading entries to display, from
415 ``SimulationStateManager.log_count_at()``.
425 scrollbar = self.
log_text.verticalScrollBar()
426 at_bottom = scrollbar.value() >= scrollbar.maximum() - 4
429 cursor.movePosition(QTextCursor.MoveOperation.End)
437 first_dropped = self.
log_text.document().findBlockByNumber(visible)
438 cursor.setPosition(max(first_dropped.position() - 1, 0))
439 cursor.movePosition(QTextCursor.MoveOperation.End, QTextCursor.MoveMode.KeepAnchor)
440 cursor.removeSelectedText()
444 scrollbar.setValue(scrollbar.maximum())
457 """Refresh the statistics table with the given snapshot values.
459 @param nodes Total number of network nodes in the simulation.
460 @param events Total number of events in the loaded trace.
461 @param max_time Simulation duration in microseconds.
462 @param qubits Number of live qubits at the current time.
463 @param entangled Number of entangled multi-qubit states.
464 @param measured Number of measured qubits.
465 @param lost Number of lost qubits (removed, neither measured nor discarded).
466 @param discarded Number of qubits removed with ``reason == "discarded"``.
469 (
"Network Nodes", str(nodes)),
470 (
"Simulation Events", str(events)),
471 (
"Duration", f
"{max_time:.3f} μs"),
472 (
"Live Qubits", str(qubits)),
473 (
"Measured Qubits", str(measured)),
474 (
"Lost Qubits", str(lost)),
475 (
"Discarded Qubits", str(discarded)),
476 (
"Entangled States (≥2)", str(entangled)),
479 for i, (prop, value)
in enumerate(stats):
480 self.
stats_table.setItem(i, 0, QTableWidgetItem(prop))
481 self.
stats_table.setItem(i, 1, QTableWidgetItem(value))
484 """Refresh the entanglement-states tab.
486 Rows are sorted by descending component size, then alphabetically,
487 so the largest entangled states appear at the top.
489 @param states Multi-qubit entanglement groups from
490 ``Snapshot.entangled_states``.
492 ordered_states = sorted(
493 (sorted(members)
for members
in states.values()), key=
lambda members: (-len(members), members)
495 self.
entangle_table.setHorizontalHeaderLabels([
"State",
"Qubits"])
497 for i, members
in enumerate(ordered_states):
498 self.
entangle_table.setItem(i, 0, QTableWidgetItem(f
"State {i + 1}"))
499 self.
entangle_table.setItem(i, 1, QTableWidgetItem(
", ".join(members)))
502 """Forward chart data to ``ChartCanvas`` if the Charts tab is visible.
504 Stores ``(state_manager, snap)`` so the charts can be redrawn
505 lazily when the user switches to the Charts tab.
507 @param state_manager Shared ``SimulationStateManager`` instance.
508 @param snap ``Snapshot`` at the current simulation time,
509 queried by the controller.
516 """Re-apply the active color palette to all styled elements.
518 Call after ``toggle_dark()`` to update the info panel's appearance.
update_stats(self, int nodes, int events, float max_time, int qubits=0, int entangled=0, int measured=0, int lost=0, int discarded=0)
Refresh the statistics table with the given snapshot values.