From 1bf911479b1e3fdc0a8ae5df5c5033f373597049 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 26 Dec 2025 21:13:04 +0100 Subject: [PATCH 01/88] Basic prototype of tabs and simulation run widget --- .../quantum_circuit_simulation_dialog.py | 297 ++++++++++++++++++ python/mqt/syrec/syrec_editor.py | 10 + 2 files changed, 307 insertions(+) create mode 100644 python/mqt/syrec/quantum_circuit_simulation_dialog.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py new file mode 100644 index 00000000..5e3942cd --- /dev/null +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -0,0 +1,297 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from PyQt6 import QtCore, QtWidgets + +from mqt import syrec + + +class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] + input_state_qubit_value_checkbox_clicked = QtCore.pyqtSignal( + int, bool, arguments=["relative_qubit_idx", "new_qubit_value"], name="inputStateQubitValueCheckboxClicked" + ) + output_state_qubit_value_checkbox_clicked = QtCore.pyqtSignal( + int, bool, arguments=["relative_qubit_idx", "new_qubit_value"], name="outputStateQubitValueCheckboxClicked" + ) + + def __init__( + self, + simulation_run_number: int, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + associated_quantum_register_label: str, + first_qubit_of_quantum_register: int, + initial_input_state: syrec.n_bit_values_container, + optional_initial_output_state: syrec.n_bit_values_container | None, + ) -> None: + # parent: QtWidgets.QWidget) -> None: + super().__init__() + + # TODO: Validation that input and output state have same size (validate all input parameters) + # TODO: Define validator for input and output state inputs + # TODO: Update input/output state value when qubit value is changed + # TODO: How to render n-dimensional variables + + self.input_state_qubit_checkbox_name_format = "q_{relative_qubit_idx:d}_in_checkB" + self.output_state_qubit_checkbox_name_format = "q_{relative_qubit_idx:d}_out_checkB" + self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" + + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(simulation_run_number)) + # main_layout.addWidget(simulation_run_wrapper_box) + + # TODO: How can we determine whether qubits are readonly + self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 + self.edit_of_qubit_values_enabled: bool = False + + self.input_state: syrec.n_bit_values_container = initial_input_state + self.output_state: syrec.n_bit_values_container | None = optional_initial_output_state + + input_column_label = QtWidgets.QLabel("Input") + output_column_label = QtWidgets.QLabel("Output") + quantum_register_label = QtWidgets.QLabel("Quantum register: " + associated_quantum_register_label) + + # TODO: Add validators + self.input_state_edit_field = QtWidgets.QLineEdit(str(initial_input_state)) + self.input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) + + self.output_state_edit_field = QtWidgets.QLineEdit() + if optional_initial_output_state is not None: + self.output_state_edit_field.setText(str(optional_initial_output_state)) + self.input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) + else: + self.output_state_edit_field.setEnabled(False) + self.output_state_edit_field.setPlaceholderText("-") + + self.view_qubit_values_toggle_button = QtWidgets.QPushButton("Edit qubit values") + + group_box_layout = QtWidgets.QVBoxLayout() + simulation_run_wrapper_box.setLayout(group_box_layout) + + quantum_register_controls_grid_layout = QtWidgets.QGridLayout() + # Grid position component order is row followed by column + quantum_register_controls_grid_layout.addWidget( + input_column_label, 0, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + quantum_register_controls_grid_layout.addWidget( + output_column_label, 0, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + quantum_register_controls_grid_layout.addWidget( + quantum_register_label, 1, 0, alignment=QtCore.Qt.AlignmentFlag.AlignRight + ) + quantum_register_controls_grid_layout.addWidget( + self.input_state_edit_field, 1, 1, alignment=QtCore.Qt.AlignmentFlag.AlignRight + ) + quantum_register_controls_grid_layout.addWidget( + self.output_state_edit_field, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignRight + ) + quantum_register_controls_grid_layout.addWidget(self.view_qubit_values_toggle_button, 1, 3) + group_box_layout.addLayout(quantum_register_controls_grid_layout) + + self.n_qubits_of_quantum_register = initial_input_state.size() + if self.n_qubits_of_quantum_register > 0 and not self.are_qubits_values_readonly: + self.qubit_values_grid_layout = QtWidgets.QGridLayout() + + for qubit in range(first_qubit_of_quantum_register, self.n_qubits_of_quantum_register): + fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ) + qubit_label = QtWidgets.QLabel( + "Qubit: " + fetched_internal_qubit_label + if fetched_internal_qubit_label is not None + else "" + ) + self.qubit_values_grid_layout.addWidget(qubit_label, qubit, 0) + + relative_qubit_idx_in_n_bit_container: int = qubit - first_qubit_of_quantum_register + input_state_qubit_value_checkbox = QtWidgets.QCheckBox( + objectName=self.input_state_qubit_checkbox_name_format.format( + relative_qubit_idx=relative_qubit_idx_in_n_bit_container + ) + ) + input_state_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=self.stringify_qubit_value( + self.input_state.test(relative_qubit_idx_in_n_bit_container) + ) + ) + ) + input_state_qubit_value_checkbox.stateChanged.connect( + lambda relative_qubit_idx=relative_qubit_idx_in_n_bit_container: self.handle_input_state_qubit_value_checkbox_state_change( + relative_qubit_idx, self.state_changed == QtCore.Qt.CheckState.Checked + ) + ) + self.qubit_values_grid_layout.addWidget( + input_state_qubit_value_checkbox, qubit, 1, alignment=QtCore.Qt.AlignmentFlag.AlignRight + ) + + output_state_qubit_value_checkbox = QtWidgets.QCheckBox( + objectName=self.output_state_qubit_checkbox_name_format.format( + relative_qubit_idx=relative_qubit_idx_in_n_bit_container + ) + ) + output_state_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=self.stringify_qubit_value( + None + if self.output_state is None + else self.output_state.test(relative_qubit_idx_in_n_bit_container) + ) + ) + ) + output_state_qubit_value_checkbox.stateChanged.connect( + lambda relative_qubit_idx=relative_qubit_idx_in_n_bit_container: self.handle_output_state_qubit_value_checkbox_state_change( + relative_qubit_idx, self.state_changed == QtCore.Qt.CheckState.Checked + ) + ) + output_state_qubit_value_checkbox.setEnabled(False) + self.qubit_values_grid_layout.addWidget( + output_state_qubit_value_checkbox, qubit, 2, alignment=QtCore.Qt.AlignmentFlag.AlignRight + ) + + group_box_layout.addLayout(self.qubit_values_grid_layout) + + simulation_run_scroll_area = QtWidgets.QScrollArea() + simulation_run_scroll_area.setWidget(simulation_run_wrapper_box) + main_layout.addWidget(simulation_run_scroll_area) + + # TODO: Update n_bit_values_container and parent textfield + def handle_input_state_qubit_value_checkbox_state_change( + self, relative_qubit_index_in_n_bit_values_container: int, qubit_value: bool + ) -> None: + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.qubit_values_grid_layout.findChild( + QtWidgets.QCheckBox, + self.input_state_qubit_checkbox_name_format.format( + relative_qubit_idx=relative_qubit_index_in_n_bit_values_container + ), + ) + if associated_qubit_value_checkbox is None: + return + + associated_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + ) + + # TODO: Update n_bit_values_container and parent textfield + def handle_output_state_qubit_value_checkbox_state_change( + self, relative_qubit_index_in_n_bit_values_container: int, qubit_value: bool + ) -> None: + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.qubit_values_grid_layout.findChild( + QtWidgets.QCheckBox, + self.output_state_qubit_checkbox_name_format.format( + relative_qubit_idx=relative_qubit_index_in_n_bit_values_container + ), + ) + if associated_qubit_value_checkbox is None: + return + + associated_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + ) + + @staticmethod + def stringify_qubit_value(qubit_value: bool | None) -> str: + if qubit_value is None: + return "UNKNOWN" + return "HIGH" if qubit_value else "LOW" + + def handle_edit_qubit_values_toggle_button_click(self) -> None: + if self.edit_of_qubit_values_enabled: + return + + if self.output_state is None: + self.output_state = syrec.n_bit_values_container(self.input_state.size()) + for qubit in range(self.output_state.size()): + if self.input_state.test(qubit): + self.output_state.set(qubit) + + +class SimulationRunDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] + def __init__( + self, + simulation_run_idx: int, # noqa: ARG002 + annotatable_quantum_computation: syrec.annotatable_quantum_computation, # noqa: ARG002 + is_delete_action_enabled: bool, # noqa: ARG002 + parent: QtWidgets.QWidget, # noqa: ARG002 + ) -> None: + super().__init__() + + self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) + + +class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] + def __init__( + self, annotatable_quantum_computation: syrec.annotatable_quantum_computation | None, parent: QtWidgets.QWidget + ) -> None: + super().__init__() + self.parent = parent + self.annotatable_quantum_computation = annotatable_quantum_computation + + self.title = "Define simulation runs for quantum computation" + self.setWindowTitle(self.title) + + self.left = 0 + self.top = 0 + self.width = 600 + self.height = 400 + self.setGeometry(self.left, self.top, self.width, self.height) + + self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) + self.simulation_runs_tab_widget.addTab( + self.initialize_some_simulation_runs_tab(), "Check some input-output mapping combinations" + ) + self.simulation_runs_tab_widget.addTab( + self.initialize_all_simulation_runs_tab(), "Check all input-output mapping combinations" + ) + self.simulation_runs_tab_widget.addTab( + self.initialize_simulation_runs_from_file_tab(), "Check input-output mapping combinations from file" + ) + self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) + + self.layout = QtWidgets.QVBoxLayout() + self.layout.addStretch() + self.layout.addWidget(self.simulation_runs_tab_widget) + self.setLayout(self.layout) + + def initialize_some_simulation_runs_tab(self) -> QtWidgets.QTabWidget: + simulation_runs_list_layout = QtWidgets.QVBoxLayout() + + in_state = syrec.n_bit_values_container(10) + out_state = syrec.n_bit_values_container(10) + simulation_run_one = InputOutputStateMappingDefinitionWidget( + 0, self.annotatable_quantum_computation, "a", 0, in_state, None + ) + simulation_run_two = InputOutputStateMappingDefinitionWidget( + 1, self.annotatable_quantum_computation, "a", 2, in_state, None + ) + simulation_run_three = InputOutputStateMappingDefinitionWidget( + 2, self.annotatable_quantum_computation, "a", 4, in_state, out_state + ) + simulation_runs_list_layout.addWidget(simulation_run_one) + simulation_runs_list_layout.addWidget(simulation_run_two) + simulation_runs_list_layout.addWidget(simulation_run_three) + simulation_runs_list_layout.addStretch() + + tab_widget = QtWidgets.QTabWidget() + tab_widget.setLayout(simulation_runs_list_layout) + return tab_widget + + @staticmethod + def initialize_all_simulation_runs_tab() -> QtWidgets.QTabWidget: + return QtWidgets.QTabWidget() + + @staticmethod + def initialize_simulation_runs_from_file_tab() -> QtWidgets.QTabWidget: + return QtWidgets.QTabWidget() + + def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: + self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 4e3d32eb..72d33f29 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -20,6 +20,9 @@ from mqt import syrec +# We are using a relative import here: https://stackoverflow.com/questions/43728431/relative-imports-modulenotfounderror-no-module-named-x +from .quantum_circuit_simulation_dialog import QuantumCircuitSimulationDialog + if TYPE_CHECKING: from collections.abc import Callable @@ -1349,6 +1352,13 @@ def update_circuit_view_and_qubit_information( self.viewer.load(annotatable_quantum_computation) self.qubits_information_lookup.set_lookup_information(annotatable_quantum_computation) + # TODO: Check other calls of dialog.exec(), see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QDialog.html#PySide6.QtWidgets.QDialog.exec + dialog = QuantumCircuitSimulationDialog(annotatable_quantum_computation, parent=self) + dialog.modal = True + dialog.setWindowTitle("Update configurable options") + # dialog.show() + dialog.exec() + def clear_error_log_and_circuit_view(self) -> None: self.logWidget.clear() self.viewer.clear() From 33c1e54a083db0c52064d5ea7faa8d0a8744301e Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 28 Dec 2025 17:45:47 +0100 Subject: [PATCH 02/88] Added quantum register search controls and fixed qubits toggle functionality --- .../quantum_circuit_simulation_dialog.py | 324 ++++++++++++++---- 1 file changed, 252 insertions(+), 72 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 5e3942cd..4708f229 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -8,11 +8,15 @@ from __future__ import annotations -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec +def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: + return qubit_label.startswith("__q") + + class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] input_state_qubit_value_checkbox_clicked = QtCore.pyqtSignal( int, bool, arguments=["relative_qubit_idx", "new_qubit_value"], name="inputStateQubitValueCheckboxClicked" @@ -25,14 +29,14 @@ def __init__( self, simulation_run_number: int, annotatable_quantum_computation: syrec.annotatable_quantum_computation, - associated_quantum_register_label: str, - first_qubit_of_quantum_register: int, initial_input_state: syrec.n_bit_values_container, optional_initial_output_state: syrec.n_bit_values_container | None, ) -> None: # parent: QtWidgets.QWidget) -> None: super().__init__() + self.annotatable_quantum_computation = annotatable_quantum_computation + # TODO: Validation that input and output state have same size (validate all input parameters) # TODO: Define validator for input and output state inputs # TODO: Update input/output state value when qubit value is changed @@ -41,12 +45,15 @@ def __init__( self.input_state_qubit_checkbox_name_format = "q_{relative_qubit_idx:d}_in_checkB" self.output_state_qubit_checkbox_name_format = "q_{relative_qubit_idx:d}_out_checkB" self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" + self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name}_qubit_values_groupbox" + self.qreg_label_name_format = "qreg_{qreg_name}_label" + self.qreg_input_state_input_field_name_format = "qreg_{qreg_name}_inputState" + self.qreg_output_state_input_field_name_format = "qreg_{qreg_name}_outputState" + self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name}_qubit_values_toggle" main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) - - simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(simulation_run_number)) - # main_layout.addWidget(simulation_run_wrapper_box) + self.simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(simulation_run_number)) # TODO: How can we determine whether qubits are readonly self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 @@ -55,52 +62,118 @@ def __init__( self.input_state: syrec.n_bit_values_container = initial_input_state self.output_state: syrec.n_bit_values_container | None = optional_initial_output_state - input_column_label = QtWidgets.QLabel("Input") - output_column_label = QtWidgets.QLabel("Output") - quantum_register_label = QtWidgets.QLabel("Quantum register: " + associated_quantum_register_label) - # TODO: Add validators - self.input_state_edit_field = QtWidgets.QLineEdit(str(initial_input_state)) - self.input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) - - self.output_state_edit_field = QtWidgets.QLineEdit() - if optional_initial_output_state is not None: - self.output_state_edit_field.setText(str(optional_initial_output_state)) - self.input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) - else: - self.output_state_edit_field.setEnabled(False) - self.output_state_edit_field.setPlaceholderText("-") - - self.view_qubit_values_toggle_button = QtWidgets.QPushButton("Edit qubit values") + self.quantum_register_controls_grid_layout = QtWidgets.QGridLayout() + self.simulation_run_wrapper_box.setLayout(self.quantum_register_controls_grid_layout) + + quantum_register_search_controls_layout = QtWidgets.QHBoxLayout() + quantum_register_search_label = QtWidgets.QLabel("Quantum register:") + self.quantum_register_search_input_field = QtWidgets.QLineEdit() + self.quantum_register_search_input_field.setPlaceholderText("") + + quantum_register_name_regular_expr = QtCore.QRegularExpression(R"(^([_A-Za-z]\w*)?$)") + quantum_register_name_validator = QtGui.QRegularExpressionValidator(quantum_register_name_regular_expr, self) + self.quantum_register_search_input_field.setValidator(quantum_register_name_validator) + + self.quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") + self.quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) + + quantum_register_search_controls_layout.addWidget(quantum_register_search_label) + quantum_register_search_controls_layout.addWidget(self.quantum_register_search_input_field) + quantum_register_search_controls_layout.addWidget(self.quantum_register_search_trigger_button) + self.quantum_register_controls_grid_layout.addLayout( + quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) - group_box_layout = QtWidgets.QVBoxLayout() - simulation_run_wrapper_box.setLayout(group_box_layout) + simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") + self.quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) - quantum_register_controls_grid_layout = QtWidgets.QGridLayout() # Grid position component order is row followed by column - quantum_register_controls_grid_layout.addWidget( - input_column_label, 0, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter - ) - quantum_register_controls_grid_layout.addWidget( - output_column_label, 0, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter - ) - quantum_register_controls_grid_layout.addWidget( - quantum_register_label, 1, 0, alignment=QtCore.Qt.AlignmentFlag.AlignRight - ) - quantum_register_controls_grid_layout.addWidget( - self.input_state_edit_field, 1, 1, alignment=QtCore.Qt.AlignmentFlag.AlignRight + input_column_label = QtWidgets.QLabel("Input") + output_column_label = QtWidgets.QLabel("Output") + + self.quantum_register_controls_grid_layout.addWidget( + input_column_label, 1, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - quantum_register_controls_grid_layout.addWidget( - self.output_state_edit_field, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignRight + self.quantum_register_controls_grid_layout.addWidget( + output_column_label, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - quantum_register_controls_grid_layout.addWidget(self.view_qubit_values_toggle_button, 1, 3) - group_box_layout.addLayout(quantum_register_controls_grid_layout) - - self.n_qubits_of_quantum_register = initial_input_state.size() - if self.n_qubits_of_quantum_register > 0 and not self.are_qubits_values_readonly: - self.qubit_values_grid_layout = QtWidgets.QGridLayout() - for qubit in range(first_qubit_of_quantum_register, self.n_qubits_of_quantum_register): + quantum_register_controls_grid_row: int = 2 + for qreg in annotatable_quantum_computation.qregs.values(): + first_qubit_of_qreg: int = qreg.start + n_qubits_of_qreg: int = qreg.size + + # Skip ancillary quantum registers (we assume that ancillary quantum registers only store ancillary qubits thus only checking the first qubit of the quantum register is sufficient) + # It is not sufficient to simply check via annotatable_quantum_computation.is_circuit_qubit_ancillary since this does not cover garbage qubits generated for local SyReC module variables. + if n_qubits_of_qreg == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( + annotatable_quantum_computation.get_qubit_label(first_qubit_of_qreg, syrec.qubit_label_type.internal) + ): + continue + + quantum_register_label = QtWidgets.QLabel( + "Quantum register: " + qreg.name, objectName=self.qreg_label_name_format.format(qreg_name=qreg.name) + ) + + input_state_edit_field = QtWidgets.QLineEdit( + objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + ) + input_state_edit_field.setText(str(initial_input_state)) + input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) + + output_state_edit_field = QtWidgets.QLineEdit( + objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + ) + if optional_initial_output_state is not None: + output_state_edit_field.setText(str(optional_initial_output_state)) + input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) + else: + output_state_edit_field.setEnabled(False) + output_state_edit_field.setPlaceholderText("-") + + edit_qubit_values_toggle_button = QtWidgets.QPushButton( + "Edit qubit values", + objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name), + ) + # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton + edit_qubit_values_toggle_button.clicked.connect( + lambda _, associated_qreg_name=qreg.name: self.handle_qreg_qubit_values_edit_toggle_button_click( + associated_qreg_name + ) + ) + + self.quantum_register_controls_grid_layout.addWidget( + quantum_register_label, + quantum_register_controls_grid_row, + 0, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + ) + self.quantum_register_controls_grid_layout.addWidget( + input_state_edit_field, + quantum_register_controls_grid_row, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignRight, + ) + self.quantum_register_controls_grid_layout.addWidget( + output_state_edit_field, + quantum_register_controls_grid_row, + 2, + alignment=QtCore.Qt.AlignmentFlag.AlignRight, + ) + self.quantum_register_controls_grid_layout.addWidget( + edit_qubit_values_toggle_button, quantum_register_controls_grid_row, 3 + ) + n_cols_in_quantum_register_controls_grid_layout: int = 4 + + # TODO: Scroll area + input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( + "Qubit values", objectName=self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) + ) + input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() + input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) + + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): + relative_qubit_idx_in_qreg: int = qubit - first_qubit_of_qreg fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal ) @@ -109,60 +182,123 @@ def __init__( if fetched_internal_qubit_label is not None else "" ) - self.qubit_values_grid_layout.addWidget(qubit_label, qubit, 0) + input_output_qubits_value_controls_groupbox_layout.addWidget(qubit_label, relative_qubit_idx_in_qreg, 0) - relative_qubit_idx_in_n_bit_container: int = qubit - first_qubit_of_quantum_register input_state_qubit_value_checkbox = QtWidgets.QCheckBox( objectName=self.input_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_idx_in_n_bit_container + relative_qubit_idx=relative_qubit_idx_in_qreg ) ) input_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value( - self.input_state.test(relative_qubit_idx_in_n_bit_container) - ) + stringified_qubit_value=self.stringify_qubit_value(self.input_state.test(qubit)) ) ) input_state_qubit_value_checkbox.stateChanged.connect( - lambda relative_qubit_idx=relative_qubit_idx_in_n_bit_container: self.handle_input_state_qubit_value_checkbox_state_change( + lambda relative_qubit_idx=relative_qubit_idx_in_qreg: self.handle_input_state_qubit_value_checkbox_state_change( relative_qubit_idx, self.state_changed == QtCore.Qt.CheckState.Checked ) ) - self.qubit_values_grid_layout.addWidget( - input_state_qubit_value_checkbox, qubit, 1, alignment=QtCore.Qt.AlignmentFlag.AlignRight + input_output_qubits_value_controls_groupbox_layout.addWidget( + input_state_qubit_value_checkbox, + relative_qubit_idx_in_qreg, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignRight, ) output_state_qubit_value_checkbox = QtWidgets.QCheckBox( objectName=self.output_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_idx_in_n_bit_container + relative_qubit_idx=relative_qubit_idx_in_qreg ) ) output_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( stringified_qubit_value=self.stringify_qubit_value( - None - if self.output_state is None - else self.output_state.test(relative_qubit_idx_in_n_bit_container) + None if self.output_state is None else self.output_state.test(qubit) ) ) ) output_state_qubit_value_checkbox.stateChanged.connect( - lambda relative_qubit_idx=relative_qubit_idx_in_n_bit_container: self.handle_output_state_qubit_value_checkbox_state_change( + lambda relative_qubit_idx=relative_qubit_idx_in_qreg: self.handle_output_state_qubit_value_checkbox_state_change( relative_qubit_idx, self.state_changed == QtCore.Qt.CheckState.Checked ) ) output_state_qubit_value_checkbox.setEnabled(False) - self.qubit_values_grid_layout.addWidget( - output_state_qubit_value_checkbox, qubit, 2, alignment=QtCore.Qt.AlignmentFlag.AlignRight + input_output_qubits_value_controls_groupbox_layout.addWidget( + output_state_qubit_value_checkbox, + relative_qubit_idx_in_qreg, + 2, + alignment=QtCore.Qt.AlignmentFlag.AlignRight, ) - group_box_layout.addLayout(self.qubit_values_grid_layout) + quantum_register_controls_grid_row += 1 + input_output_qubits_value_controls_groupbox.setVisible(False) + self.quantum_register_controls_grid_layout.addWidget( + input_output_qubits_value_controls_groupbox, + quantum_register_controls_grid_row, + 0, + 1, + n_cols_in_quantum_register_controls_grid_layout, + ) + + # Add a spacer item that will take the remaining horizontal space in the grid layout for each quantum register while vertical resizing should only take the minimum required spacing. + quantum_register_controls_grid_spacer_widget = QtWidgets.QSpacerItem( + 2, 2, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.quantum_register_controls_grid_layout.addItem( + quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 4 + ) + quantum_register_controls_grid_row += 1 + + self.quantum_register_controls_grid_layout.setColumnStretch(0, 0) + self.quantum_register_controls_grid_layout.setColumnStretch(1, 0) + self.quantum_register_controls_grid_layout.setColumnStretch(2, 0) simulation_run_scroll_area = QtWidgets.QScrollArea() - simulation_run_scroll_area.setWidget(simulation_run_wrapper_box) + simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) + simulation_run_scroll_area.setWidgetResizable(True) main_layout.addWidget(simulation_run_scroll_area) + def handle_quantum_register_name_search(self) -> None: + for qreg in self.annotatable_quantum_computation.qregs.values(): + if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( + self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ): + continue + + qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg.name) + ) + qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) + ) + ) + + if ( + qreg_name_label is None + or qreg_input_state_input_field is None + or qreg_output_state_input_field is None + or qreg_edit_qubit_values_toggle_button is None + ): + # TODO: This should not happen + continue + + should_control_be_visible: bool = ( + self.quantum_register_search_input_field.text() is None + or qreg.name.startswith(self.quantum_register_search_input_field.text()) + ) + qreg_name_label.setVisible(should_control_be_visible) + qreg_input_state_input_field.setVisible(should_control_be_visible) + qreg_output_state_input_field.setVisible(should_control_be_visible) + qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) + # TODO: Update n_bit_values_container and parent textfield def handle_input_state_qubit_value_checkbox_state_change( self, relative_qubit_index_in_n_bit_values_container: int, qubit_value: bool @@ -203,6 +339,51 @@ def stringify_qubit_value(qubit_value: bool | None) -> str: return "UNKNOWN" return "HIGH" if qubit_value else "LOW" + def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: + for qreg in self.annotatable_quantum_computation.qregs.values(): + if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( + self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ): + continue + + # TODO: QtCore.Qt.FindDirectChildrenOnly + qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) + ) + qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) + ) + + if ( + qreg_input_state_input_field is None + or qreg_output_state_input_field is None + or qubit_values_groupbox is None + or qubit_values_toggle_button is None + ): + # TODO: This should not happen + continue + + if qreg.name == associated_qreg_name and not qubit_values_groupbox.isVisible(): + qubit_values_groupbox.setVisible(True) + qubit_values_toggle_button.setText("Toggle qubit values edit") + qreg_input_state_input_field.setEnabled(False) + qreg_output_state_input_field.setEnabled(False) + self.quantum_register_search_input_field.setEnabled(False) + self.quantum_register_search_trigger_button.setEnabled(False) + else: + qubit_values_groupbox.setVisible(False) + qubit_values_toggle_button.setText("Edit qubit values") + qreg_input_state_input_field.setEnabled(True) + qreg_output_state_input_field.setEnabled(not qreg_output_state_input_field.text().empty()) + self.quantum_register_search_input_field.setEnabled(True) + self.quantum_register_search_trigger_button.setEnabled(True) + def handle_edit_qubit_values_toggle_button_click(self) -> None: if self.edit_of_qubit_values_enabled: return @@ -230,7 +411,7 @@ def __init__( class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( - self, annotatable_quantum_computation: syrec.annotatable_quantum_computation | None, parent: QtWidgets.QWidget + self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtWidgets.QWidget ) -> None: super().__init__() self.parent = parent @@ -241,8 +422,8 @@ def __init__( self.left = 0 self.top = 0 - self.width = 600 - self.height = 400 + self.width = 1200 + self.height = 800 self.setGeometry(self.left, self.top, self.width, self.height) self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) @@ -265,21 +446,20 @@ def __init__( def initialize_some_simulation_runs_tab(self) -> QtWidgets.QTabWidget: simulation_runs_list_layout = QtWidgets.QVBoxLayout() - in_state = syrec.n_bit_values_container(10) - out_state = syrec.n_bit_values_container(10) + in_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) + out_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) simulation_run_one = InputOutputStateMappingDefinitionWidget( - 0, self.annotatable_quantum_computation, "a", 0, in_state, None + 0, self.annotatable_quantum_computation, in_state, None ) simulation_run_two = InputOutputStateMappingDefinitionWidget( - 1, self.annotatable_quantum_computation, "a", 2, in_state, None + 1, self.annotatable_quantum_computation, in_state, None ) simulation_run_three = InputOutputStateMappingDefinitionWidget( - 2, self.annotatable_quantum_computation, "a", 4, in_state, out_state + 2, self.annotatable_quantum_computation, in_state, out_state ) simulation_runs_list_layout.addWidget(simulation_run_one) simulation_runs_list_layout.addWidget(simulation_run_two) simulation_runs_list_layout.addWidget(simulation_run_three) - simulation_runs_list_layout.addStretch() tab_widget = QtWidgets.QTabWidget() tab_widget.setLayout(simulation_runs_list_layout) From 9008f95c5eb93cd0ecaa89664b2bc58c883957c8 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 28 Dec 2025 22:30:33 +0100 Subject: [PATCH 03/88] Added validator for quantum register contents input fields. Added qubit search controls and modified alignments for qubit value edit controls --- .../quantum_circuit_simulation_dialog.py | 267 ++++++++++++++---- 1 file changed, 206 insertions(+), 61 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 4708f229..3cbe95fd 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -17,6 +17,15 @@ def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> return qubit_label.startswith("__q") +def stringify_some_qubits_of_n_bit_values_container( + n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int +) -> str: + if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): + return "" + + return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) + + class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] input_state_qubit_value_checkbox_clicked = QtCore.pyqtSignal( int, bool, arguments=["relative_qubit_idx", "new_qubit_value"], name="inputStateQubitValueCheckboxClicked" @@ -42,14 +51,16 @@ def __init__( # TODO: Update input/output state value when qubit value is changed # TODO: How to render n-dimensional variables - self.input_state_qubit_checkbox_name_format = "q_{relative_qubit_idx:d}_in_checkB" - self.output_state_qubit_checkbox_name_format = "q_{relative_qubit_idx:d}_out_checkB" + self.qubit_label_name_format = "q_{qubit:d}_lbl" + self.input_state_qubit_checkbox_name_format = "q_{qubit:d}_in_checkB" + self.output_state_qubit_checkbox_name_format = "q_{qubit:d}_out_checkB" self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" - self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name}_qubit_values_groupbox" - self.qreg_label_name_format = "qreg_{qreg_name}_label" - self.qreg_input_state_input_field_name_format = "qreg_{qreg_name}_inputState" - self.qreg_output_state_input_field_name_format = "qreg_{qreg_name}_outputState" - self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name}_qubit_values_toggle" + self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name:s}_qubit_values_groupbox" + self.qreg_label_name_format = "qreg_{qreg_name:s}_lbl" + self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_inputState" + self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_outputState" + self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" + self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) @@ -63,8 +74,8 @@ def __init__( self.output_state: syrec.n_bit_values_container | None = optional_initial_output_state # TODO: Add validators - self.quantum_register_controls_grid_layout = QtWidgets.QGridLayout() - self.simulation_run_wrapper_box.setLayout(self.quantum_register_controls_grid_layout) + quantum_register_controls_grid_layout = QtWidgets.QGridLayout() + self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) quantum_register_search_controls_layout = QtWidgets.QHBoxLayout() quantum_register_search_label = QtWidgets.QLabel("Quantum register:") @@ -81,24 +92,29 @@ def __init__( quantum_register_search_controls_layout.addWidget(quantum_register_search_label) quantum_register_search_controls_layout.addWidget(self.quantum_register_search_input_field) quantum_register_search_controls_layout.addWidget(self.quantum_register_search_trigger_button) - self.quantum_register_controls_grid_layout.addLayout( + quantum_register_controls_grid_layout.addLayout( quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") - self.quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) + quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) # Grid position component order is row followed by column input_column_label = QtWidgets.QLabel("Input") output_column_label = QtWidgets.QLabel("Output") - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( input_column_label, 1, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( output_column_label, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) + n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^(\b)?$") + n_bit_values_container_contents_validator = QtGui.QRegularExpressionValidator( + n_bit_values_container_contents_validator_regular_expr, self + ) + quantum_register_controls_grid_row: int = 2 for qreg in annotatable_quantum_computation.qregs.values(): first_qubit_of_qreg: int = qreg.start @@ -118,19 +134,32 @@ def __init__( input_state_edit_field = QtWidgets.QLineEdit( objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) ) - input_state_edit_field.setText(str(initial_input_state)) - input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) + input_state_edit_field.setText( + stringify_some_qubits_of_n_bit_values_container( + initial_input_state, first_qubit_of_qreg, n_qubits_of_qreg + ) + ) + input_state_edit_field.setReadOnly(self.are_qubits_values_readonly) + input_state_edit_field.setValidator(n_bit_values_container_contents_validator) + input_state_edit_field.setMaxLength(n_qubits_of_qreg) output_state_edit_field = QtWidgets.QLineEdit( objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) ) if optional_initial_output_state is not None: - output_state_edit_field.setText(str(optional_initial_output_state)) - input_state_edit_field.setReadOnly(not self.are_qubits_values_readonly) + output_state_edit_field.setText( + stringify_some_qubits_of_n_bit_values_container( + optional_initial_output_state, first_qubit_of_qreg, n_qubits_of_qreg + ) + ) + output_state_edit_field.setReadOnly(self.are_qubits_values_readonly) else: output_state_edit_field.setEnabled(False) output_state_edit_field.setPlaceholderText("-") + output_state_edit_field.setValidator(n_bit_values_container_contents_validator) + output_state_edit_field.setMaxLength(n_qubits_of_qreg) + edit_qubit_values_toggle_button = QtWidgets.QPushButton( "Edit qubit values", objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name), @@ -142,28 +171,28 @@ def __init__( ) ) - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( quantum_register_label, quantum_register_controls_grid_row, 0, alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( input_state_edit_field, quantum_register_controls_grid_row, 1, - alignment=QtCore.Qt.AlignmentFlag.AlignRight, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( output_state_edit_field, quantum_register_controls_grid_row, 2, - alignment=QtCore.Qt.AlignmentFlag.AlignRight, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( edit_qubit_values_toggle_button, quantum_register_controls_grid_row, 3 ) - n_cols_in_quantum_register_controls_grid_layout: int = 4 + n_cols_in_quantum_register_controls_grid_layout: int = 3 # TODO: Scroll area input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( @@ -172,44 +201,70 @@ def __init__( input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) + qubit_search_layout = QtWidgets.QHBoxLayout() + + qubit_search_label = QtWidgets.QLabel("Qubit") + qubit_search_layout.addWidget(qubit_search_label) + + qubit_search_input_field = QtWidgets.QLineEdit( + objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg.name) + ) + qubit_search_input_field.setPlaceholderText("") + qubit_search_layout.addWidget(qubit_search_input_field) + + qubit_search_trigger_button = QtWidgets.QPushButton("Search") + qubit_search_trigger_button.clicked.connect( + lambda _, associated_qreg_name=qreg.name: self.handle_qubit_search_trigger_button_click( + associated_qreg_name + ) + ) + qubit_search_layout.addWidget(qubit_search_trigger_button) + + input_output_qubits_value_controls_groupbox_layout.addLayout( + qubit_search_layout, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter + ) + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): - relative_qubit_idx_in_qreg: int = qubit - first_qubit_of_qreg + one_based_relative_qubit_idx_in_qreg: int = (qubit - first_qubit_of_qreg) + 1 fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal ) qubit_label = QtWidgets.QLabel( "Qubit: " + fetched_internal_qubit_label if fetched_internal_qubit_label is not None - else "" + else "", + objectName=self.qubit_label_name_format.format(qubit=qubit), + ) + input_output_qubits_value_controls_groupbox_layout.addWidget( + qubit_label, one_based_relative_qubit_idx_in_qreg, 0 ) - input_output_qubits_value_controls_groupbox_layout.addWidget(qubit_label, relative_qubit_idx_in_qreg, 0) input_state_qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=self.input_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_idx_in_qreg - ) + objectName=self.input_state_qubit_checkbox_name_format.format(qubit=qubit) ) input_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( stringified_qubit_value=self.stringify_qubit_value(self.input_state.test(qubit)) ) ) - input_state_qubit_value_checkbox.stateChanged.connect( - lambda relative_qubit_idx=relative_qubit_idx_in_qreg: self.handle_input_state_qubit_value_checkbox_state_change( - relative_qubit_idx, self.state_changed == QtCore.Qt.CheckState.Checked + input_state_qubit_value_checkbox.checkStateChanged.connect( + lambda state, + associated_qreg_name=qreg.name, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg: self.handle_input_state_qubit_value_checkbox_state_change( + associated_qreg_name, + relative_qubit_index_in_quantum_register, + state == QtCore.Qt.CheckState.Checked, ) ) input_output_qubits_value_controls_groupbox_layout.addWidget( input_state_qubit_value_checkbox, - relative_qubit_idx_in_qreg, + one_based_relative_qubit_idx_in_qreg, 1, - alignment=QtCore.Qt.AlignmentFlag.AlignRight, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) output_state_qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=self.output_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_idx_in_qreg - ) + objectName=self.output_state_qubit_checkbox_name_format.format(qubit=qubit) ) output_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( @@ -218,22 +273,30 @@ def __init__( ) ) ) - output_state_qubit_value_checkbox.stateChanged.connect( - lambda relative_qubit_idx=relative_qubit_idx_in_qreg: self.handle_output_state_qubit_value_checkbox_state_change( - relative_qubit_idx, self.state_changed == QtCore.Qt.CheckState.Checked + output_state_qubit_value_checkbox.checkStateChanged.connect( + lambda state, + associated_qreg_name=qreg.name, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg: self.handle_output_state_qubit_value_checkbox_state_change( + associated_qreg_name, + relative_qubit_index_in_quantum_register, + state == QtCore.Qt.CheckState.Checked, ) ) output_state_qubit_value_checkbox.setEnabled(False) input_output_qubits_value_controls_groupbox_layout.addWidget( output_state_qubit_value_checkbox, - relative_qubit_idx_in_qreg, + one_based_relative_qubit_idx_in_qreg, 2, - alignment=QtCore.Qt.AlignmentFlag.AlignRight, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 0) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) + quantum_register_controls_grid_row += 1 input_output_qubits_value_controls_groupbox.setVisible(False) - self.quantum_register_controls_grid_layout.addWidget( + quantum_register_controls_grid_layout.addWidget( input_output_qubits_value_controls_groupbox, quantum_register_controls_grid_row, 0, @@ -245,14 +308,17 @@ def __init__( quantum_register_controls_grid_spacer_widget = QtWidgets.QSpacerItem( 2, 2, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) - self.quantum_register_controls_grid_layout.addItem( + quantum_register_controls_grid_layout.addItem( quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 4 ) quantum_register_controls_grid_row += 1 - self.quantum_register_controls_grid_layout.setColumnStretch(0, 0) - self.quantum_register_controls_grid_layout.setColumnStretch(1, 0) - self.quantum_register_controls_grid_layout.setColumnStretch(2, 0) + quantum_register_controls_grid_layout.setColumnStretch(0, 0) + quantum_register_controls_grid_layout.setColumnStretch(1, 1) + quantum_register_controls_grid_layout.setColumnStretch(2, 1) + quantum_register_controls_grid_layout.setColumnStretch(3, 0) + quantum_register_controls_grid_layout.setColumnStretch(4, 2) + quantum_register_controls_grid_layout.setColumnStretch(5, 0) simulation_run_scroll_area = QtWidgets.QScrollArea() simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) @@ -301,45 +367,124 @@ def handle_quantum_register_name_search(self) -> None: # TODO: Update n_bit_values_container and parent textfield def handle_input_state_qubit_value_checkbox_state_change( - self, relative_qubit_index_in_n_bit_values_container: int, qubit_value: bool + self, associated_qreg_name: str, relative_qubit_index_in_quantum_register: int, qubit_value: bool ) -> None: - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.qubit_values_grid_layout.findChild( + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, self.input_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_index_in_n_bit_values_container + relative_qubit_idx=relative_qubit_index_in_quantum_register ), ) - if associated_qubit_value_checkbox is None: + + qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=associated_qreg_name) + ) + + if associated_qubit_value_checkbox is None or qreg_input_state_input_field is None: + # TODO: This should not happen return associated_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) ) + curr_stringified_input_state: str = qreg_input_state_input_field.text() + qreg_input_state_input_field.setText( + curr_stringified_input_state[:relative_qubit_index_in_quantum_register] + + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") + + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] + ) + # TODO: Update n_bit_values_container and parent textfield def handle_output_state_qubit_value_checkbox_state_change( - self, relative_qubit_index_in_n_bit_values_container: int, qubit_value: bool + self, associated_qreg_name: str, relative_qubit_index_in_quantum_register: int, qubit_value: bool ) -> None: - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.qubit_values_grid_layout.findChild( + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_index_in_n_bit_values_container + relative_qubit_idx=relative_qubit_index_in_quantum_register ), ) - if associated_qubit_value_checkbox is None: + + qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=associated_qreg_name) + ) + + if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: + # TODO: This should not happen return associated_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) ) + curr_stringified_output_state: str = qreg_output_state_input_field.text() + qreg_output_state_input_field.setText( + curr_stringified_output_state[:relative_qubit_index_in_quantum_register] + + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") + + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] + ) + @staticmethod def stringify_qubit_value(qubit_value: bool | None) -> str: if qubit_value is None: return "UNKNOWN" return "HIGH" if qubit_value else "LOW" + def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: + for qreg in self.annotatable_quantum_computation.qregs.values(): + if ( + qreg.size == 0 + or does_qubit_label_start_with_internal_qubit_label_prefix( + self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ) + or qreg.name != associated_quantum_register_name + ): + continue + + qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + self.qreg_qubit_values_groupbox_format.format(qreg_name=associated_quantum_register_name), + ) + if qreg_qubits_groupbox is None: + # TODO: This should not happen + continue + + qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLineEdit, + self.qreg_qubit_search_input_field_name_format.format(qreg_name=associated_quantum_register_name), + ) + if qubit_search_input_field is None: + # TODO: This should not happen + continue + + for qubit in range(qreg.start, qreg.start + qreg.size): + qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, self.qubit_label_name_format.format(qubit=qubit) + ) + input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, self.input_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + if ( + qubit_value_label is None + or input_state_qubit_checkbox is None + or output_state_qubit_checkbox is None + ): + # TODO: This should not happen + continue + + does_qubit_label_match_search_text: bool = self.annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ).startswith(qubit_search_input_field.text()) + qubit_value_label.setVisible(does_qubit_label_match_search_text) + input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: + is_qubit_values_edit_enabled_for_any_qreg: bool = False for qreg in self.annotatable_quantum_computation.qregs.values(): if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) @@ -370,19 +515,19 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name continue if qreg.name == associated_qreg_name and not qubit_values_groupbox.isVisible(): + is_qubit_values_edit_enabled_for_any_qreg = True qubit_values_groupbox.setVisible(True) qubit_values_toggle_button.setText("Toggle qubit values edit") qreg_input_state_input_field.setEnabled(False) qreg_output_state_input_field.setEnabled(False) - self.quantum_register_search_input_field.setEnabled(False) - self.quantum_register_search_trigger_button.setEnabled(False) else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText("Edit qubit values") qreg_input_state_input_field.setEnabled(True) - qreg_output_state_input_field.setEnabled(not qreg_output_state_input_field.text().empty()) - self.quantum_register_search_input_field.setEnabled(True) - self.quantum_register_search_trigger_button.setEnabled(True) + qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 + + self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) + self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) def handle_edit_qubit_values_toggle_button_click(self) -> None: if self.edit_of_qubit_values_enabled: From 7f103dcc93beb9351de682284d1755a85cbe9b5f Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Mon, 29 Dec 2025 20:16:56 +0100 Subject: [PATCH 04/88] Removed some unnecessary scroll areas between simulation run widget and parent list --- .../quantum_circuit_simulation_dialog.py | 255 +++++++++++++----- 1 file changed, 185 insertions(+), 70 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 3cbe95fd..13877fd7 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -8,11 +8,42 @@ from __future__ import annotations +from dataclasses import dataclass + from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec +@dataclass +class InputOutputStateMapping: + input_state: syrec.n_bit_values_container + output_state: syrec.n_bit_values_container | None + + def initialize_output_state_as_copy_of_input_state(self) -> bool: + if self.output_state is not None: + return False + + self.output_state = syrec.n_bit_values_container(self.input_state.size()) + for i in range(self.output_state.size()): + self.output_state.set(self.input_state.test(i)) + return True + + def update_input_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: + if qubit < 0 or qubit >= self.input_state.size(): + return False + + self.input_state.set(qubit, qubit_value) + return True + + def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: + if self.output_state is None or qubit < 0 or qubit >= self.output_state.size(): + return False + + self.output_state.set(qubit, qubit_value) + return True + + def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: return qubit_label.startswith("__q") @@ -27,24 +58,39 @@ def stringify_some_qubits_of_n_bit_values_container( class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] - input_state_qubit_value_checkbox_clicked = QtCore.pyqtSignal( - int, bool, arguments=["relative_qubit_idx", "new_qubit_value"], name="inputStateQubitValueCheckboxClicked" + input_state_qubit_value_change = QtCore.pyqtSignal( + int, + int, + bool, + arguments=["simulation_run_number", "qubit", "new_qubit_value"], + name="inputStateQubitValueChanged", ) - output_state_qubit_value_checkbox_clicked = QtCore.pyqtSignal( - int, bool, arguments=["relative_qubit_idx", "new_qubit_value"], name="outputStateQubitValueCheckboxClicked" + output_state_qubit_value_change = QtCore.pyqtSignal( + int, + int, + bool, + arguments=["simulation_run_number", "qubit", "new_qubit_value"], + name="outputStateQubitValueChanged", ) + requested_simulation_run_deletion = QtCore.pyqtSignal( + int, arguments=["simulation_run_number"], name="simulationRunDeleted" + ) + request_output_state_initialization = QtCore.pyqtSignal(name="requestedOutputStateInitialization") + request_output_state_reset = QtCore.pyqtSignal(name="requestedOutputStateReset") def __init__( self, simulation_run_number: int, annotatable_quantum_computation: syrec.annotatable_quantum_computation, - initial_input_state: syrec.n_bit_values_container, - optional_initial_output_state: syrec.n_bit_values_container | None, + input_output_state_mapping: InputOutputStateMapping, + is_input_state_readonly: bool = False, ) -> None: # parent: QtWidgets.QWidget) -> None: super().__init__() + self.simulation_run_number = simulation_run_number self.annotatable_quantum_computation = annotatable_quantum_computation + self.is_input_state_readonly = is_input_state_readonly # TODO: Validation that input and output state have same size (validate all input parameters) # TODO: Define validator for input and output state inputs @@ -64,15 +110,12 @@ def __init__( main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) - self.simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(simulation_run_number)) + self.simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(self.simulation_run_number)) # TODO: How can we determine whether qubits are readonly - self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 + self.are_qubits_values_readonly: bool = input_output_state_mapping.input_state.size() == 0 self.edit_of_qubit_values_enabled: bool = False - self.input_state: syrec.n_bit_values_container = initial_input_state - self.output_state: syrec.n_bit_values_container | None = optional_initial_output_state - # TODO: Add validators quantum_register_controls_grid_layout = QtWidgets.QGridLayout() self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) @@ -96,8 +139,10 @@ def __init__( quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") - quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) + if not self.is_input_state_readonly: + simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") + simulation_run_delete_button.clicked.connect(self.handle_simulation_run_deletion_button_click) + quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) # Grid position component order is row followed by column input_column_label = QtWidgets.QLabel("Input") @@ -136,23 +181,23 @@ def __init__( ) input_state_edit_field.setText( stringify_some_qubits_of_n_bit_values_container( - initial_input_state, first_qubit_of_qreg, n_qubits_of_qreg + input_output_state_mapping.input_state, first_qubit_of_qreg, n_qubits_of_qreg ) ) - input_state_edit_field.setReadOnly(self.are_qubits_values_readonly) + input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) input_state_edit_field.setValidator(n_bit_values_container_contents_validator) input_state_edit_field.setMaxLength(n_qubits_of_qreg) output_state_edit_field = QtWidgets.QLineEdit( objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) ) - if optional_initial_output_state is not None: + if input_output_state_mapping.output_state is not None: output_state_edit_field.setText( stringify_some_qubits_of_n_bit_values_container( - optional_initial_output_state, first_qubit_of_qreg, n_qubits_of_qreg + input_output_state_mapping.output_state, first_qubit_of_qreg, n_qubits_of_qreg ) ) - output_state_edit_field.setReadOnly(self.are_qubits_values_readonly) + output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) else: output_state_edit_field.setEnabled(False) output_state_edit_field.setPlaceholderText("-") @@ -244,18 +289,28 @@ def __init__( ) input_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value(self.input_state.test(qubit)) + stringified_qubit_value=self.stringify_qubit_value( + input_output_state_mapping.input_state.test(qubit) + ) ) ) - input_state_qubit_value_checkbox.checkStateChanged.connect( - lambda state, - associated_qreg_name=qreg.name, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg: self.handle_input_state_qubit_value_checkbox_state_change( - associated_qreg_name, - relative_qubit_index_in_quantum_register, - state == QtCore.Qt.CheckState.Checked, + + if not self.is_input_state_readonly: + input_state_qubit_value_checkbox.checkStateChanged.connect( + lambda state, + associated_qreg_name=qreg.name, + associated_qubit=qubit, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg + - 1: self.handle_input_state_qubit_value_checkbox_state_change( + associated_qreg_name, + associated_qubit, + relative_qubit_index_in_quantum_register, + state == QtCore.Qt.CheckState.Checked, + ) ) - ) + else: + input_state_qubit_value_checkbox.setEnabled(False) + input_output_qubits_value_controls_groupbox_layout.addWidget( input_state_qubit_value_checkbox, one_based_relative_qubit_idx_in_qreg, @@ -269,20 +324,25 @@ def __init__( output_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( stringified_qubit_value=self.stringify_qubit_value( - None if self.output_state is None else self.output_state.test(qubit) + None + if input_output_state_mapping.output_state is None + else input_output_state_mapping.output_state.test(qubit) ) ) ) output_state_qubit_value_checkbox.checkStateChanged.connect( lambda state, associated_qreg_name=qreg.name, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg: self.handle_output_state_qubit_value_checkbox_state_change( + associated_qubit=qubit, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg + - 1: self.handle_output_state_qubit_value_checkbox_state_change( associated_qreg_name, + associated_qubit, relative_qubit_index_in_quantum_register, state == QtCore.Qt.CheckState.Checked, ) ) - output_state_qubit_value_checkbox.setEnabled(False) + output_state_qubit_value_checkbox.setEnabled(input_output_state_mapping.output_state is not None) input_output_qubits_value_controls_groupbox_layout.addWidget( output_state_qubit_value_checkbox, one_based_relative_qubit_idx_in_qreg, @@ -320,10 +380,11 @@ def __init__( quantum_register_controls_grid_layout.setColumnStretch(4, 2) quantum_register_controls_grid_layout.setColumnStretch(5, 0) - simulation_run_scroll_area = QtWidgets.QScrollArea() - simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) - simulation_run_scroll_area.setWidgetResizable(True) - main_layout.addWidget(simulation_run_scroll_area) + # simulation_run_scroll_area = QtWidgets.QScrollArea() + # simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) + # simulation_run_scroll_area.setWidgetResizable(True) + # main_layout.addWidget(simulation_run_scroll_area) + main_layout.addWidget(self.simulation_run_wrapper_box) def handle_quantum_register_name_search(self) -> None: for qreg in self.annotatable_quantum_computation.qregs.values(): @@ -367,13 +428,15 @@ def handle_quantum_register_name_search(self) -> None: # TODO: Update n_bit_values_container and parent textfield def handle_input_state_qubit_value_checkbox_state_change( - self, associated_qreg_name: str, relative_qubit_index_in_quantum_register: int, qubit_value: bool + self, + associated_qreg_name: str, + associated_qubit: int, + relative_qubit_index_in_quantum_register: int, + qubit_value: bool, ) -> None: associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - self.input_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_index_in_quantum_register - ), + self.input_state_qubit_checkbox_name_format.format(qubit=associated_qubit), ) qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( @@ -397,13 +460,15 @@ def handle_input_state_qubit_value_checkbox_state_change( # TODO: Update n_bit_values_container and parent textfield def handle_output_state_qubit_value_checkbox_state_change( - self, associated_qreg_name: str, relative_qubit_index_in_quantum_register: int, qubit_value: bool + self, + associated_qreg_name: str, + associated_qubit: int, + relative_qubit_index_in_quantum_register: int, + qubit_value: bool, ) -> None: associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - self.output_state_qubit_checkbox_name_format.format( - relative_qubit_idx=relative_qubit_index_in_quantum_register - ), + self.output_state_qubit_checkbox_name_format.format(qubit=associated_qubit), ) qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( @@ -523,21 +588,18 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText("Edit qubit values") - qreg_input_state_input_field.setEnabled(True) + qreg_input_state_input_field.setEnabled(not self.is_input_state_readonly) qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) - def handle_edit_qubit_values_toggle_button_click(self) -> None: - if self.edit_of_qubit_values_enabled: - return + def handle_simulation_run_deletion_button_click(self) -> None: + self.requested_simulation_run_deletion.emit(self.simulation_run_number) - if self.output_state is None: - self.output_state = syrec.n_bit_values_container(self.input_state.size()) - for qubit in range(self.output_state.size()): - if self.input_state.test(qubit): - self.output_state.set(qubit) + def handle_simulation_run_number_update(self, new_simulation_run_number: int) -> None: + self.simulation_run_number = new_simulation_run_number + self.simulation_run_wrapper_box.setText("Simulation run #" + str(self.simulation_run_number)) class SimulationRunDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] @@ -571,6 +633,7 @@ def __init__( self.height = 800 self.setGeometry(self.left, self.top, self.width, self.height) + self.defined_simulation_runs: list[InputOutputStateMapping] = [] self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self.simulation_runs_tab_widget.addTab( self.initialize_some_simulation_runs_tab(), "Check some input-output mapping combinations" @@ -584,31 +647,62 @@ def __init__( self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) self.layout = QtWidgets.QVBoxLayout() - self.layout.addStretch() self.layout.addWidget(self.simulation_runs_tab_widget) + # self.layout.addStretch() self.setLayout(self.layout) - def initialize_some_simulation_runs_tab(self) -> QtWidgets.QTabWidget: + def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: + wrapper_widget = QtWidgets.QFrame(self) simulation_runs_list_layout = QtWidgets.QVBoxLayout() + wrapper_widget.setLayout(simulation_runs_list_layout) - in_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) - out_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) - simulation_run_one = InputOutputStateMappingDefinitionWidget( - 0, self.annotatable_quantum_computation, in_state, None - ) - simulation_run_two = InputOutputStateMappingDefinitionWidget( - 1, self.annotatable_quantum_computation, in_state, None - ) - simulation_run_three = InputOutputStateMappingDefinitionWidget( - 2, self.annotatable_quantum_computation, in_state, out_state - ) - simulation_runs_list_layout.addWidget(simulation_run_one) - simulation_runs_list_layout.addWidget(simulation_run_two) - simulation_runs_list_layout.addWidget(simulation_run_three) + for i in range(3): + in_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) + out_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) - tab_widget = QtWidgets.QTabWidget() - tab_widget.setLayout(simulation_runs_list_layout) - return tab_widget + in_out_state_mapping: InputOutputStateMapping | None = None + if i < 2: + in_out_state_mapping = InputOutputStateMapping(in_state, None) + else: + in_out_state_mapping = InputOutputStateMapping(in_state, out_state) + + simulation_run_widget = InputOutputStateMappingDefinitionWidget( + i, self.annotatable_quantum_computation, in_out_state_mapping, is_input_state_readonly=(i == 1) + ) + # input_state_qubit_value_change = QtCore.pyqtSignal(int, bool, arguments=["qubit", "new_qubit_value"], name="inputStateQubitValueChanged") + # output_state_qubit_value_change = QtCore.pyqtSignal(int, bool, arguments=["qubit", "new_qubit_value"], name="outputStateQubitValueChanged") + # simulation_run_deletion = QtCore.pyqtSignal(int, arguments=["simulation_run_number"], name="simulationRunDeleted") + # request_output_state_initialization = QtCore.pyqtSignal(name="requestedOutputStateInitialization") + # request_output_state_reset = QtCore.pyqtSignal(name="requestedOutputStateReset") + + simulation_run_widget.inputStateQubitValueChanged.connect( + self.handle_simulation_run_input_state_qubit_value_change + ) + simulation_run_widget.outputStateQubitValueChanged.connect( + self.handle_simulation_run_output_state_qubit_value_change + ) + simulation_run_widget.requested_simulation_run_deletion.connect(self.handle_simulation_run_deletion_request) + simulation_runs_list_layout.addWidget(simulation_run_widget) + + self.defined_simulation_runs.append(in_out_state_mapping) + + # simulation_run_one = InputOutputStateMappingDefinitionWidget( + # 0, self.annotatable_quantum_computation, InputOutputStateMapping(in_state, None) + # ) + # simulation_run_two = InputOutputStateMappingDefinitionWidget( + # 1, self.annotatable_quantum_computation, InputOutputStateMapping(in_state, None), is_input_state_readonly=True + # ) + # simulation_run_three = InputOutputStateMappingDefinitionWidget( + # 2, self.annotatable_quantum_computation, InputOutputStateMapping(in_state, out_state) + # ) + # simulation_runs_list_layout.addWidget(simulation_run_one) + # simulation_runs_list_layout.addWidget(simulation_run_two) + # simulation_runs_list_layout.addWidget(simulation_run_three) + + simulation_runs_list_scrollarea = QtWidgets.QScrollArea() + simulation_runs_list_scrollarea.setWidget(wrapper_widget) + simulation_runs_list_scrollarea.setWidgetResizable(True) + return simulation_runs_list_scrollarea @staticmethod def initialize_all_simulation_runs_tab() -> QtWidgets.QTabWidget: @@ -620,3 +714,24 @@ def initialize_simulation_runs_from_file_tab() -> QtWidgets.QTabWidget: def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) + + def handle_simulation_run_input_state_qubit_value_change( + self, simulation_run_number: int, qubit: int, new_qubit_value: bool + ) -> None: + pass + + def handle_simulation_run_output_state_qubit_value_change( + self, simulation_run_number: int, qubit: int, new_qubit_value: bool + ) -> None: + pass + + def handle_simulation_run_deletion_request(self, simulation_run_number: int) -> None: + if simulation_run_number < 0 or simulation_run_number >= len(self.defined_simulation_runs): + # TODO: Log error? + return + + current_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if current_tab_widget is None: + # TODO: This should not happen + return + # self.defined_simulation_runs.pop(simulation_run_number) From 146b2fbd503adc5c56057abb9c3a48a0c7dc75c1 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 31 Dec 2025 14:24:27 +0100 Subject: [PATCH 05/88] Added comment regarding potential rendering issues when using widgets instead of model-view architecture of simulation run list --- python/mqt/syrec/quantum_circuit_simulation_dialog.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 13877fd7..6d6eca7e 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -704,6 +704,10 @@ def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: simulation_runs_list_scrollarea.setWidgetResizable(True) return simulation_runs_list_scrollarea + # Since this function will render many items one should use a QListView with a custom styled delegate to improve rendering performance + # (see: https://forum.qt.io/topic/98733/how-can-i-make-my-listview-that-uses-custom-widgets-more-efficient) + # How would one then edit the simulation run that is not rendered as a widget? + # (regarding performance issues when rendering a lot of items in a list, tree or table view: https://forum.qt.io/topic/159449/qtreeview-with-lots-of-items-is-really-slow-can-it-be-optimised-or-is-something-buggy/31) @staticmethod def initialize_all_simulation_runs_tab() -> QtWidgets.QTabWidget: return QtWidgets.QTabWidget() From 7be1d58404afdd567908e839b1d39fff0b8a3d1d Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 31 Dec 2025 19:25:54 +0100 Subject: [PATCH 06/88] Basic prototype using model-view architecture to display simulation runs instead of widget based design --- .../quantum_circuit_simulation_dialog.py | 101 +++++------- .../qt_simulation_run_model.py | 156 ++++++++++++++++++ 2 files changed, 195 insertions(+), 62 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_model.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 6d6eca7e..590aa542 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -8,40 +8,45 @@ from __future__ import annotations -from dataclasses import dataclass - from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec +# from dataclasses import dataclass +# from .qt_simulation_run_model import InputOutputStateMapping, QSimulationRunModel +from .simulation_view.qt_simulation_run_model import ( + InputOutputStateMapping, + QtSimulationRunModel, + SimulationRunModelStyledItemDelegate, +) -@dataclass -class InputOutputStateMapping: - input_state: syrec.n_bit_values_container - output_state: syrec.n_bit_values_container | None +# @dataclass +# class InputOutputStateMapping: +# input_state: syrec.n_bit_values_container +# output_state: syrec.n_bit_values_container | None - def initialize_output_state_as_copy_of_input_state(self) -> bool: - if self.output_state is not None: - return False +# def initialize_output_state_as_copy_of_input_state(self) -> bool: +# if self.output_state is not None: +# return False - self.output_state = syrec.n_bit_values_container(self.input_state.size()) - for i in range(self.output_state.size()): - self.output_state.set(self.input_state.test(i)) - return True +# self.output_state = syrec.n_bit_values_container(self.input_state.size()) +# for i in range(self.output_state.size()): +# self.output_state.set(self.input_state.test(i)) +# return True - def update_input_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: - if qubit < 0 or qubit >= self.input_state.size(): - return False +# def update_input_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: +# if qubit < 0 or qubit >= self.input_state.size(): +# return False - self.input_state.set(qubit, qubit_value) - return True +# self.input_state.set(qubit, qubit_value) +# return True - def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: - if self.output_state is None or qubit < 0 or qubit >= self.output_state.size(): - return False +# def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: +# if self.output_state is None or qubit < 0 or qubit >= self.output_state.size(): +# return False - self.output_state.set(qubit, qubit_value) - return True +# self.output_state.set(qubit, qubit_value) +# return True def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: @@ -633,7 +638,14 @@ def __init__( self.height = 800 self.setGeometry(self.left, self.top, self.width, self.height) - self.defined_simulation_runs: list[InputOutputStateMapping] = [] + self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(self) # type: ignore[no-untyped-call] + self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() + self.simulation_runs_list_view.setModel(self.simulation_runs_model) + self.simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] + + self.simulation_runs_list_view.setUniformItemSizes(True) + self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self.simulation_runs_tab_widget.addTab( self.initialize_some_simulation_runs_tab(), "Check some input-output mapping combinations" @@ -652,11 +664,7 @@ def __init__( self.setLayout(self.layout) def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: - wrapper_widget = QtWidgets.QFrame(self) - simulation_runs_list_layout = QtWidgets.QVBoxLayout() - wrapper_widget.setLayout(simulation_runs_list_layout) - - for i in range(3): + for i in range(2): in_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) out_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) @@ -666,41 +674,10 @@ def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: else: in_out_state_mapping = InputOutputStateMapping(in_state, out_state) - simulation_run_widget = InputOutputStateMappingDefinitionWidget( - i, self.annotatable_quantum_computation, in_out_state_mapping, is_input_state_readonly=(i == 1) - ) - # input_state_qubit_value_change = QtCore.pyqtSignal(int, bool, arguments=["qubit", "new_qubit_value"], name="inputStateQubitValueChanged") - # output_state_qubit_value_change = QtCore.pyqtSignal(int, bool, arguments=["qubit", "new_qubit_value"], name="outputStateQubitValueChanged") - # simulation_run_deletion = QtCore.pyqtSignal(int, arguments=["simulation_run_number"], name="simulationRunDeleted") - # request_output_state_initialization = QtCore.pyqtSignal(name="requestedOutputStateInitialization") - # request_output_state_reset = QtCore.pyqtSignal(name="requestedOutputStateReset") - - simulation_run_widget.inputStateQubitValueChanged.connect( - self.handle_simulation_run_input_state_qubit_value_change - ) - simulation_run_widget.outputStateQubitValueChanged.connect( - self.handle_simulation_run_output_state_qubit_value_change - ) - simulation_run_widget.requested_simulation_run_deletion.connect(self.handle_simulation_run_deletion_request) - simulation_runs_list_layout.addWidget(simulation_run_widget) - - self.defined_simulation_runs.append(in_out_state_mapping) - - # simulation_run_one = InputOutputStateMappingDefinitionWidget( - # 0, self.annotatable_quantum_computation, InputOutputStateMapping(in_state, None) - # ) - # simulation_run_two = InputOutputStateMappingDefinitionWidget( - # 1, self.annotatable_quantum_computation, InputOutputStateMapping(in_state, None), is_input_state_readonly=True - # ) - # simulation_run_three = InputOutputStateMappingDefinitionWidget( - # 2, self.annotatable_quantum_computation, InputOutputStateMapping(in_state, out_state) - # ) - # simulation_runs_list_layout.addWidget(simulation_run_one) - # simulation_runs_list_layout.addWidget(simulation_run_two) - # simulation_runs_list_layout.addWidget(simulation_run_three) + self.simulation_runs_model.add_simulation_run(in_out_state_mapping) simulation_runs_list_scrollarea = QtWidgets.QScrollArea() - simulation_runs_list_scrollarea.setWidget(wrapper_widget) + simulation_runs_list_scrollarea.setWidget(self.simulation_runs_list_view) simulation_runs_list_scrollarea.setWidgetResizable(True) return simulation_runs_list_scrollarea diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py new file mode 100644 index 00000000..6fdf661e --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -0,0 +1,156 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from dataclasses import dataclass + +from PyQt6 import QtCore, QtGui, QtWidgets + +from mqt import syrec + +# First custom item data role usable according to: https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum +SIMULATION_RUN_IO_STATE_QT_ROLE: int = QtCore.Qt.ItemDataRole.UserRole + + +@dataclass +class InputOutputStateMapping: + input_state: syrec.n_bit_values_container + output_state: syrec.n_bit_values_container | None + + def initialize_output_state_as_copy_of_input_state(self) -> bool: + if self.output_state is not None: + return False + + self.output_state = syrec.n_bit_values_container(self.input_state.size()) + for i in range(self.output_state.size()): + self.output_state.set(self.input_state.test(i)) + return True + + def update_input_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: + if qubit < 0 or qubit >= self.input_state.size(): + return False + + self.input_state.set(qubit, qubit_value) + return True + + def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: + if self.output_state is None or qubit < 0 or qubit >= self.output_state.size(): + return False + + self.output_state.set(qubit, qubit_value) + return True + + +# Progress bar delegate C++ example: https://code.qt.io/cgit/qt/qtbase.git/tree/examples/network/torrent?h=5.15 +class SimulationRunModelStyledItemDelegate(QtWidgets.QStyledItemDelegate): # type: ignore[misc] + def __init__(self, parent=None): + super().__init__(parent) + self.padding = 10 + + @staticmethod + def paint(painter, option, index): + if not index.isValid(): + return + + associated_input_output_mapping: InputOutputStateMapping = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + + painter.save() + rect = option.rect + if option.state & QtWidgets.QStyle.StateFlag.State_Selected: + painter.setBrush(QtGui.QColor("#e6f3ff")) + else: + painter.setBrush(QtGui.QColor("#ffffff")) + + painter.setPen(QtGui.QColor("#cccccc")) + # painter.drawRoundedRect(rect.adjusted(5, 5, -5, -5), 8, 8) + + # 2. Draw Title + title_rect = QtCore.QRect(rect.left() + 15, rect.top() + 15, 100, 50) + painter.setPen(QtCore.Qt.GlobalColor.black) + font = painter.font() + font.setBold(True) + painter.setFont(font) + painter.drawText(title_rect, QtCore.Qt.AlignmentFlag.AlignLeft, "Test") + + col_width = (rect.width() - 150) // 3 + + for i in range(15): # Limit to 9 for example + row = i // 3 + col = i % 3 + label_rect = QtCore.QRect( + rect.left() + 15 + (col * col_width), rect.top() + 45 + (row * 25), col_width - 10, 20 + ) + painter.setBrush(QtGui.QColor("#f0f0f0")) + painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.drawRoundedRect(label_rect, 4, 4) + painter.setPen(QtCore.Qt.GlobalColor.darkGray) + painter.drawText( + label_rect, QtCore.Qt.AlignmentFlag.AlignCenter, str(associated_input_output_mapping.input_state) + ) + + painter.restore() + + # def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex): + # super().paint(painter, option, index) + + # print(index.row()) + # print(index.column()) + + +# Example delegate: https://stackoverflow.com/questions/53105343/is-it-possible-to-add-a-custom-widget-into-a-qlistview +class QtSimulationRunModel(QtCore.QAbstractListModel): # type: ignore[misc] + def __init__(self, parent=None): + super().__init__(parent) + + self.input_output_state_mappings: list[InputOutputStateMapping] = [] + + def rowCount(self, parent: QtCore.QModelIndex) -> int: # noqa: N802 + return 0 if parent.isValid() else len(self.input_output_state_mappings) + + def data(self, index: QtCore.QModelIndex, role: int) -> object: + return ( + None + if not index.isValid() or role != SIMULATION_RUN_IO_STATE_QT_ROLE + else self.input_output_state_mappings[index.row()] + ) + + def add_simulation_run(self, input_output_state_mapping: InputOutputStateMapping) -> bool: + n_simulation_runs: int = len(self.input_output_state_mappings) + self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) + self.input_output_state_mappings.append(input_output_state_mapping) + self.endInsertRows() + return True + + def delete_simulation_run(self, index: QtCore.QModelIndex) -> bool: + # self.beginRemoveRows() + if self.is_model_index_valid(index): + self.input_output_state_mappings.remove(index.row()) + self.layoutChanged.emit() + return True + return False + # self.endRemoveRows() + + def update_input_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: + if self.is_model_index_valid(index) and self.input_output_state_mappings[ + index.row() + ].update_input_state_qubit_value(qubit, qubit_value): + self.dataChanged.emit(index, index) + return True + return False + + def update_output_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: + if self.is_model_index_valid(index) and self.input_output_state_mappings[ + index.row() + ].update_output_state_qubit_value(qubit, qubit_value): + self.dataChanged.emit(index, index) + return True + return False + + def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: + return index.isValid() and index.row() >= 0 and index.row() < len(self.input_output_state_mappings) # type: ignore[no-any-return] From 9abbe448e78953c64cfc86e6bc002c3f3a25f6a8 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 2 Jan 2026 16:18:27 +0100 Subject: [PATCH 07/88] Basic still faulty print of quantum register contents in QStyledItemDelegate overwrite --- .../quantum_circuit_simulation_dialog.py | 8 +- .../qt_simulation_run_model.py | 447 ++++++++++++++++-- 2 files changed, 407 insertions(+), 48 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 590aa542..90f33670 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -638,14 +638,16 @@ def __init__( self.height = 800 self.setGeometry(self.left, self.top, self.width, self.height) - self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(self) # type: ignore[no-untyped-call] + self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() self.simulation_runs_list_view.setModel(self.simulation_runs_model) self.simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] - self.simulation_runs_list_view.setUniformItemSizes(True) self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + self.simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + # self.simulation_runs_list_view.setSpacing(10) + # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self.simulation_runs_tab_widget.addTab( self.initialize_some_simulation_runs_tab(), "Check some input-output mapping combinations" @@ -664,7 +666,7 @@ def __init__( self.setLayout(self.layout) def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: - for i in range(2): + for i in range(10): in_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) out_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 6fdf661e..05b4b2b0 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -9,13 +9,38 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Final from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec +# TODO: Mark as const: https://stackoverflow.com/a/57596202 +# Some debugging tips: https://www.eso.org/~eltmgr/ECS/documents-latest/CUT/sphinx_doc/latest/docs/500_gui_development.html#gdb # First custom item data role usable according to: https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum -SIMULATION_RUN_IO_STATE_QT_ROLE: int = QtCore.Qt.ItemDataRole.UserRole + +# TODO: Why does the mypy checker report the error "no-any-return" when processing the python function: +# def _get_vertical_text_width(options: QtWidgets.QStyleOptionsViewItem, font_size: int) -> int: +# return QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height() +# +# The most common reason is that mypy does not have type information for the QtGui or QtWidgets modules. If you haven't installed the type stubs for your Qt bindings, mypy treats all calls to those libraries as returning Any. +# When you call .height(), mypy sees it as Any. Returning Any from a function marked as -> int triggers the no-any-return warning because mypy cannot verify that the value is actually an integer. +# Solution: +# Install the appropriate type stubs for your framework: +# - For PyQt6: pip install PyQt6-stubs +# - For PySide6: pip install shiboken6 (Type information is usually bundled, but ensure your environment is configured correctly). +SIMULATION_RUN_IO_STATE_QT_ROLE: Final[int] = QtCore.Qt.ItemDataRole.UserRole +QUANTUM_REGISTER_LAYOUT_QT_ROLE: Final[int] = SIMULATION_RUN_IO_STATE_QT_ROLE + 1 +LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE: Final[int] = QUANTUM_REGISTER_LAYOUT_QT_ROLE + 1 +LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE: Final[int] = LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE + 1 +LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: Final[int] = LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE + 1 + + +@dataclass +class QuantumRegisterLayout: + quantum_register_name: str + first_qubit_of_quantum_register: int + quantum_register_size: int @dataclass @@ -47,79 +72,409 @@ def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool return True -# Progress bar delegate C++ example: https://code.qt.io/cgit/qt/qtbase.git/tree/examples/network/torrent?h=5.15 +# Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html class SimulationRunModelStyledItemDelegate(QtWidgets.QStyledItemDelegate): # type: ignore[misc] def __init__(self, parent=None): super().__init__(parent) - self.padding = 10 + + # TODO: Mark as const: https://stackoverflow.com/a/57596202 + self.simulation_run_group_box_title_font_size: Final[int] = 14 + self.simulation_run_group_box_content_font_size: Final[int] = 10 + self.quantum_register_layout_info_text_font_size: Final[int] = 8 + self.stringified_quantum_register_y_spacing: Final[int] = 4 + self.stringified_quantum_register_x_spacing: Final[int] = 6 + self.simulation_run_contents_padding_size: Final[int] = 20 + self.simulation_run_group_box_y_spacing: Final[int] = 10 + + self.quantum_register_layout_text_format = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" + self.quantum_register_name_column_header = "Quantum register" + self.input_state_value_column_header = "INPUT" + self.output_state_value_column_header = "OUTPUT" @staticmethod - def paint(painter, option, index): + def _get_horizontal_text_width(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: + return int( + QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).horizontalAdvance( + text + ) + ) + + @staticmethod + def _get_vertical_text_width(options: QtWidgets.QStyleOptionsViewItem, font_size: int) -> int: + return int(QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height()) + + def _get_estimated_quantum_register_name_column_width( + self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int + ) -> int: + if not index.isValid(): + return 0 + + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) + largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) + largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) + + return (2 * self.stringified_quantum_register_x_spacing) + max( + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + self.quantum_register_name_column_header, option, font_size + ), + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option, font_size + ), + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + self.quantum_register_layout_text_format.format( + first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size + ), + option, + font_size, + ), + ) + + def _get_estimated_quantum_register_contents_column_width( + self, option: QtWidgets.QStyleOptionViewItem, font_size: int, with_leading_whitespace: bool + ) -> int: + return ( + 2 * self.stringified_quantum_register_x_spacing if with_leading_whitespace else 0 + ) + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + "".join(["0" for i in range(32)]), option, font_size + ) + + # TODO: Group box header? + def _get_estimated_bounding_rect( + self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex + ) -> QtCore.QSize: + if not index.isValid(): + return QtCore.QSize(0, 0) + + n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + simulation_run_content_height: int = ( + self.simulation_run_group_box_y_spacing + + self.simulation_run_contents_padding_size + + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_title_font_size + ) + + self.stringified_quantum_register_y_spacing + + n_qregs + * ( + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.quantum_register_layout_info_text_font_size + ) + + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_content_font_size + ) + ) + + ( + (2 * (n_qregs - 1) * self.stringified_quantum_register_y_spacing) + if n_qregs > 1 + else self.stringified_quantum_register_y_spacing + ) + + self.simulation_run_contents_padding_size + + self.simulation_run_group_box_y_spacing + ) + + quantum_register_content_width: int = self._get_estimated_quantum_register_contents_column_width( + option, self.simulation_run_group_box_content_font_size, True + ) + simulation_run_content_width = ( + self.simulation_run_contents_padding_size + + self._get_estimated_quantum_register_name_column_width( + option, index, self.simulation_run_group_box_content_font_size + ) + + (2 * quantum_register_content_width) + + self.simulation_run_contents_padding_size + ) + return QtCore.QSize( + min(simulation_run_content_width, option.rect.bottomRight().x()), + max(simulation_run_content_height, option.rect.topRight().y()), + ) + + def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 + return self._get_estimated_bounding_rect(option, index) + + @staticmethod + def _paint_rect_edge_points( + painter: QtGui.QPainter, rect: QtCore.QRect, font_size: int, color: QtGui.Color + ) -> None: + painter.save() + custom_pen = QtGui.QPen(color) + custom_pen.setWidth(font_size) + painter.setPen(custom_pen) + + painter.drawPoint(QtCore.QPoint(rect.topLeft())) + painter.drawPoint(QtCore.QPoint(rect.topRight())) + painter.drawPoint(QtCore.QPoint(rect.bottomLeft())) + painter.drawPoint(QtCore.QPoint(rect.bottomRight())) + painter.restore() + + @staticmethod + def _stringify_some_qubits_of_n_bit_values_container( + n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int + ) -> str: + if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): + return "" + + return "".join([ + "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) + ]) + + def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid(): return associated_input_output_mapping: InputOutputStateMapping = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) painter.save() - rect = option.rect - if option.state & QtWidgets.QStyle.StateFlag.State_Selected: - painter.setBrush(QtGui.QColor("#e6f3ff")) - else: - painter.setBrush(QtGui.QColor("#ffffff")) - - painter.setPen(QtGui.QColor("#cccccc")) - # painter.drawRoundedRect(rect.adjusted(5, 5, -5, -5), 8, 8) - - # 2. Draw Title - title_rect = QtCore.QRect(rect.left() + 15, rect.top() + 15, 100, 50) - painter.setPen(QtCore.Qt.GlobalColor.black) - font = painter.font() - font.setBold(True) - painter.setFont(font) - painter.drawText(title_rect, QtCore.Qt.AlignmentFlag.AlignLeft, "Test") - - col_width = (rect.width() - 150) // 3 - - for i in range(15): # Limit to 9 for example - row = i // 3 - col = i % 3 - label_rect = QtCore.QRect( - rect.left() + 15 + (col * col_width), rect.top() + 45 + (row * 25), col_width - 10, 20 + estimated_simulation_run_container_size: QtCore.QSize = self._get_estimated_bounding_rect(option, index) + # simulation_run_container_rect = QtCore.QRect(option.rect.topLeft().x(), (index.row() * estimated_simulation_run_container_size.height()) + option.rect.topLeft().y(), estimated_simulation_run_container_size.width(), estimated_simulation_run_container_size.height()) + simulation_run_container_rect = QtCore.QRect( + option.rect.topLeft().x(), + option.rect.topLeft().y() + self.simulation_run_group_box_y_spacing, + estimated_simulation_run_container_size.width(), + estimated_simulation_run_container_size.height() - 2 * self.simulation_run_group_box_y_spacing, + ) + + if QtWidgets.QStyle.StateFlag.State_Selected in option.state: + # print(str(index.row()) + " selected!") + painter.fillRect(simulation_run_container_rect, option.palette.highlight()) + painter.setBrush(option.palette.highlightedText()) + + group_box_opt = QtWidgets.QStyleOptionGroupBox() + group_box_opt.rect = simulation_run_container_rect + group_box_opt.text = "Simulation run #" + str(index.row()) + group_box_opt.color = QtCore.Qt.GlobalColor.black + group_box_opt.textAlignment = QtCore.Qt.AlignmentFlag.AlignLeft + group_box_opt.subControls = ( + QtWidgets.QStyle.SubControl.SC_GroupBoxFrame | QtWidgets.QStyle.SubControl.SC_GroupBoxLabel + ) + group_box_opt.state = QtWidgets.QStyle.StateFlag.State_Raised + group_box_opt.features = QtWidgets.QStyleOptionFrame.FrameFeature.Rounded + + # 2. Draw the control using the current application style + # Using the widget's style ensures it respects OS themes + + # 3. Draw the GroupBox + app_style = QtWidgets.QApplication.style() + app_style.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_GroupBox, group_box_opt, painter) + + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, simulation_run_container_rect, 5, QtCore.Qt.GlobalColor.darkMagenta + ) + + # 3. Calculate where the content inside the box should go + # We use subControlRect to find where the frame actually is + # 4. Calculate Content Area + # This returns a rect relative to the group_box_opt.rect + relative_group_box_content_rect = app_style.subControlRect( + QtWidgets.QStyle.ComplexControl.CC_GroupBox, + group_box_opt, + QtWidgets.QStyle.SubControl.SC_GroupBoxContents, + None, + ) + # The calculation for the position of the contents of the group box does not seems to set the y-coordinate correctly while the x coordinate, width and height are correctly set? Return value of function call might be relative to parent? + relative_group_box_content_rect.setTop( + simulation_run_container_rect.top() + relative_group_box_content_rect.top() + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, relative_group_box_content_rect, 5, QtCore.Qt.GlobalColor.magenta + ) + + quantum_register_name_column_width: int = self._get_estimated_quantum_register_name_column_width( + option, index, self.simulation_run_group_box_content_font_size + ) + quantum_register_name_column_start_x: int = ( + relative_group_box_content_rect.topLeft().x() + self.simulation_run_contents_padding_size + ) + + initial_column_one_rect = QtCore.QRect( + quantum_register_name_column_start_x + self.simulation_run_contents_padding_size, + relative_group_box_content_rect.topLeft().y() + self.stringified_quantum_register_y_spacing, + quantum_register_name_column_width, + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_content_font_size + ), + ) + painter.drawText( + initial_column_one_rect, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + "Quantum register", + ) + + quantum_register_content_width_without_padding: int = ( + self._get_estimated_quantum_register_contents_column_width( + option, self.simulation_run_group_box_content_font_size, False + ) + ) + quantum_register_input_values_start_x: int = ( + initial_column_one_rect.topRight().x() + self.stringified_quantum_register_x_spacing + ) + + initial_column_two_rect = QtCore.QRect( + quantum_register_input_values_start_x, + initial_column_one_rect.topLeft().y(), + quantum_register_content_width_without_padding, + initial_column_one_rect.height(), + ) + painter.drawText( + initial_column_two_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "INPUT" + ) + + quantum_register_output_values_start_x: int = ( + initial_column_two_rect.topRight().x() + self.stringified_quantum_register_x_spacing + ) + initial_column_three_rect = QtCore.QRect( + quantum_register_output_values_start_x, + initial_column_one_rect.topLeft().y(), + quantum_register_content_width_without_padding, + initial_column_two_rect.height(), + ) + painter.drawText( + initial_column_three_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "OUTPUT" + ) + + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, initial_column_one_rect, 5, QtCore.Qt.GlobalColor.red + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, initial_column_two_rect, 5, QtCore.Qt.GlobalColor.blue + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, initial_column_three_rect, 5, QtCore.Qt.GlobalColor.green + ) + + row_idx: int = 1 + row_i_y_offset: int = ( + self.stringified_quantum_register_y_spacing + + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_content_font_size ) - painter.setBrush(QtGui.QColor("#f0f0f0")) - painter.setPen(QtCore.Qt.PenStyle.NoPen) - painter.drawRoundedRect(label_rect, 4, 4) - painter.setPen(QtCore.Qt.GlobalColor.darkGray) + ) + for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): + curr_row_y_offset: int = row_idx * row_i_y_offset + + row_i_column_one = initial_column_one_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) painter.drawText( - label_rect, QtCore.Qt.AlignmentFlag.AlignCenter, str(associated_input_output_mapping.input_state) + row_i_column_one, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + qreg_layout.quantum_register_name, ) - painter.restore() + row_i_column_two = initial_column_two_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + painter.drawText( + row_i_column_two, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.input_state, + qreg_layout.first_qubit_of_quantum_register, + qreg_layout.quantum_register_size, + ), + ) - # def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex): - # super().paint(painter, option, index) + row_i_column_three = initial_column_three_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + painter.drawText( + row_i_column_three, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.output_state, + qreg_layout.first_qubit_of_quantum_register, + qreg_layout.quantum_register_size, + ) + if associated_input_output_mapping.output_state is not None + else "", + ) - # print(index.row()) - # print(index.column()) + painter.save() + quantum_layout_info_text_font = QtGui.QFont( + painter.font().family(), self.quantum_register_layout_info_text_font_size + ) + painter.setPen(QtCore.Qt.GlobalColor.gray) + painter.setFont(quantum_layout_info_text_font) + + row_i_plus_column_one = row_i_column_one.adjusted(0, row_i_y_offset, 0, row_i_y_offset) + painter.drawText( + row_i_plus_column_one, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + self.quantum_register_layout_text_format.format( + first_qubit=qreg_layout.first_qubit_of_quantum_register, n_qubits=qreg_layout.quantum_register_size + ), + ) + painter.restore() + + row_idx += 2 + painter.restore() + return # Example delegate: https://stackoverflow.com/questions/53105343/is-it-possible-to-add-a-custom-widget-into-a-qlistview class QtSimulationRunModel(QtCore.QAbstractListModel): # type: ignore[misc] - def __init__(self, parent=None): + def __init__( + self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtCore.QObject = None + ): super().__init__(parent) - self.input_output_state_mappings: list[InputOutputStateMapping] = [] + self.quantum_register_layouts: list[QuantumRegisterLayout] = ( + QtSimulationRunModel.__record_quantum_register_layouts(annotatable_quantum_computation) + ) + self.longest_quantum_register_name: str = "" + self.largest_quantum_register_size: int = 0 + self.largest_first_qubit_of_quantum_registers: int = 0 + + for qreg_layout in self.quantum_register_layouts: + self.longest_quantum_register_name = ( + qreg_layout.quantum_register_name + if len(qreg_layout.quantum_register_name) > len(self.longest_quantum_register_name) + else self.longest_quantum_register_name + ) + self.largest_quantum_register_size = max( + qreg_layout.quantum_register_size, self.largest_quantum_register_size + ) + + if len(self.quantum_register_layouts) > 0: + self.largest_first_qubit_of_quantum_registers = self.quantum_register_layouts[ + len(self.quantum_register_layouts) - 1 + ].first_qubit_of_quantum_register + + @staticmethod + def _does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: + return qubit_label.startswith("__q") + + @staticmethod + def __record_quantum_register_layouts( + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + ) -> list[QuantumRegisterLayout]: + quantum_register_layouts: list[QuantumRegisterLayout] = [] + for qreg in annotatable_quantum_computation.qregs.values(): + if qreg.size == 0 or QtSimulationRunModel._does_qubit_label_start_with_internal_qubit_label_prefix( + annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ): + continue + + quantum_register_layouts.append(QuantumRegisterLayout(qreg.name, qreg.start, qreg.size)) + return quantum_register_layouts def rowCount(self, parent: QtCore.QModelIndex) -> int: # noqa: N802 return 0 if parent.isValid() else len(self.input_output_state_mappings) def data(self, index: QtCore.QModelIndex, role: int) -> object: - return ( - None - if not index.isValid() or role != SIMULATION_RUN_IO_STATE_QT_ROLE - else self.input_output_state_mappings[index.row()] - ) + if not index.isValid(): + return None + + if role == SIMULATION_RUN_IO_STATE_QT_ROLE: + return self.input_output_state_mappings[index.row()] + + if role == QUANTUM_REGISTER_LAYOUT_QT_ROLE: + return self.quantum_register_layouts + + if role == LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE: + return self.longest_quantum_register_name + + if role == LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE: + return self.largest_quantum_register_size + + if role == LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: + return self.largest_first_qubit_of_quantum_registers + + return None + # TODO: Check for duplicates? def add_simulation_run(self, input_output_state_mapping: InputOutputStateMapping) -> bool: n_simulation_runs: int = len(self.input_output_state_mappings) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) @@ -136,6 +491,7 @@ def delete_simulation_run(self, index: QtCore.QModelIndex) -> bool: return False # self.endRemoveRows() + # TODO: Check for duplicates? def update_input_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: if self.is_model_index_valid(index) and self.input_output_state_mappings[ index.row() @@ -144,6 +500,7 @@ def update_input_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, return True return False + # TODO: Check for duplicates? def update_output_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: if self.is_model_index_valid(index) and self.input_output_state_mappings[ index.row() From 9f0b75921302bc9ed03dcd5226b3f9cc586a32b6 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 4 Jan 2026 13:51:34 +0100 Subject: [PATCH 08/88] Split model and styled item delegate as well as simulation run edit dialog into separate files. Setup basic simulation run controls and layout --- .../quantum_circuit_simulation_dialog.py | 766 +++--------------- .../qt_edit_simulation_run_editor.py | 590 ++++++++++++++ .../qt_simulation_run_model.py | 333 +------- .../qt_simulation_run_styled_item_delegate.py | 363 +++++++++ 4 files changed, 1081 insertions(+), 971 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 90f33670..4ec9109f 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -8,667 +8,172 @@ from __future__ import annotations -from PyQt6 import QtCore, QtGui, QtWidgets +from typing import Final + +from PyQt6 import QtWidgets from mqt import syrec -# from dataclasses import dataclass -# from .qt_simulation_run_model import InputOutputStateMapping, QSimulationRunModel -from .simulation_view.qt_simulation_run_model import ( - InputOutputStateMapping, - QtSimulationRunModel, - SimulationRunModelStyledItemDelegate, -) - -# @dataclass -# class InputOutputStateMapping: -# input_state: syrec.n_bit_values_container -# output_state: syrec.n_bit_values_container | None - -# def initialize_output_state_as_copy_of_input_state(self) -> bool: -# if self.output_state is not None: -# return False - -# self.output_state = syrec.n_bit_values_container(self.input_state.size()) -# for i in range(self.output_state.size()): -# self.output_state.set(self.input_state.test(i)) -# return True - -# def update_input_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: -# if qubit < 0 or qubit >= self.input_state.size(): -# return False - -# self.input_state.set(qubit, qubit_value) -# return True - -# def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: -# if self.output_state is None or qubit < 0 or qubit >= self.output_state.size(): -# return False - -# self.output_state.set(qubit, qubit_value) -# return True - - -def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: - return qubit_label.startswith("__q") - - -def stringify_some_qubits_of_n_bit_values_container( - n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int -) -> str: - if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): - return "" - - return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) - - -class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] - input_state_qubit_value_change = QtCore.pyqtSignal( - int, - int, - bool, - arguments=["simulation_run_number", "qubit", "new_qubit_value"], - name="inputStateQubitValueChanged", - ) - output_state_qubit_value_change = QtCore.pyqtSignal( - int, - int, - bool, - arguments=["simulation_run_number", "qubit", "new_qubit_value"], - name="outputStateQubitValueChanged", - ) - requested_simulation_run_deletion = QtCore.pyqtSignal( - int, arguments=["simulation_run_number"], name="simulationRunDeleted" - ) - request_output_state_initialization = QtCore.pyqtSignal(name="requestedOutputStateInitialization") - request_output_state_reset = QtCore.pyqtSignal(name="requestedOutputStateReset") +from .simulation_view.qt_simulation_run_model import InputOutputStateMapping, QtSimulationRunModel +from .simulation_view.qt_simulation_run_styled_item_delegate import SimulationRunModelStyledItemDelegate + +LOADED_FROM_FILE_INPUT_FIELD_NAME = "load_from_file_input_field" +ADD_SIM_RUN_BTN_NAME = "add_sim_run_btn" +EDIT_SIM_RUN_BTN_NAME = "edit_sim_run_btn" +DELETE_SIM_RUN_BTN_NAME = "delete_sim_run_btn" +SAVE_SIM_RUNS_TO_FILE_BTN_NAME = "save_sims_to_file_btn" +RUN_SIM_RUNS_BTN_NAME = "run_sims_btn" +RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME = "run_sims_stop_first_failure_btn" + +class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( - self, - simulation_run_number: int, - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - input_output_state_mapping: InputOutputStateMapping, - is_input_state_readonly: bool = False, + self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtWidgets.QWidget ) -> None: - # parent: QtWidgets.QWidget) -> None: super().__init__() - - self.simulation_run_number = simulation_run_number + self.parent = parent self.annotatable_quantum_computation = annotatable_quantum_computation - self.is_input_state_readonly = is_input_state_readonly - - # TODO: Validation that input and output state have same size (validate all input parameters) - # TODO: Define validator for input and output state inputs - # TODO: Update input/output state value when qubit value is changed - # TODO: How to render n-dimensional variables - - self.qubit_label_name_format = "q_{qubit:d}_lbl" - self.input_state_qubit_checkbox_name_format = "q_{qubit:d}_in_checkB" - self.output_state_qubit_checkbox_name_format = "q_{qubit:d}_out_checkB" - self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" - self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name:s}_qubit_values_groupbox" - self.qreg_label_name_format = "qreg_{qreg_name:s}_lbl" - self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_inputState" - self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_outputState" - self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" - self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" - - main_layout = QtWidgets.QVBoxLayout() - self.setLayout(main_layout) - self.simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(self.simulation_run_number)) - - # TODO: How can we determine whether qubits are readonly - self.are_qubits_values_readonly: bool = input_output_state_mapping.input_state.size() == 0 - self.edit_of_qubit_values_enabled: bool = False - - # TODO: Add validators - quantum_register_controls_grid_layout = QtWidgets.QGridLayout() - self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) - - quantum_register_search_controls_layout = QtWidgets.QHBoxLayout() - quantum_register_search_label = QtWidgets.QLabel("Quantum register:") - self.quantum_register_search_input_field = QtWidgets.QLineEdit() - self.quantum_register_search_input_field.setPlaceholderText("") - - quantum_register_name_regular_expr = QtCore.QRegularExpression(R"(^([_A-Za-z]\w*)?$)") - quantum_register_name_validator = QtGui.QRegularExpressionValidator(quantum_register_name_regular_expr, self) - self.quantum_register_search_input_field.setValidator(quantum_register_name_validator) - - self.quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") - self.quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) - - quantum_register_search_controls_layout.addWidget(quantum_register_search_label) - quantum_register_search_controls_layout.addWidget(self.quantum_register_search_input_field) - quantum_register_search_controls_layout.addWidget(self.quantum_register_search_trigger_button) - quantum_register_controls_grid_layout.addLayout( - quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter - ) - if not self.is_input_state_readonly: - simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") - simulation_run_delete_button.clicked.connect(self.handle_simulation_run_deletion_button_click) - quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) + self.title = "Define simulation runs for quantum computation" + self.setWindowTitle(self.title) + + self.left = 0 + self.top = 0 + self.width = 1200 + self.height = 800 + self.setGeometry(self.left, self.top, self.width, self.height) - # Grid position component order is row followed by column - input_column_label = QtWidgets.QLabel("Input") - output_column_label = QtWidgets.QLabel("Output") + self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) - quantum_register_controls_grid_layout.addWidget( - input_column_label, 1, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) + self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) + self.simulation_runs_tab_widget.addTab( + QuantumCircuitSimulationDialog.initialize_simulation_runs_tab_widget(self.simulation_runs_model), + "Check some input-output mapping combinations", ) - quantum_register_controls_grid_layout.addWidget( - output_column_label, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + self.simulation_runs_tab_widget.addTab( + QuantumCircuitSimulationDialog.initialize_simulation_runs_tab_widget(self.simulation_runs_model), + "Check all input-output mapping combinations", ) - - n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^(\b)?$") - n_bit_values_container_contents_validator = QtGui.QRegularExpressionValidator( - n_bit_values_container_contents_validator_regular_expr, self + self.simulation_runs_tab_widget.addTab( + QuantumCircuitSimulationDialog.initialize_simulation_runs_tab_widget( + self.simulation_runs_model, create_load_from_file_controls=True + ), + "Check input-output mapping combinations from file", ) + self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) - quantum_register_controls_grid_row: int = 2 - for qreg in annotatable_quantum_computation.qregs.values(): - first_qubit_of_qreg: int = qreg.start - n_qubits_of_qreg: int = qreg.size - - # Skip ancillary quantum registers (we assume that ancillary quantum registers only store ancillary qubits thus only checking the first qubit of the quantum register is sufficient) - # It is not sufficient to simply check via annotatable_quantum_computation.is_circuit_qubit_ancillary since this does not cover garbage qubits generated for local SyReC module variables. - if n_qubits_of_qreg == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( - annotatable_quantum_computation.get_qubit_label(first_qubit_of_qreg, syrec.qubit_label_type.internal) - ): - continue - - quantum_register_label = QtWidgets.QLabel( - "Quantum register: " + qreg.name, objectName=self.qreg_label_name_format.format(qreg_name=qreg.name) - ) - - input_state_edit_field = QtWidgets.QLineEdit( - objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) - ) - input_state_edit_field.setText( - stringify_some_qubits_of_n_bit_values_container( - input_output_state_mapping.input_state, first_qubit_of_qreg, n_qubits_of_qreg - ) - ) - input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) - input_state_edit_field.setValidator(n_bit_values_container_contents_validator) - input_state_edit_field.setMaxLength(n_qubits_of_qreg) - - output_state_edit_field = QtWidgets.QLineEdit( - objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) - ) - if input_output_state_mapping.output_state is not None: - output_state_edit_field.setText( - stringify_some_qubits_of_n_bit_values_container( - input_output_state_mapping.output_state, first_qubit_of_qreg, n_qubits_of_qreg - ) - ) - output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) - else: - output_state_edit_field.setEnabled(False) - output_state_edit_field.setPlaceholderText("-") - - output_state_edit_field.setValidator(n_bit_values_container_contents_validator) - output_state_edit_field.setMaxLength(n_qubits_of_qreg) - - edit_qubit_values_toggle_button = QtWidgets.QPushButton( - "Edit qubit values", - objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name), - ) - # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton - edit_qubit_values_toggle_button.clicked.connect( - lambda _, associated_qreg_name=qreg.name: self.handle_qreg_qubit_values_edit_toggle_button_click( - associated_qreg_name - ) - ) - - quantum_register_controls_grid_layout.addWidget( - quantum_register_label, - quantum_register_controls_grid_row, - 0, - alignment=QtCore.Qt.AlignmentFlag.AlignLeft, - ) - quantum_register_controls_grid_layout.addWidget( - input_state_edit_field, - quantum_register_controls_grid_row, - 1, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, - ) - quantum_register_controls_grid_layout.addWidget( - output_state_edit_field, - quantum_register_controls_grid_row, - 2, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, - ) - quantum_register_controls_grid_layout.addWidget( - edit_qubit_values_toggle_button, quantum_register_controls_grid_row, 3 - ) - n_cols_in_quantum_register_controls_grid_layout: int = 3 - - # TODO: Scroll area - input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( - "Qubit values", objectName=self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) - ) - input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() - input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) - - qubit_search_layout = QtWidgets.QHBoxLayout() + QuantumCircuitSimulationDialog.generate_some_simulation_runs( + 20, self.annotatable_quantum_computation, self.simulation_runs_model + ) - qubit_search_label = QtWidgets.QLabel("Qubit") - qubit_search_layout.addWidget(qubit_search_label) + self.layout = QtWidgets.QVBoxLayout() + self.layout.addWidget(self.simulation_runs_tab_widget) + self.setLayout(self.layout) - qubit_search_input_field = QtWidgets.QLineEdit( - objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg.name) - ) - qubit_search_input_field.setPlaceholderText("") - qubit_search_layout.addWidget(qubit_search_input_field) - - qubit_search_trigger_button = QtWidgets.QPushButton("Search") - qubit_search_trigger_button.clicked.connect( - lambda _, associated_qreg_name=qreg.name: self.handle_qubit_search_trigger_button_click( - associated_qreg_name - ) - ) - qubit_search_layout.addWidget(qubit_search_trigger_button) + # TODO: Load from file controls + @staticmethod + def initialize_simulation_runs_tab_widget( + shared_simulation_runs_model: QtSimulationRunModel, create_load_from_file_controls: bool = False + ) -> QtWidgets.QWidget: + tab_wrapper_widget = QtWidgets.QFrame() + tab_wrapper_widget_layout = QtWidgets.QVBoxLayout() + tab_wrapper_widget.setLayout(tab_wrapper_widget_layout) + + manual_y_space_size: Final[int] = 35 + if create_load_from_file_controls: + tab_wrapper_widget_layout.addLayout( + QuantumCircuitSimulationDialog.initialize_load_simulation_runs_from_file_controls() + ) + tab_wrapper_widget_layout.addSpacing(manual_y_space_size) + + # BEGIN: Create simulation runs list view Qt elements + simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() + simulation_runs_list_view.setModel(shared_simulation_runs_model) + simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] + simulation_runs_list_view.setUniformItemSizes(True) + simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) + simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) - input_output_qubits_value_controls_groupbox_layout.addLayout( - qubit_search_layout, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter - ) + simulation_runs_list_scrollarea = QtWidgets.QScrollArea() + simulation_runs_list_scrollarea.setAutoFillBackground(True) + simulation_runs_list_scrollarea.setWidget(simulation_runs_list_view) + simulation_runs_list_scrollarea.setWidgetResizable(True) + tab_wrapper_widget_layout.addWidget(simulation_runs_list_scrollarea) + # END: Create simulation runs list view Qt elements - for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): - one_based_relative_qubit_idx_in_qreg: int = (qubit - first_qubit_of_qreg) + 1 - fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( - qubit, syrec.qubit_label_type.internal - ) - qubit_label = QtWidgets.QLabel( - "Qubit: " + fetched_internal_qubit_label - if fetched_internal_qubit_label is not None - else "", - objectName=self.qubit_label_name_format.format(qubit=qubit), - ) - input_output_qubits_value_controls_groupbox_layout.addWidget( - qubit_label, one_based_relative_qubit_idx_in_qreg, 0 - ) - - input_state_qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=self.input_state_qubit_checkbox_name_format.format(qubit=qubit) - ) - input_state_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value( - input_output_state_mapping.input_state.test(qubit) - ) - ) - ) - - if not self.is_input_state_readonly: - input_state_qubit_value_checkbox.checkStateChanged.connect( - lambda state, - associated_qreg_name=qreg.name, - associated_qubit=qubit, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - - 1: self.handle_input_state_qubit_value_checkbox_state_change( - associated_qreg_name, - associated_qubit, - relative_qubit_index_in_quantum_register, - state == QtCore.Qt.CheckState.Checked, - ) - ) - else: - input_state_qubit_value_checkbox.setEnabled(False) - - input_output_qubits_value_controls_groupbox_layout.addWidget( - input_state_qubit_value_checkbox, - one_based_relative_qubit_idx_in_qreg, - 1, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, - ) - - output_state_qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=self.output_state_qubit_checkbox_name_format.format(qubit=qubit) - ) - output_state_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value( - None - if input_output_state_mapping.output_state is None - else input_output_state_mapping.output_state.test(qubit) - ) - ) - ) - output_state_qubit_value_checkbox.checkStateChanged.connect( - lambda state, - associated_qreg_name=qreg.name, - associated_qubit=qubit, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - - 1: self.handle_output_state_qubit_value_checkbox_state_change( - associated_qreg_name, - associated_qubit, - relative_qubit_index_in_quantum_register, - state == QtCore.Qt.CheckState.Checked, - ) - ) - output_state_qubit_value_checkbox.setEnabled(input_output_state_mapping.output_state is not None) - input_output_qubits_value_controls_groupbox_layout.addWidget( - output_state_qubit_value_checkbox, - one_based_relative_qubit_idx_in_qreg, - 2, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, - ) - - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 0) - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) - - quantum_register_controls_grid_row += 1 - input_output_qubits_value_controls_groupbox.setVisible(False) - quantum_register_controls_grid_layout.addWidget( - input_output_qubits_value_controls_groupbox, - quantum_register_controls_grid_row, - 0, - 1, - n_cols_in_quantum_register_controls_grid_layout, - ) + # BEGIN: Create simulation runs list modification Qt elements + simulation_runs_list_modification_buttons_layout = QtWidgets.QHBoxLayout() + simulation_runs_list_modification_buttons_layout.addStretch() + add_simulation_run_button = QtWidgets.QPushButton("Add simulation run", objectName=ADD_SIM_RUN_BTN_NAME) + simulation_runs_list_modification_buttons_layout.addWidget(add_simulation_run_button) - # Add a spacer item that will take the remaining horizontal space in the grid layout for each quantum register while vertical resizing should only take the minimum required spacing. - quantum_register_controls_grid_spacer_widget = QtWidgets.QSpacerItem( - 2, 2, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum - ) - quantum_register_controls_grid_layout.addItem( - quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 4 - ) - quantum_register_controls_grid_row += 1 - - quantum_register_controls_grid_layout.setColumnStretch(0, 0) - quantum_register_controls_grid_layout.setColumnStretch(1, 1) - quantum_register_controls_grid_layout.setColumnStretch(2, 1) - quantum_register_controls_grid_layout.setColumnStretch(3, 0) - quantum_register_controls_grid_layout.setColumnStretch(4, 2) - quantum_register_controls_grid_layout.setColumnStretch(5, 0) - - # simulation_run_scroll_area = QtWidgets.QScrollArea() - # simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) - # simulation_run_scroll_area.setWidgetResizable(True) - # main_layout.addWidget(simulation_run_scroll_area) - main_layout.addWidget(self.simulation_run_wrapper_box) - - def handle_quantum_register_name_search(self) -> None: - for qreg in self.annotatable_quantum_computation.qregs.values(): - if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( - self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) - ): - continue - - qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg.name) - ) - qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) - ) - qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) - ) - qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( - self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) - ) - ) + edit_simulation_run_button = QtWidgets.QPushButton("Edit simulation run", objectName=EDIT_SIM_RUN_BTN_NAME) + simulation_runs_list_modification_buttons_layout.addWidget(edit_simulation_run_button) - if ( - qreg_name_label is None - or qreg_input_state_input_field is None - or qreg_output_state_input_field is None - or qreg_edit_qubit_values_toggle_button is None - ): - # TODO: This should not happen - continue - - should_control_be_visible: bool = ( - self.quantum_register_search_input_field.text() is None - or qreg.name.startswith(self.quantum_register_search_input_field.text()) - ) - qreg_name_label.setVisible(should_control_be_visible) - qreg_input_state_input_field.setVisible(should_control_be_visible) - qreg_output_state_input_field.setVisible(should_control_be_visible) - qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) - - # TODO: Update n_bit_values_container and parent textfield - def handle_input_state_qubit_value_checkbox_state_change( - self, - associated_qreg_name: str, - associated_qubit: int, - relative_qubit_index_in_quantum_register: int, - qubit_value: bool, - ) -> None: - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, - self.input_state_qubit_checkbox_name_format.format(qubit=associated_qubit), + delete_simulation_run_button = QtWidgets.QPushButton( + "Delete simulation run", objectName=DELETE_SIM_RUN_BTN_NAME ) + simulation_runs_list_modification_buttons_layout.addWidget(delete_simulation_run_button) - qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=associated_qreg_name) - ) + simulation_runs_list_modification_buttons_layout.addStretch() + tab_wrapper_widget_layout.addLayout(simulation_runs_list_modification_buttons_layout) + # END: Create simulation runs list modification Qt elements - if associated_qubit_value_checkbox is None or qreg_input_state_input_field is None: - # TODO: This should not happen - return + # BEGIN: Create simulation runs execution Qt elements + simulation_runs_execution_buttons_layout = QtWidgets.QHBoxLayout() + simulation_runs_execution_buttons_layout.addStretch() - associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + save_simulation_runs_to_file_button = QtWidgets.QPushButton( + "Save simulation runs to file", objectName="SAVE_SIM_RUNS_TO_FILE_BTN_NAME" ) + simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) - curr_stringified_input_state: str = qreg_input_state_input_field.text() - qreg_input_state_input_field.setText( - curr_stringified_input_state[:relative_qubit_index_in_quantum_register] - + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") - + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] - ) + run_simulation_runs_button = QtWidgets.QPushButton("Run simulation runs", objectName="RUN_SIM_RUNS_BTN_NAME") + simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_button) - # TODO: Update n_bit_values_container and parent textfield - def handle_output_state_qubit_value_checkbox_state_change( - self, - associated_qreg_name: str, - associated_qubit: int, - relative_qubit_index_in_quantum_register: int, - qubit_value: bool, - ) -> None: - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, - self.output_state_qubit_checkbox_name_format.format(qubit=associated_qubit), + run_simulation_runs_stop_at_first_failure_button = QtWidgets.QPushButton( + "Run simulation runs (stop at first failure)", objectName="RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME" ) + simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_stop_at_first_failure_button) - qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=associated_qreg_name) - ) + simulation_runs_execution_buttons_layout.addStretch() - if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: - # TODO: This should not happen - return - - associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) - ) - - curr_stringified_output_state: str = qreg_output_state_input_field.text() - qreg_output_state_input_field.setText( - curr_stringified_output_state[:relative_qubit_index_in_quantum_register] - + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") - + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] - ) + tab_wrapper_widget_layout.addSpacing(manual_y_space_size) + tab_wrapper_widget_layout.addLayout(simulation_runs_execution_buttons_layout) + # END: Create simulation runs execution Qt elements + return tab_wrapper_widget @staticmethod - def stringify_qubit_value(qubit_value: bool | None) -> str: - if qubit_value is None: - return "UNKNOWN" - return "HIGH" if qubit_value else "LOW" - - def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: - for qreg in self.annotatable_quantum_computation.qregs.values(): - if ( - qreg.size == 0 - or does_qubit_label_start_with_internal_qubit_label_prefix( - self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) - ) - or qreg.name != associated_quantum_register_name - ): - continue - - qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, - self.qreg_qubit_values_groupbox_format.format(qreg_name=associated_quantum_register_name), - ) - if qreg_qubits_groupbox is None: - # TODO: This should not happen - continue + def initialize_load_simulation_runs_from_file_controls() -> QtWidgets.QLayout: + controls_layout = QtWidgets.QHBoxLayout() + controls_layout.addStretch() - qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( - QtWidgets.QLineEdit, - self.qreg_qubit_search_input_field_name_format.format(qreg_name=associated_quantum_register_name), - ) - if qubit_search_input_field is None: - # TODO: This should not happen - continue - - for qubit in range(qreg.start, qreg.start + qreg.size): - qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( - QtWidgets.QLabel, self.qubit_label_name_format.format(qubit=qubit) - ) - input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, self.input_state_qubit_checkbox_name_format.format(qubit=qubit) - ) - output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format(qubit=qubit) - ) - if ( - qubit_value_label is None - or input_state_qubit_checkbox is None - or output_state_qubit_checkbox is None - ): - # TODO: This should not happen - continue - - does_qubit_label_match_search_text: bool = self.annotatable_quantum_computation.get_qubit_label( - qubit, syrec.qubit_label_type.internal - ).startswith(qubit_search_input_field.text()) - qubit_value_label.setVisible(does_qubit_label_match_search_text) - input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) - output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) - - def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: - is_qubit_values_edit_enabled_for_any_qreg: bool = False - for qreg in self.annotatable_quantum_computation.qregs.values(): - if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( - self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) - ): - continue - - # TODO: QtCore.Qt.FindDirectChildrenOnly - qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) - ) - qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) - ) - qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) - ) - qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) - ) + info_label = QtWidgets.QLabel("File to load simulation runs from:") + controls_layout.addWidget(info_label) - if ( - qreg_input_state_input_field is None - or qreg_output_state_input_field is None - or qubit_values_groupbox is None - or qubit_values_toggle_button is None - ): - # TODO: This should not happen - continue - - if qreg.name == associated_qreg_name and not qubit_values_groupbox.isVisible(): - is_qubit_values_edit_enabled_for_any_qreg = True - qubit_values_groupbox.setVisible(True) - qubit_values_toggle_button.setText("Toggle qubit values edit") - qreg_input_state_input_field.setEnabled(False) - qreg_output_state_input_field.setEnabled(False) - else: - qubit_values_groupbox.setVisible(False) - qubit_values_toggle_button.setText("Edit qubit values") - qreg_input_state_input_field.setEnabled(not self.is_input_state_readonly) - qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 + selected_file_name_input_field = QtWidgets.QLineEdit(objectName=LOADED_FROM_FILE_INPUT_FIELD_NAME) + selected_file_name_input_field.setEnabled(False) + controls_layout.addWidget(selected_file_name_input_field) - self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) - self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) + open_file_dialog_button = QtWidgets.QPushButton("Select file...") + controls_layout.addWidget(open_file_dialog_button) - def handle_simulation_run_deletion_button_click(self) -> None: - self.requested_simulation_run_deletion.emit(self.simulation_run_number) + trigger_load_from_file_button = QtWidgets.QPushButton("Load from file") + controls_layout.addWidget(trigger_load_from_file_button) - def handle_simulation_run_number_update(self, new_simulation_run_number: int) -> None: - self.simulation_run_number = new_simulation_run_number - self.simulation_run_wrapper_box.setText("Simulation run #" + str(self.simulation_run_number)) + controls_layout.addStretch() + return controls_layout - -class SimulationRunDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] - def __init__( - self, - simulation_run_idx: int, # noqa: ARG002 - annotatable_quantum_computation: syrec.annotatable_quantum_computation, # noqa: ARG002 - is_delete_action_enabled: bool, # noqa: ARG002 - parent: QtWidgets.QWidget, # noqa: ARG002 - ) -> None: - super().__init__() - - self.layout = QtWidgets.QVBoxLayout() - self.setLayout(self.layout) - - -class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] - def __init__( - self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtWidgets.QWidget + def generate_some_simulation_runs( + self: int, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + shared_simulation_runs_model: QtSimulationRunModel, ) -> None: - super().__init__() - self.parent = parent - self.annotatable_quantum_computation = annotatable_quantum_computation - - self.title = "Define simulation runs for quantum computation" - self.setWindowTitle(self.title) - - self.left = 0 - self.top = 0 - self.width = 1200 - self.height = 800 - self.setGeometry(self.left, self.top, self.width, self.height) - - self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) - self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() - self.simulation_runs_list_view.setModel(self.simulation_runs_model) - self.simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] - self.simulation_runs_list_view.setUniformItemSizes(True) - self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) - self.simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) - # self.simulation_runs_list_view.setSpacing(10) - - # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) - self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) - self.simulation_runs_tab_widget.addTab( - self.initialize_some_simulation_runs_tab(), "Check some input-output mapping combinations" - ) - self.simulation_runs_tab_widget.addTab( - self.initialize_all_simulation_runs_tab(), "Check all input-output mapping combinations" - ) - self.simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_from_file_tab(), "Check input-output mapping combinations from file" - ) - self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) - - self.layout = QtWidgets.QVBoxLayout() - self.layout.addWidget(self.simulation_runs_tab_widget) - # self.layout.addStretch() - self.setLayout(self.layout) - - def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: - for i in range(10): - in_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) - out_state = syrec.n_bit_values_container(self.annotatable_quantum_computation.num_qubits) + for i in range(self): + in_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_qubits) + out_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_qubits) in_out_state_mapping: InputOutputStateMapping | None = None if i < 2: @@ -676,24 +181,7 @@ def initialize_some_simulation_runs_tab(self) -> QtWidgets.QWidget: else: in_out_state_mapping = InputOutputStateMapping(in_state, out_state) - self.simulation_runs_model.add_simulation_run(in_out_state_mapping) - - simulation_runs_list_scrollarea = QtWidgets.QScrollArea() - simulation_runs_list_scrollarea.setWidget(self.simulation_runs_list_view) - simulation_runs_list_scrollarea.setWidgetResizable(True) - return simulation_runs_list_scrollarea - - # Since this function will render many items one should use a QListView with a custom styled delegate to improve rendering performance - # (see: https://forum.qt.io/topic/98733/how-can-i-make-my-listview-that-uses-custom-widgets-more-efficient) - # How would one then edit the simulation run that is not rendered as a widget? - # (regarding performance issues when rendering a lot of items in a list, tree or table view: https://forum.qt.io/topic/159449/qtreeview-with-lots-of-items-is-really-slow-can-it-be-optimised-or-is-something-buggy/31) - @staticmethod - def initialize_all_simulation_runs_tab() -> QtWidgets.QTabWidget: - return QtWidgets.QTabWidget() - - @staticmethod - def initialize_simulation_runs_from_file_tab() -> QtWidgets.QTabWidget: - return QtWidgets.QTabWidget() + shared_simulation_runs_model.add_simulation_run(in_out_state_mapping) def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) diff --git a/python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py b/python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py new file mode 100644 index 00000000..ec69d82f --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py @@ -0,0 +1,590 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PyQt6 import QtCore, QtGui, QtWidgets + +from mqt import syrec + +if TYPE_CHECKING: + from .qt_simulation_run_model import InputOutputStateMapping + + +def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: + return qubit_label.startswith("__q") + + +def stringify_some_qubits_of_n_bit_values_container( + n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int +) -> str: + if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): + return "" + + return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) + + +class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] + input_state_qubit_value_change = QtCore.pyqtSignal( + int, + int, + bool, + arguments=["simulation_run_number", "qubit", "new_qubit_value"], + name="inputStateQubitValueChanged", + ) + output_state_qubit_value_change = QtCore.pyqtSignal( + int, + int, + bool, + arguments=["simulation_run_number", "qubit", "new_qubit_value"], + name="outputStateQubitValueChanged", + ) + requested_simulation_run_deletion = QtCore.pyqtSignal( + int, arguments=["simulation_run_number"], name="simulationRunDeleted" + ) + request_output_state_initialization = QtCore.pyqtSignal(name="requestedOutputStateInitialization") + request_output_state_reset = QtCore.pyqtSignal(name="requestedOutputStateReset") + + def __init__( + self, + simulation_run_number: int, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + input_output_state_mapping: InputOutputStateMapping, + is_input_state_readonly: bool = False, + ) -> None: + # parent: QtWidgets.QWidget) -> None: + super().__init__() + + self.simulation_run_number = simulation_run_number + self.annotatable_quantum_computation = annotatable_quantum_computation + self.is_input_state_readonly = is_input_state_readonly + + # TODO: Validation that input and output state have same size (validate all input parameters) + # TODO: Define validator for input and output state inputs + # TODO: Update input/output state value when qubit value is changed + # TODO: How to render n-dimensional variables + + self.qubit_label_name_format = "q_{qubit:d}_lbl" + self.input_state_qubit_checkbox_name_format = "q_{qubit:d}_in_checkB" + self.output_state_qubit_checkbox_name_format = "q_{qubit:d}_out_checkB" + self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" + self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name:s}_qubit_values_groupbox" + self.qreg_label_name_format = "qreg_{qreg_name:s}_lbl" + self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_inputState" + self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_outputState" + self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" + self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" + + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + self.simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(self.simulation_run_number)) + + # TODO: How can we determine whether qubits are readonly + self.are_qubits_values_readonly: bool = input_output_state_mapping.input_state.size() == 0 + self.edit_of_qubit_values_enabled: bool = False + + # TODO: Add validators + quantum_register_controls_grid_layout = QtWidgets.QGridLayout() + self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) + + quantum_register_search_controls_layout = QtWidgets.QHBoxLayout() + quantum_register_search_label = QtWidgets.QLabel("Quantum register:") + self.quantum_register_search_input_field = QtWidgets.QLineEdit() + self.quantum_register_search_input_field.setPlaceholderText("") + + quantum_register_name_regular_expr = QtCore.QRegularExpression(R"(^([_A-Za-z]\w*)?$)") + quantum_register_name_validator = QtGui.QRegularExpressionValidator(quantum_register_name_regular_expr, self) + self.quantum_register_search_input_field.setValidator(quantum_register_name_validator) + + self.quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") + self.quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) + + quantum_register_search_controls_layout.addWidget(quantum_register_search_label) + quantum_register_search_controls_layout.addWidget(self.quantum_register_search_input_field) + quantum_register_search_controls_layout.addWidget(self.quantum_register_search_trigger_button) + quantum_register_controls_grid_layout.addLayout( + quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + + if not self.is_input_state_readonly: + simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") + simulation_run_delete_button.clicked.connect(self.handle_simulation_run_deletion_button_click) + quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) + + # Grid position component order is row followed by column + input_column_label = QtWidgets.QLabel("Input") + output_column_label = QtWidgets.QLabel("Output") + + quantum_register_controls_grid_layout.addWidget( + input_column_label, 1, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + quantum_register_controls_grid_layout.addWidget( + output_column_label, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + + n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^(\b)?$") + n_bit_values_container_contents_validator = QtGui.QRegularExpressionValidator( + n_bit_values_container_contents_validator_regular_expr, self + ) + + quantum_register_controls_grid_row: int = 2 + for qreg in annotatable_quantum_computation.qregs.values(): + first_qubit_of_qreg: int = qreg.start + n_qubits_of_qreg: int = qreg.size + + # Skip ancillary quantum registers (we assume that ancillary quantum registers only store ancillary qubits thus only checking the first qubit of the quantum register is sufficient) + # It is not sufficient to simply check via annotatable_quantum_computation.is_circuit_qubit_ancillary since this does not cover garbage qubits generated for local SyReC module variables. + if n_qubits_of_qreg == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( + annotatable_quantum_computation.get_qubit_label(first_qubit_of_qreg, syrec.qubit_label_type.internal) + ): + continue + + quantum_register_label = QtWidgets.QLabel( + "Quantum register: " + qreg.name, objectName=self.qreg_label_name_format.format(qreg_name=qreg.name) + ) + + input_state_edit_field = QtWidgets.QLineEdit( + objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + ) + input_state_edit_field.setText( + stringify_some_qubits_of_n_bit_values_container( + input_output_state_mapping.input_state, first_qubit_of_qreg, n_qubits_of_qreg + ) + ) + input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) + input_state_edit_field.setValidator(n_bit_values_container_contents_validator) + input_state_edit_field.setMaxLength(n_qubits_of_qreg) + + output_state_edit_field = QtWidgets.QLineEdit( + objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + ) + if input_output_state_mapping.output_state is not None: + output_state_edit_field.setText( + stringify_some_qubits_of_n_bit_values_container( + input_output_state_mapping.output_state, first_qubit_of_qreg, n_qubits_of_qreg + ) + ) + output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) + else: + output_state_edit_field.setEnabled(False) + output_state_edit_field.setPlaceholderText("-") + + output_state_edit_field.setValidator(n_bit_values_container_contents_validator) + output_state_edit_field.setMaxLength(n_qubits_of_qreg) + + edit_qubit_values_toggle_button = QtWidgets.QPushButton( + "Edit qubit values", + objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name), + ) + # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton + edit_qubit_values_toggle_button.clicked.connect( + lambda _, associated_qreg_name=qreg.name: self.handle_qreg_qubit_values_edit_toggle_button_click( + associated_qreg_name + ) + ) + + quantum_register_controls_grid_layout.addWidget( + quantum_register_label, + quantum_register_controls_grid_row, + 0, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + ) + quantum_register_controls_grid_layout.addWidget( + input_state_edit_field, + quantum_register_controls_grid_row, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + ) + quantum_register_controls_grid_layout.addWidget( + output_state_edit_field, + quantum_register_controls_grid_row, + 2, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + ) + quantum_register_controls_grid_layout.addWidget( + edit_qubit_values_toggle_button, quantum_register_controls_grid_row, 3 + ) + n_cols_in_quantum_register_controls_grid_layout: int = 3 + + # TODO: Scroll area + input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( + "Qubit values", objectName=self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) + ) + input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() + input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) + + qubit_search_layout = QtWidgets.QHBoxLayout() + + qubit_search_label = QtWidgets.QLabel("Qubit") + qubit_search_layout.addWidget(qubit_search_label) + + qubit_search_input_field = QtWidgets.QLineEdit( + objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg.name) + ) + qubit_search_input_field.setPlaceholderText("") + qubit_search_layout.addWidget(qubit_search_input_field) + + qubit_search_trigger_button = QtWidgets.QPushButton("Search") + qubit_search_trigger_button.clicked.connect( + lambda _, associated_qreg_name=qreg.name: self.handle_qubit_search_trigger_button_click( + associated_qreg_name + ) + ) + qubit_search_layout.addWidget(qubit_search_trigger_button) + + input_output_qubits_value_controls_groupbox_layout.addLayout( + qubit_search_layout, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): + one_based_relative_qubit_idx_in_qreg: int = (qubit - first_qubit_of_qreg) + 1 + fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ) + qubit_label = QtWidgets.QLabel( + "Qubit: " + fetched_internal_qubit_label + if fetched_internal_qubit_label is not None + else "", + objectName=self.qubit_label_name_format.format(qubit=qubit), + ) + input_output_qubits_value_controls_groupbox_layout.addWidget( + qubit_label, one_based_relative_qubit_idx_in_qreg, 0 + ) + + input_state_qubit_value_checkbox = QtWidgets.QCheckBox( + objectName=self.input_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + input_state_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=self.stringify_qubit_value( + input_output_state_mapping.input_state.test(qubit) + ) + ) + ) + + if not self.is_input_state_readonly: + input_state_qubit_value_checkbox.checkStateChanged.connect( + lambda state, + associated_qreg_name=qreg.name, + associated_qubit=qubit, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg + - 1: self.handle_input_state_qubit_value_checkbox_state_change( + associated_qreg_name, + associated_qubit, + relative_qubit_index_in_quantum_register, + state == QtCore.Qt.CheckState.Checked, + ) + ) + else: + input_state_qubit_value_checkbox.setEnabled(False) + + input_output_qubits_value_controls_groupbox_layout.addWidget( + input_state_qubit_value_checkbox, + one_based_relative_qubit_idx_in_qreg, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + ) + + output_state_qubit_value_checkbox = QtWidgets.QCheckBox( + objectName=self.output_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + output_state_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=self.stringify_qubit_value( + None + if input_output_state_mapping.output_state is None + else input_output_state_mapping.output_state.test(qubit) + ) + ) + ) + output_state_qubit_value_checkbox.checkStateChanged.connect( + lambda state, + associated_qreg_name=qreg.name, + associated_qubit=qubit, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg + - 1: self.handle_output_state_qubit_value_checkbox_state_change( + associated_qreg_name, + associated_qubit, + relative_qubit_index_in_quantum_register, + state == QtCore.Qt.CheckState.Checked, + ) + ) + output_state_qubit_value_checkbox.setEnabled(input_output_state_mapping.output_state is not None) + input_output_qubits_value_controls_groupbox_layout.addWidget( + output_state_qubit_value_checkbox, + one_based_relative_qubit_idx_in_qreg, + 2, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + ) + + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 0) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) + + quantum_register_controls_grid_row += 1 + input_output_qubits_value_controls_groupbox.setVisible(False) + quantum_register_controls_grid_layout.addWidget( + input_output_qubits_value_controls_groupbox, + quantum_register_controls_grid_row, + 0, + 1, + n_cols_in_quantum_register_controls_grid_layout, + ) + + # Add a spacer item that will take the remaining horizontal space in the grid layout for each quantum register while vertical resizing should only take the minimum required spacing. + quantum_register_controls_grid_spacer_widget = QtWidgets.QSpacerItem( + 2, 2, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + quantum_register_controls_grid_layout.addItem( + quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 4 + ) + quantum_register_controls_grid_row += 1 + + quantum_register_controls_grid_layout.setColumnStretch(0, 0) + quantum_register_controls_grid_layout.setColumnStretch(1, 1) + quantum_register_controls_grid_layout.setColumnStretch(2, 1) + quantum_register_controls_grid_layout.setColumnStretch(3, 0) + quantum_register_controls_grid_layout.setColumnStretch(4, 2) + quantum_register_controls_grid_layout.setColumnStretch(5, 0) + + # simulation_run_scroll_area = QtWidgets.QScrollArea() + # simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) + # simulation_run_scroll_area.setWidgetResizable(True) + # main_layout.addWidget(simulation_run_scroll_area) + main_layout.addWidget(self.simulation_run_wrapper_box) + + def handle_quantum_register_name_search(self) -> None: + for qreg in self.annotatable_quantum_computation.qregs.values(): + if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( + self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ): + continue + + qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg.name) + ) + qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) + ) + ) + + if ( + qreg_name_label is None + or qreg_input_state_input_field is None + or qreg_output_state_input_field is None + or qreg_edit_qubit_values_toggle_button is None + ): + # TODO: This should not happen + continue + + should_control_be_visible: bool = ( + self.quantum_register_search_input_field.text() is None + or qreg.name.startswith(self.quantum_register_search_input_field.text()) + ) + qreg_name_label.setVisible(should_control_be_visible) + qreg_input_state_input_field.setVisible(should_control_be_visible) + qreg_output_state_input_field.setVisible(should_control_be_visible) + qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) + + # TODO: Update n_bit_values_container and parent textfield + def handle_input_state_qubit_value_checkbox_state_change( + self, + associated_qreg_name: str, + associated_qubit: int, + relative_qubit_index_in_quantum_register: int, + qubit_value: bool, + ) -> None: + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, + self.input_state_qubit_checkbox_name_format.format(qubit=associated_qubit), + ) + + qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=associated_qreg_name) + ) + + if associated_qubit_value_checkbox is None or qreg_input_state_input_field is None: + # TODO: This should not happen + return + + associated_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + ) + + curr_stringified_input_state: str = qreg_input_state_input_field.text() + qreg_input_state_input_field.setText( + curr_stringified_input_state[:relative_qubit_index_in_quantum_register] + + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") + + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] + ) + + # TODO: Update n_bit_values_container and parent textfield + def handle_output_state_qubit_value_checkbox_state_change( + self, + associated_qreg_name: str, + associated_qubit: int, + relative_qubit_index_in_quantum_register: int, + qubit_value: bool, + ) -> None: + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, + self.output_state_qubit_checkbox_name_format.format(qubit=associated_qubit), + ) + + qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=associated_qreg_name) + ) + + if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: + # TODO: This should not happen + return + + associated_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + ) + + curr_stringified_output_state: str = qreg_output_state_input_field.text() + qreg_output_state_input_field.setText( + curr_stringified_output_state[:relative_qubit_index_in_quantum_register] + + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") + + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] + ) + + @staticmethod + def stringify_qubit_value(qubit_value: bool | None) -> str: + if qubit_value is None: + return "UNKNOWN" + return "HIGH" if qubit_value else "LOW" + + def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: + for qreg in self.annotatable_quantum_computation.qregs.values(): + if ( + qreg.size == 0 + or does_qubit_label_start_with_internal_qubit_label_prefix( + self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ) + or qreg.name != associated_quantum_register_name + ): + continue + + qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + self.qreg_qubit_values_groupbox_format.format(qreg_name=associated_quantum_register_name), + ) + if qreg_qubits_groupbox is None: + # TODO: This should not happen + continue + + qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLineEdit, + self.qreg_qubit_search_input_field_name_format.format(qreg_name=associated_quantum_register_name), + ) + if qubit_search_input_field is None: + # TODO: This should not happen + continue + + for qubit in range(qreg.start, qreg.start + qreg.size): + qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, self.qubit_label_name_format.format(qubit=qubit) + ) + input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, self.input_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + if ( + qubit_value_label is None + or input_state_qubit_checkbox is None + or output_state_qubit_checkbox is None + ): + # TODO: This should not happen + continue + + does_qubit_label_match_search_text: bool = self.annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ).startswith(qubit_search_input_field.text()) + qubit_value_label.setVisible(does_qubit_label_match_search_text) + input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + + def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: + is_qubit_values_edit_enabled_for_any_qreg: bool = False + for qreg in self.annotatable_quantum_computation.qregs.values(): + if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( + self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + ): + continue + + # TODO: QtCore.Qt.FindDirectChildrenOnly + qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + ) + qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) + ) + qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) + ) + + if ( + qreg_input_state_input_field is None + or qreg_output_state_input_field is None + or qubit_values_groupbox is None + or qubit_values_toggle_button is None + ): + # TODO: This should not happen + continue + + if qreg.name == associated_qreg_name and not qubit_values_groupbox.isVisible(): + is_qubit_values_edit_enabled_for_any_qreg = True + qubit_values_groupbox.setVisible(True) + qubit_values_toggle_button.setText("Toggle qubit values edit") + qreg_input_state_input_field.setEnabled(False) + qreg_output_state_input_field.setEnabled(False) + else: + qubit_values_groupbox.setVisible(False) + qubit_values_toggle_button.setText("Edit qubit values") + qreg_input_state_input_field.setEnabled(not self.is_input_state_readonly) + qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 + + self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) + self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) + + def handle_simulation_run_deletion_button_click(self) -> None: + self.requested_simulation_run_deletion.emit(self.simulation_run_number) + + def handle_simulation_run_number_update(self, new_simulation_run_number: int) -> None: + self.simulation_run_number = new_simulation_run_number + self.simulation_run_wrapper_box.setText("Simulation run #" + str(self.simulation_run_number)) + + +class SimulationRunDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] + def __init__( + self, + simulation_run_idx: int, # noqa: ARG002 + annotatable_quantum_computation: syrec.annotatable_quantum_computation, # noqa: ARG002 + is_delete_action_enabled: bool, # noqa: ARG002 + parent: QtWidgets.QWidget, # noqa: ARG002 + ) -> None: + super().__init__() + + self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 05b4b2b0..62d28d72 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from typing import Final -from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6 import QtCore from mqt import syrec @@ -72,337 +72,6 @@ def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool return True -# Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html -class SimulationRunModelStyledItemDelegate(QtWidgets.QStyledItemDelegate): # type: ignore[misc] - def __init__(self, parent=None): - super().__init__(parent) - - # TODO: Mark as const: https://stackoverflow.com/a/57596202 - self.simulation_run_group_box_title_font_size: Final[int] = 14 - self.simulation_run_group_box_content_font_size: Final[int] = 10 - self.quantum_register_layout_info_text_font_size: Final[int] = 8 - self.stringified_quantum_register_y_spacing: Final[int] = 4 - self.stringified_quantum_register_x_spacing: Final[int] = 6 - self.simulation_run_contents_padding_size: Final[int] = 20 - self.simulation_run_group_box_y_spacing: Final[int] = 10 - - self.quantum_register_layout_text_format = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" - self.quantum_register_name_column_header = "Quantum register" - self.input_state_value_column_header = "INPUT" - self.output_state_value_column_header = "OUTPUT" - - @staticmethod - def _get_horizontal_text_width(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: - return int( - QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).horizontalAdvance( - text - ) - ) - - @staticmethod - def _get_vertical_text_width(options: QtWidgets.QStyleOptionsViewItem, font_size: int) -> int: - return int(QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height()) - - def _get_estimated_quantum_register_name_column_width( - self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int - ) -> int: - if not index.isValid(): - return 0 - - index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) - largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) - largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) - - return (2 * self.stringified_quantum_register_x_spacing) + max( - SimulationRunModelStyledItemDelegate._get_horizontal_text_width( - self.quantum_register_name_column_header, option, font_size - ), - SimulationRunModelStyledItemDelegate._get_horizontal_text_width( - index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option, font_size - ), - SimulationRunModelStyledItemDelegate._get_horizontal_text_width( - self.quantum_register_layout_text_format.format( - first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size - ), - option, - font_size, - ), - ) - - def _get_estimated_quantum_register_contents_column_width( - self, option: QtWidgets.QStyleOptionViewItem, font_size: int, with_leading_whitespace: bool - ) -> int: - return ( - 2 * self.stringified_quantum_register_x_spacing if with_leading_whitespace else 0 - ) + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( - "".join(["0" for i in range(32)]), option, font_size - ) - - # TODO: Group box header? - def _get_estimated_bounding_rect( - self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex - ) -> QtCore.QSize: - if not index.isValid(): - return QtCore.QSize(0, 0) - - n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) - simulation_run_content_height: int = ( - self.simulation_run_group_box_y_spacing - + self.simulation_run_contents_padding_size - + SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.simulation_run_group_box_title_font_size - ) - + self.stringified_quantum_register_y_spacing - + n_qregs - * ( - SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.quantum_register_layout_info_text_font_size - ) - + SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.simulation_run_group_box_content_font_size - ) - ) - + ( - (2 * (n_qregs - 1) * self.stringified_quantum_register_y_spacing) - if n_qregs > 1 - else self.stringified_quantum_register_y_spacing - ) - + self.simulation_run_contents_padding_size - + self.simulation_run_group_box_y_spacing - ) - - quantum_register_content_width: int = self._get_estimated_quantum_register_contents_column_width( - option, self.simulation_run_group_box_content_font_size, True - ) - simulation_run_content_width = ( - self.simulation_run_contents_padding_size - + self._get_estimated_quantum_register_name_column_width( - option, index, self.simulation_run_group_box_content_font_size - ) - + (2 * quantum_register_content_width) - + self.simulation_run_contents_padding_size - ) - return QtCore.QSize( - min(simulation_run_content_width, option.rect.bottomRight().x()), - max(simulation_run_content_height, option.rect.topRight().y()), - ) - - def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - return self._get_estimated_bounding_rect(option, index) - - @staticmethod - def _paint_rect_edge_points( - painter: QtGui.QPainter, rect: QtCore.QRect, font_size: int, color: QtGui.Color - ) -> None: - painter.save() - custom_pen = QtGui.QPen(color) - custom_pen.setWidth(font_size) - painter.setPen(custom_pen) - - painter.drawPoint(QtCore.QPoint(rect.topLeft())) - painter.drawPoint(QtCore.QPoint(rect.topRight())) - painter.drawPoint(QtCore.QPoint(rect.bottomLeft())) - painter.drawPoint(QtCore.QPoint(rect.bottomRight())) - painter.restore() - - @staticmethod - def _stringify_some_qubits_of_n_bit_values_container( - n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int - ) -> str: - if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): - return "" - - return "".join([ - "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) - ]) - - def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: - if not index.isValid(): - return - - associated_input_output_mapping: InputOutputStateMapping = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) - - painter.save() - estimated_simulation_run_container_size: QtCore.QSize = self._get_estimated_bounding_rect(option, index) - # simulation_run_container_rect = QtCore.QRect(option.rect.topLeft().x(), (index.row() * estimated_simulation_run_container_size.height()) + option.rect.topLeft().y(), estimated_simulation_run_container_size.width(), estimated_simulation_run_container_size.height()) - simulation_run_container_rect = QtCore.QRect( - option.rect.topLeft().x(), - option.rect.topLeft().y() + self.simulation_run_group_box_y_spacing, - estimated_simulation_run_container_size.width(), - estimated_simulation_run_container_size.height() - 2 * self.simulation_run_group_box_y_spacing, - ) - - if QtWidgets.QStyle.StateFlag.State_Selected in option.state: - # print(str(index.row()) + " selected!") - painter.fillRect(simulation_run_container_rect, option.palette.highlight()) - painter.setBrush(option.palette.highlightedText()) - - group_box_opt = QtWidgets.QStyleOptionGroupBox() - group_box_opt.rect = simulation_run_container_rect - group_box_opt.text = "Simulation run #" + str(index.row()) - group_box_opt.color = QtCore.Qt.GlobalColor.black - group_box_opt.textAlignment = QtCore.Qt.AlignmentFlag.AlignLeft - group_box_opt.subControls = ( - QtWidgets.QStyle.SubControl.SC_GroupBoxFrame | QtWidgets.QStyle.SubControl.SC_GroupBoxLabel - ) - group_box_opt.state = QtWidgets.QStyle.StateFlag.State_Raised - group_box_opt.features = QtWidgets.QStyleOptionFrame.FrameFeature.Rounded - - # 2. Draw the control using the current application style - # Using the widget's style ensures it respects OS themes - - # 3. Draw the GroupBox - app_style = QtWidgets.QApplication.style() - app_style.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_GroupBox, group_box_opt, painter) - - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, simulation_run_container_rect, 5, QtCore.Qt.GlobalColor.darkMagenta - ) - - # 3. Calculate where the content inside the box should go - # We use subControlRect to find where the frame actually is - # 4. Calculate Content Area - # This returns a rect relative to the group_box_opt.rect - relative_group_box_content_rect = app_style.subControlRect( - QtWidgets.QStyle.ComplexControl.CC_GroupBox, - group_box_opt, - QtWidgets.QStyle.SubControl.SC_GroupBoxContents, - None, - ) - # The calculation for the position of the contents of the group box does not seems to set the y-coordinate correctly while the x coordinate, width and height are correctly set? Return value of function call might be relative to parent? - relative_group_box_content_rect.setTop( - simulation_run_container_rect.top() + relative_group_box_content_rect.top() - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, relative_group_box_content_rect, 5, QtCore.Qt.GlobalColor.magenta - ) - - quantum_register_name_column_width: int = self._get_estimated_quantum_register_name_column_width( - option, index, self.simulation_run_group_box_content_font_size - ) - quantum_register_name_column_start_x: int = ( - relative_group_box_content_rect.topLeft().x() + self.simulation_run_contents_padding_size - ) - - initial_column_one_rect = QtCore.QRect( - quantum_register_name_column_start_x + self.simulation_run_contents_padding_size, - relative_group_box_content_rect.topLeft().y() + self.stringified_quantum_register_y_spacing, - quantum_register_name_column_width, - SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.simulation_run_group_box_content_font_size - ), - ) - painter.drawText( - initial_column_one_rect, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - "Quantum register", - ) - - quantum_register_content_width_without_padding: int = ( - self._get_estimated_quantum_register_contents_column_width( - option, self.simulation_run_group_box_content_font_size, False - ) - ) - quantum_register_input_values_start_x: int = ( - initial_column_one_rect.topRight().x() + self.stringified_quantum_register_x_spacing - ) - - initial_column_two_rect = QtCore.QRect( - quantum_register_input_values_start_x, - initial_column_one_rect.topLeft().y(), - quantum_register_content_width_without_padding, - initial_column_one_rect.height(), - ) - painter.drawText( - initial_column_two_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "INPUT" - ) - - quantum_register_output_values_start_x: int = ( - initial_column_two_rect.topRight().x() + self.stringified_quantum_register_x_spacing - ) - initial_column_three_rect = QtCore.QRect( - quantum_register_output_values_start_x, - initial_column_one_rect.topLeft().y(), - quantum_register_content_width_without_padding, - initial_column_two_rect.height(), - ) - painter.drawText( - initial_column_three_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "OUTPUT" - ) - - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, initial_column_one_rect, 5, QtCore.Qt.GlobalColor.red - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, initial_column_two_rect, 5, QtCore.Qt.GlobalColor.blue - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, initial_column_three_rect, 5, QtCore.Qt.GlobalColor.green - ) - - row_idx: int = 1 - row_i_y_offset: int = ( - self.stringified_quantum_register_y_spacing - + SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.simulation_run_group_box_content_font_size - ) - ) - for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): - curr_row_y_offset: int = row_idx * row_i_y_offset - - row_i_column_one = initial_column_one_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) - painter.drawText( - row_i_column_one, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - qreg_layout.quantum_register_name, - ) - - row_i_column_two = initial_column_two_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) - painter.drawText( - row_i_column_two, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.input_state, - qreg_layout.first_qubit_of_quantum_register, - qreg_layout.quantum_register_size, - ), - ) - - row_i_column_three = initial_column_three_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) - painter.drawText( - row_i_column_three, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.output_state, - qreg_layout.first_qubit_of_quantum_register, - qreg_layout.quantum_register_size, - ) - if associated_input_output_mapping.output_state is not None - else "", - ) - - painter.save() - quantum_layout_info_text_font = QtGui.QFont( - painter.font().family(), self.quantum_register_layout_info_text_font_size - ) - painter.setPen(QtCore.Qt.GlobalColor.gray) - painter.setFont(quantum_layout_info_text_font) - - row_i_plus_column_one = row_i_column_one.adjusted(0, row_i_y_offset, 0, row_i_y_offset) - painter.drawText( - row_i_plus_column_one, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - self.quantum_register_layout_text_format.format( - first_qubit=qreg_layout.first_qubit_of_quantum_register, n_qubits=qreg_layout.quantum_register_size - ), - ) - painter.restore() - - row_idx += 2 - painter.restore() - return - - # Example delegate: https://stackoverflow.com/questions/53105343/is-it-possible-to-add-a-custom-widget-into-a-qlistview class QtSimulationRunModel(QtCore.QAbstractListModel): # type: ignore[misc] def __init__( diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py new file mode 100644 index 00000000..332981b8 --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py @@ -0,0 +1,363 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore, QtGui, QtWidgets + +from .qt_simulation_run_model import ( + LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, + LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, + LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE, + QUANTUM_REGISTER_LAYOUT_QT_ROLE, + SIMULATION_RUN_IO_STATE_QT_ROLE, +) + +if TYPE_CHECKING: + from mqt import syrec + + from .qt_simulation_run_model import ( + InputOutputStateMapping, + ) + + +# Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html +class SimulationRunModelStyledItemDelegate(QtWidgets.QStyledItemDelegate): # type: ignore[misc] + def __init__(self, parent=None): + super().__init__(parent) + + # TODO: Mark as const: https://stackoverflow.com/a/57596202 + self.simulation_run_group_box_title_font_size: Final[int] = 14 + self.simulation_run_group_box_content_font_size: Final[int] = 10 + self.quantum_register_layout_info_text_font_size: Final[int] = 8 + self.stringified_quantum_register_y_spacing: Final[int] = 4 + self.stringified_quantum_register_x_spacing: Final[int] = 6 + self.simulation_run_contents_padding_size: Final[int] = 20 + self.simulation_run_group_box_y_spacing: Final[int] = 10 + + self.quantum_register_layout_text_format = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" + self.quantum_register_name_column_header = "Quantum register" + self.input_state_value_column_header = "INPUT" + self.output_state_value_column_header = "OUTPUT" + + @staticmethod + def _get_horizontal_text_width(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: + return int( + QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).horizontalAdvance( + text + ) + ) + + @staticmethod + def _get_vertical_text_width(options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: + return int(QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height()) + + def _get_estimated_quantum_register_name_column_width( + self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int + ) -> int: + if not index.isValid(): + return 0 + + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) + largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) + largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) + + return (2 * self.stringified_quantum_register_x_spacing) + max( + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + self.quantum_register_name_column_header, option, font_size + ), + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option, font_size + ), + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + self.quantum_register_layout_text_format.format( + first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size + ), + option, + font_size, + ), + ) + + def _get_estimated_quantum_register_contents_column_width( + self, option: QtWidgets.QStyleOptionViewItem, font_size: int, with_leading_whitespace: bool + ) -> int: + return ( + 2 * self.stringified_quantum_register_x_spacing if with_leading_whitespace else 0 + ) + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + "".join(["0" for i in range(32)]), option, font_size + ) + + # TODO: Group box header? + def _get_estimated_bounding_rect( + self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex + ) -> QtCore.QSize: + if not index.isValid(): + return QtCore.QSize(0, 0) + + n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + simulation_run_content_height: int = ( + self.simulation_run_group_box_y_spacing + + self.simulation_run_contents_padding_size + + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_title_font_size + ) + + self.stringified_quantum_register_y_spacing + + n_qregs + * ( + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.quantum_register_layout_info_text_font_size + ) + + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_content_font_size + ) + ) + + ( + (2 * (n_qregs - 1) * self.stringified_quantum_register_y_spacing) + if n_qregs > 1 + else self.stringified_quantum_register_y_spacing + ) + + self.simulation_run_contents_padding_size + + self.simulation_run_group_box_y_spacing + ) + + quantum_register_content_width: int = self._get_estimated_quantum_register_contents_column_width( + option, self.simulation_run_group_box_content_font_size, True + ) + simulation_run_content_width = ( + self.simulation_run_contents_padding_size + + self._get_estimated_quantum_register_name_column_width( + option, index, self.simulation_run_group_box_content_font_size + ) + + (2 * quantum_register_content_width) + + self.simulation_run_contents_padding_size + ) + return QtCore.QSize( + min(simulation_run_content_width, option.rect.bottomRight().x()), + max(simulation_run_content_height, option.rect.topRight().y()), + ) + + def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 + return self._get_estimated_bounding_rect(option, index) + + @staticmethod + def _paint_rect_edge_points( + painter: QtGui.QPainter, rect: QtCore.QRect, font_size: int, color: QtGui.QColor, index: QtCore.QModelIndex + ) -> None: + painter.save() + custom_pen = QtGui.QPen(color) + custom_pen.setWidth(font_size) + painter.setPen(custom_pen) + + painter.drawPoint(QtCore.QPoint(rect.topLeft())) + painter.drawText(rect.topLeft().x(), rect.topLeft().y(), str(index.row()) + "-TL") + painter.drawPoint(QtCore.QPoint(rect.topRight())) + painter.drawText(rect.topRight().x(), rect.topRight().y(), str(index.row()) + "-TR") + painter.drawPoint(QtCore.QPoint(rect.bottomLeft())) + painter.drawText(rect.bottomLeft().x(), rect.bottomLeft().y(), str(index.row()) + "-BL") + painter.drawPoint(QtCore.QPoint(rect.bottomRight())) + painter.drawText(rect.bottomRight().x(), rect.bottomRight().y(), str(index.row()) + "-BR") + painter.restore() + + @staticmethod + def _stringify_some_qubits_of_n_bit_values_container( + n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int + ) -> str: + if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): + return "" + + return "".join([ + "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) + ]) + + def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: + if not index.isValid(): + return + + associated_input_output_mapping: InputOutputStateMapping = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + + painter.save() + estimated_simulation_run_container_size: QtCore.QSize = self._get_estimated_bounding_rect(option, index) + # simulation_run_container_rect = QtCore.QRect(option.rect.topLeft().x(), (index.row() * estimated_simulation_run_container_size.height()) + option.rect.topLeft().y(), estimated_simulation_run_container_size.width(), estimated_simulation_run_container_size.height()) + simulation_run_container_rect = QtCore.QRect( + option.rect.topLeft().x(), + option.rect.topLeft().y() + self.simulation_run_group_box_y_spacing, + estimated_simulation_run_container_size.width(), + estimated_simulation_run_container_size.height() - 2 * self.simulation_run_group_box_y_spacing, + ) + + if QtWidgets.QStyle.StateFlag.State_Selected in option.state: + # print(str(index.row()) + " selected!") + painter.fillRect(simulation_run_container_rect, option.palette.highlight()) + painter.setBrush(option.palette.highlightedText()) + + group_box_opt = QtWidgets.QStyleOptionGroupBox() + group_box_opt.rect = simulation_run_container_rect + group_box_opt.text = "Simulation run #" + str(index.row()) + group_box_opt.color = QtCore.Qt.GlobalColor.black + group_box_opt.textAlignment = QtCore.Qt.AlignmentFlag.AlignLeft + group_box_opt.subControls = ( + QtWidgets.QStyle.SubControl.SC_GroupBoxFrame | QtWidgets.QStyle.SubControl.SC_GroupBoxLabel + ) + group_box_opt.state = QtWidgets.QStyle.StateFlag.State_Raised + group_box_opt.features = QtWidgets.QStyleOptionFrame.FrameFeature.Rounded + + # 2. Draw the control using the current application style + # Using the widget's style ensures it respects OS themes + + # 3. Draw the GroupBox + app_style = QtWidgets.QApplication.style() + app_style.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_GroupBox, group_box_opt, painter) + + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, simulation_run_container_rect, 5, QtCore.Qt.GlobalColor.darkMagenta, index + ) + + # 3. Calculate where the content inside the box should go + # We use subControlRect to find where the frame actually is + # 4. Calculate Content Area + # This returns a rect relative to the group_box_opt.rect + relative_group_box_content_rect = app_style.subControlRect( + QtWidgets.QStyle.ComplexControl.CC_GroupBox, + group_box_opt, + QtWidgets.QStyle.SubControl.SC_GroupBoxContents, + None, + ) + # The calculation for the position of the contents of the group box does not seems to set the y-coordinate correctly while the x coordinate, width and height are correctly set? Return value of function call might be relative to parent? + relative_group_box_content_rect.setTop( + simulation_run_container_rect.top() + relative_group_box_content_rect.top() + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, relative_group_box_content_rect, 5, QtCore.Qt.GlobalColor.magenta, index + ) + + quantum_register_name_column_width: int = self._get_estimated_quantum_register_name_column_width( + option, index, self.simulation_run_group_box_content_font_size + ) + quantum_register_name_column_start_x: int = ( + relative_group_box_content_rect.topLeft().x() + self.simulation_run_contents_padding_size + ) + + initial_column_one_rect = QtCore.QRect( + quantum_register_name_column_start_x + self.simulation_run_contents_padding_size, + relative_group_box_content_rect.topLeft().y() + self.stringified_quantum_register_y_spacing, + quantum_register_name_column_width, + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_content_font_size + ), + ) + painter.drawText( + initial_column_one_rect, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + "Quantum register", + ) + + quantum_register_content_width_without_padding: int = ( + self._get_estimated_quantum_register_contents_column_width( + option, self.simulation_run_group_box_content_font_size, False + ) + ) + quantum_register_input_values_start_x: int = ( + initial_column_one_rect.topRight().x() + self.stringified_quantum_register_x_spacing + ) + + initial_column_two_rect = QtCore.QRect( + quantum_register_input_values_start_x, + initial_column_one_rect.topLeft().y(), + quantum_register_content_width_without_padding, + initial_column_one_rect.height(), + ) + painter.drawText( + initial_column_two_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "INPUT" + ) + + quantum_register_output_values_start_x: int = ( + initial_column_two_rect.topRight().x() + self.stringified_quantum_register_x_spacing + ) + initial_column_three_rect = QtCore.QRect( + quantum_register_output_values_start_x, + initial_column_one_rect.topLeft().y(), + quantum_register_content_width_without_padding, + initial_column_two_rect.height(), + ) + painter.drawText( + initial_column_three_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "OUTPUT" + ) + + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, initial_column_one_rect, 5, QtCore.Qt.GlobalColor.red, index + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, initial_column_two_rect, 5, QtCore.Qt.GlobalColor.blue, index + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, initial_column_three_rect, 5, QtCore.Qt.GlobalColor.green, index + ) + + row_idx: int = 1 + row_i_y_offset: int = ( + self.stringified_quantum_register_y_spacing + + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + option, self.simulation_run_group_box_content_font_size + ) + ) + for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): + curr_row_y_offset: int = row_idx * row_i_y_offset + + row_i_column_one = initial_column_one_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + painter.drawText( + row_i_column_one, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + qreg_layout.quantum_register_name, + ) + + row_i_column_two = initial_column_two_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + painter.drawText( + row_i_column_two, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.input_state, + qreg_layout.first_qubit_of_quantum_register, + qreg_layout.quantum_register_size, + ), + ) + + row_i_column_three = initial_column_three_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + painter.drawText( + row_i_column_three, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.output_state, + qreg_layout.first_qubit_of_quantum_register, + qreg_layout.quantum_register_size, + ) + if associated_input_output_mapping.output_state is not None + else "", + ) + + painter.save() + quantum_layout_info_text_font = QtGui.QFont( + painter.font().family(), self.quantum_register_layout_info_text_font_size + ) + painter.setPen(QtCore.Qt.GlobalColor.gray) + painter.setFont(quantum_layout_info_text_font) + + row_i_plus_column_one = row_i_column_one.adjusted(0, row_i_y_offset, 0, row_i_y_offset) + painter.drawText( + row_i_plus_column_one, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + self.quantum_register_layout_text_format.format( + first_qubit=qreg_layout.first_qubit_of_quantum_register, n_qubits=qreg_layout.quantum_register_size + ), + ) + painter.restore() + + row_idx += 2 + painter.restore() + return From 45e6a910afc0a6f46c387c526722616c2dc96bd0 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 4 Jan 2026 21:45:07 +0100 Subject: [PATCH 09/88] Add logic for simulation run list controls as well as for simulation runs execution controls --- .../quantum_circuit_simulation_dialog.py | 152 +++++++++++++++--- .../qt_simulation_run_model.py | 11 +- 2 files changed, 138 insertions(+), 25 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 4ec9109f..61277996 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -10,20 +10,21 @@ from typing import Final -from PyQt6 import QtWidgets +from PyQt6 import QtCore, QtWidgets from mqt import syrec from .simulation_view.qt_simulation_run_model import InputOutputStateMapping, QtSimulationRunModel from .simulation_view.qt_simulation_run_styled_item_delegate import SimulationRunModelStyledItemDelegate -LOADED_FROM_FILE_INPUT_FIELD_NAME = "load_from_file_input_field" -ADD_SIM_RUN_BTN_NAME = "add_sim_run_btn" -EDIT_SIM_RUN_BTN_NAME = "edit_sim_run_btn" -DELETE_SIM_RUN_BTN_NAME = "delete_sim_run_btn" -SAVE_SIM_RUNS_TO_FILE_BTN_NAME = "save_sims_to_file_btn" -RUN_SIM_RUNS_BTN_NAME = "run_sims_btn" -RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME = "run_sims_stop_first_failure_btn" +LOADED_FROM_FILE_INPUT_FIELD_NAME: Final[str] = "load_from_file_input_field" +ADD_SIM_RUN_BTN_NAME: Final[str] = "add_sim_run_btn" +EDIT_SIM_RUN_BTN_NAME: Final[str] = "edit_sim_run_btn" +DELETE_SIM_RUN_BTN_NAME: Final[str] = "delete_sim_run_btn" +SAVE_SIM_RUNS_TO_FILE_BTN_NAME: Final[str] = "save_sims_to_file_btn" +RUN_SIM_RUNS_BTN_NAME: Final[str] = "run_sims_btn" +RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME: Final[str] = "run_sims_stop_first_failure_btn" +SIMULATION_RUNS_LIST_VIEW_NAME: Final[str] = "sim_runs_list_view" class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] @@ -43,22 +44,21 @@ def __init__( self.height = 800 self.setGeometry(self.left, self.top, self.width, self.height) + self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self.simulation_runs_tab_widget.addTab( - QuantumCircuitSimulationDialog.initialize_simulation_runs_tab_widget(self.simulation_runs_model), + self.initialize_simulation_runs_tab_widget(self.simulation_runs_model), "Check some input-output mapping combinations", ) self.simulation_runs_tab_widget.addTab( - QuantumCircuitSimulationDialog.initialize_simulation_runs_tab_widget(self.simulation_runs_model), + self.initialize_simulation_runs_tab_widget(self.simulation_runs_model), "Check all input-output mapping combinations", ) self.simulation_runs_tab_widget.addTab( - QuantumCircuitSimulationDialog.initialize_simulation_runs_tab_widget( - self.simulation_runs_model, create_load_from_file_controls=True - ), + self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, create_load_from_file_controls=True), "Check input-output mapping combinations from file", ) self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) @@ -72,9 +72,9 @@ def __init__( self.setLayout(self.layout) # TODO: Load from file controls - @staticmethod + def initialize_simulation_runs_tab_widget( - shared_simulation_runs_model: QtSimulationRunModel, create_load_from_file_controls: bool = False + self, shared_simulation_runs_model: QtSimulationRunModel, create_load_from_file_controls: bool = False ) -> QtWidgets.QWidget: tab_wrapper_widget = QtWidgets.QFrame() tab_wrapper_widget_layout = QtWidgets.QVBoxLayout() @@ -88,13 +88,14 @@ def initialize_simulation_runs_tab_widget( tab_wrapper_widget_layout.addSpacing(manual_y_space_size) # BEGIN: Create simulation runs list view Qt elements - simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() + simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView(objectName=SIMULATION_RUNS_LIST_VIEW_NAME) simulation_runs_list_view.setModel(shared_simulation_runs_model) simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] simulation_runs_list_view.setUniformItemSizes(True) simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) simulation_runs_list_scrollarea = QtWidgets.QScrollArea() simulation_runs_list_scrollarea.setAutoFillBackground(True) @@ -107,14 +108,20 @@ def initialize_simulation_runs_tab_widget( simulation_runs_list_modification_buttons_layout = QtWidgets.QHBoxLayout() simulation_runs_list_modification_buttons_layout.addStretch() add_simulation_run_button = QtWidgets.QPushButton("Add simulation run", objectName=ADD_SIM_RUN_BTN_NAME) + add_simulation_run_button.setEnabled(True) + add_simulation_run_button.clicked.connect(self.handle_simulation_run_add_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(add_simulation_run_button) edit_simulation_run_button = QtWidgets.QPushButton("Edit simulation run", objectName=EDIT_SIM_RUN_BTN_NAME) + edit_simulation_run_button.setEnabled(False) + edit_simulation_run_button.clicked.connect(QuantumCircuitSimulationDialog.handle_simulation_run_edit_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(edit_simulation_run_button) delete_simulation_run_button = QtWidgets.QPushButton( "Delete simulation run", objectName=DELETE_SIM_RUN_BTN_NAME ) + delete_simulation_run_button.setEnabled(False) + delete_simulation_run_button.clicked.connect(self.handle_simulation_run_delete_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(delete_simulation_run_button) simulation_runs_list_modification_buttons_layout.addStretch() @@ -126,16 +133,19 @@ def initialize_simulation_runs_tab_widget( simulation_runs_execution_buttons_layout.addStretch() save_simulation_runs_to_file_button = QtWidgets.QPushButton( - "Save simulation runs to file", objectName="SAVE_SIM_RUNS_TO_FILE_BTN_NAME" + "Save simulation runs to file", objectName=SAVE_SIM_RUNS_TO_FILE_BTN_NAME ) + save_simulation_runs_to_file_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) - run_simulation_runs_button = QtWidgets.QPushButton("Run simulation runs", objectName="RUN_SIM_RUNS_BTN_NAME") + run_simulation_runs_button = QtWidgets.QPushButton("Run simulation runs", objectName=RUN_SIM_RUNS_BTN_NAME) + run_simulation_runs_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_button) run_simulation_runs_stop_at_first_failure_button = QtWidgets.QPushButton( - "Run simulation runs (stop at first failure)", objectName="RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME" + "Run simulation runs (stop at first failure)", objectName=RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME ) + run_simulation_runs_stop_at_first_failure_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_stop_at_first_failure_button) simulation_runs_execution_buttons_layout.addStretch() @@ -145,6 +155,79 @@ def initialize_simulation_runs_tab_widget( # END: Create simulation runs execution Qt elements return tab_wrapper_widget + def handle_simulation_run_selection_change( + self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection + ) -> None: + if selected.isEmpty() == deselected.isEmpty(): + return + + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + return + + is_list_item_selected: bool = not selected.isEmpty() and deselected.isEmpty() + + add_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME + ) + edit_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, EDIT_SIM_RUN_BTN_NAME + ) + delete_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, DELETE_SIM_RUN_BTN_NAME + ) + + if add_simulation_run_btn is None or edit_simulation_run_btn is None or delete_simulation_run_btn is None: + return + + add_simulation_run_btn.setEnabled(not is_list_item_selected) + edit_simulation_run_btn.setEnabled(is_list_item_selected) + delete_simulation_run_btn.setEnabled(is_list_item_selected) + + def handle_simulation_run_add_btn_click(self) -> None: + if not self.simulation_runs_model.add_simulation_run( + InputOutputStateMapping( + input_state=syrec.n_bit_values_container(self.expected_input_output_state_size), output_state=None + ) + ): + return + + self.set_enabled_state_of_simulation_runs_execution_controls(True) + + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + return + + simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( + QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME + ) + if simulation_runs_list_view is None: + return + + simulation_runs_list_view.scrollToBottom() + + @staticmethod + def handle_simulation_run_edit_btn_click() -> None: + return + + def handle_simulation_run_delete_btn_click(self) -> None: + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + return + + simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( + QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME + ) + if simulation_runs_list_view is None: + return + + if not self.simulation_runs_model.delete_simulation_run(simulation_runs_list_view.currentIndex()): + return + + self.set_enabled_state_of_simulation_runs_execution_controls( + self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0 + ) + @staticmethod def initialize_load_simulation_runs_from_file_controls() -> QtWidgets.QLayout: controls_layout = QtWidgets.QHBoxLayout() @@ -172,8 +255,8 @@ def generate_some_simulation_runs( shared_simulation_runs_model: QtSimulationRunModel, ) -> None: for i in range(self): - in_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_qubits) - out_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_qubits) + in_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) + out_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) in_out_state_mapping: InputOutputStateMapping | None = None if i < 2: @@ -185,6 +268,33 @@ def generate_some_simulation_runs( def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) + self.set_enabled_state_of_simulation_runs_execution_controls(False) + + def set_enabled_state_of_simulation_runs_execution_controls(self, should_controls_be_enabled: bool) -> None: + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + return + + run_simulation_runs_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_NAME + ) + run_simulation_runs_stop_at_first_failure_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME + ) + save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, SAVE_SIM_RUNS_TO_FILE_BTN_NAME + ) + + if ( + run_simulation_runs_btn is None + or run_simulation_runs_stop_at_first_failure_btn is None + or save_simulation_runs_to_file_btn is None + ): + return + + run_simulation_runs_btn.setEnabled(should_controls_be_enabled) + run_simulation_runs_stop_at_first_failure_btn.setEnabled(should_controls_be_enabled) + save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) def handle_simulation_run_input_state_qubit_value_change( self, simulation_run_number: int, qubit: int, new_qubit_value: bool diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 62d28d72..51444bf5 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -152,13 +152,16 @@ def add_simulation_run(self, input_output_state_mapping: InputOutputStateMapping return True def delete_simulation_run(self, index: QtCore.QModelIndex) -> bool: - # self.beginRemoveRows() + self.beginRemoveRows(QtCore.QModelIndex(), index.row(), index.row()) + if self.is_model_index_valid(index): - self.input_output_state_mappings.remove(index.row()) - self.layoutChanged.emit() + self.input_output_state_mappings.pop(index.row()) + self.endRemoveRows() + # self.layoutChanged.emit() return True + + self.endRemoveRows() return False - # self.endRemoveRows() # TODO: Check for duplicates? def update_input_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: From e41054a177d839a981de0d597e71871dbe0490f1 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 4 Jan 2026 22:38:29 +0100 Subject: [PATCH 10/88] Added icons to buttons of simulation run overview dialog --- .../quantum_circuit_simulation_dialog.py | 38 ++++++++++++++----- .../qt_simulation_run_styled_item_delegate.py | 20 +++++++--- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 61277996..51c566a8 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -10,7 +10,7 @@ from typing import Final -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec @@ -63,8 +63,9 @@ def __init__( ) self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) + n_simulation_runs_to_add: Final[int] = 5 QuantumCircuitSimulationDialog.generate_some_simulation_runs( - 20, self.annotatable_quantum_computation, self.simulation_runs_model + n_simulation_runs_to_add, self.annotatable_quantum_computation, self.simulation_runs_model ) self.layout = QtWidgets.QVBoxLayout() @@ -107,18 +108,27 @@ def initialize_simulation_runs_tab_widget( # BEGIN: Create simulation runs list modification Qt elements simulation_runs_list_modification_buttons_layout = QtWidgets.QHBoxLayout() simulation_runs_list_modification_buttons_layout.addStretch() - add_simulation_run_button = QtWidgets.QPushButton("Add simulation run", objectName=ADD_SIM_RUN_BTN_NAME) + + add_simulation_run_button = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.ListAdd), "Add simulation run", objectName=ADD_SIM_RUN_BTN_NAME + ) add_simulation_run_button.setEnabled(True) add_simulation_run_button.clicked.connect(self.handle_simulation_run_add_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(add_simulation_run_button) - edit_simulation_run_button = QtWidgets.QPushButton("Edit simulation run", objectName=EDIT_SIM_RUN_BTN_NAME) + edit_simulation_run_button = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.DocumentProperties), + "Edit simulation run", + objectName=EDIT_SIM_RUN_BTN_NAME, + ) edit_simulation_run_button.setEnabled(False) edit_simulation_run_button.clicked.connect(QuantumCircuitSimulationDialog.handle_simulation_run_edit_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(edit_simulation_run_button) delete_simulation_run_button = QtWidgets.QPushButton( - "Delete simulation run", objectName=DELETE_SIM_RUN_BTN_NAME + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.EditDelete), + "Delete simulation run", + objectName=DELETE_SIM_RUN_BTN_NAME, ) delete_simulation_run_button.setEnabled(False) delete_simulation_run_button.clicked.connect(self.handle_simulation_run_delete_btn_click) @@ -133,17 +143,25 @@ def initialize_simulation_runs_tab_widget( simulation_runs_execution_buttons_layout.addStretch() save_simulation_runs_to_file_button = QtWidgets.QPushButton( - "Save simulation runs to file", objectName=SAVE_SIM_RUNS_TO_FILE_BTN_NAME + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.DocumentSave), + "Save simulation runs to file", + objectName=SAVE_SIM_RUNS_TO_FILE_BTN_NAME, ) save_simulation_runs_to_file_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) - run_simulation_runs_button = QtWidgets.QPushButton("Run simulation runs", objectName=RUN_SIM_RUNS_BTN_NAME) + run_simulation_runs_button = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart), + "Run simulation runs", + objectName=RUN_SIM_RUNS_BTN_NAME, + ) run_simulation_runs_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_button) run_simulation_runs_stop_at_first_failure_button = QtWidgets.QPushButton( - "Run simulation runs (stop at first failure)", objectName=RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart), + "Run simulation runs (stop at first failure)", + objectName=RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME, ) run_simulation_runs_stop_at_first_failure_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_stop_at_first_failure_button) @@ -243,7 +261,9 @@ def initialize_load_simulation_runs_from_file_controls() -> QtWidgets.QLayout: open_file_dialog_button = QtWidgets.QPushButton("Select file...") controls_layout.addWidget(open_file_dialog_button) - trigger_load_from_file_button = QtWidgets.QPushButton("Load from file") + trigger_load_from_file_button = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.DocumentOpen), "Load from file" + ) controls_layout.addWidget(trigger_load_from_file_button) controls_layout.addStretch() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py index 332981b8..a4ae07c6 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py @@ -86,15 +86,19 @@ def _get_estimated_quantum_register_name_column_width( ) def _get_estimated_quantum_register_contents_column_width( - self, option: QtWidgets.QStyleOptionViewItem, font_size: int, with_leading_whitespace: bool + self, + option: QtWidgets.QStyleOptionViewItem, + largest_quantum_register_size_in_qubits: int, + font_size: int, + with_leading_whitespace: bool, ) -> int: return ( 2 * self.stringified_quantum_register_x_spacing if with_leading_whitespace else 0 ) + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( - "".join(["0" for i in range(32)]), option, font_size + "".join(["0" for i in range(largest_quantum_register_size_in_qubits)]), option, font_size ) - # TODO: Group box header? + # TODO: Long quantum registers that cause the total width to be larger than the containing bounding rect should be truncated (i.e. with a text ellipsis) with total estimated content width truncated to max. width of containing bounding rectangle? def _get_estimated_bounding_rect( self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex ) -> QtCore.QSize: @@ -128,7 +132,10 @@ def _get_estimated_bounding_rect( ) quantum_register_content_width: int = self._get_estimated_quantum_register_contents_column_width( - option, self.simulation_run_group_box_content_font_size, True + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + self.simulation_run_group_box_content_font_size, + True, ) simulation_run_content_width = ( self.simulation_run_contents_padding_size @@ -260,7 +267,10 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, quantum_register_content_width_without_padding: int = ( self._get_estimated_quantum_register_contents_column_width( - option, self.simulation_run_group_box_content_font_size, False + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + self.simulation_run_group_box_content_font_size, + False, ) ) quantum_register_input_values_start_x: int = ( From 74f39cd04bff61633136c557407ef62304314bf0 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Mon, 5 Jan 2026 16:26:56 +0100 Subject: [PATCH 11/88] Fixed height calculation for simulation run cards --- .../quantum_circuit_simulation_dialog.py | 6 +- .../qt_simulation_run_model.py | 2 +- .../qt_simulation_run_styled_item_delegate.py | 247 +++++++++--------- 3 files changed, 132 insertions(+), 123 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 51c566a8..8076bade 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -63,7 +63,7 @@ def __init__( ) self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) - n_simulation_runs_to_add: Final[int] = 5 + n_simulation_runs_to_add: Final[int] = 10 QuantumCircuitSimulationDialog.generate_some_simulation_runs( n_simulation_runs_to_add, self.annotatable_quantum_computation, self.simulation_runs_model ) @@ -93,13 +93,15 @@ def initialize_simulation_runs_tab_widget( simulation_runs_list_view.setModel(shared_simulation_runs_model) simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] simulation_runs_list_view.setUniformItemSizes(True) + simulation_runs_list_view.setAutoFillBackground(False) + simulation_runs_list_view.setSpacing(5) simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) simulation_runs_list_scrollarea = QtWidgets.QScrollArea() - simulation_runs_list_scrollarea.setAutoFillBackground(True) + simulation_runs_list_scrollarea.setAutoFillBackground(False) simulation_runs_list_scrollarea.setWidget(simulation_runs_list_view) simulation_runs_list_scrollarea.setWidgetResizable(True) tab_wrapper_widget_layout.addWidget(simulation_runs_list_scrollarea) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 51444bf5..b1aabff1 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -36,7 +36,7 @@ LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: Final[int] = LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE + 1 -@dataclass +@dataclass(frozen=True) class QuantumRegisterLayout: quantum_register_name: str first_qubit_of_quantum_register: int diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py index a4ae07c6..aeb0e207 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py @@ -48,7 +48,7 @@ def __init__(self, parent=None): self.output_state_value_column_header = "OUTPUT" @staticmethod - def _get_horizontal_text_width(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: + def _get_text_width_for_font_size(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: return int( QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).horizontalAdvance( text @@ -56,7 +56,7 @@ def _get_horizontal_text_width(text: str, options: QtWidgets.QStyleOptionViewIte ) @staticmethod - def _get_vertical_text_width(options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: + def _get_text_height_for_font_size(options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: return int(QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height()) def _get_estimated_quantum_register_name_column_width( @@ -70,13 +70,13 @@ def _get_estimated_quantum_register_name_column_width( largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) return (2 * self.stringified_quantum_register_x_spacing) + max( - SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( self.quantum_register_name_column_header, option, font_size ), - SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option, font_size ), - SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( self.quantum_register_layout_text_format.format( first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size ), @@ -94,7 +94,7 @@ def _get_estimated_quantum_register_contents_column_width( ) -> int: return ( 2 * self.stringified_quantum_register_x_spacing if with_leading_whitespace else 0 - ) + SimulationRunModelStyledItemDelegate._get_horizontal_text_width( + ) + SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( "".join(["0" for i in range(largest_quantum_register_size_in_qubits)]), option, font_size ) @@ -106,48 +106,62 @@ def _get_estimated_bounding_rect( return QtCore.QSize(0, 0) n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) - simulation_run_content_height: int = ( - self.simulation_run_group_box_y_spacing - + self.simulation_run_contents_padding_size - + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + # Quantum register contents are displayed as two rows containing the following information: + # R0: + # R1: + group_box_title_height: int = ( + self.stringified_quantum_register_y_spacing + + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_title_font_size ) - + self.stringified_quantum_register_y_spacing - + n_qregs - * ( - SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.quantum_register_layout_info_text_font_size - ) - + SimulationRunModelStyledItemDelegate._get_vertical_text_width( - option, self.simulation_run_group_box_content_font_size - ) + ) + + qreg_contents_text_height: int = ( + self.stringified_quantum_register_y_spacing + + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + option, self.simulation_run_group_box_content_font_size ) - + ( - (2 * (n_qregs - 1) * self.stringified_quantum_register_y_spacing) - if n_qregs > 1 - else self.stringified_quantum_register_y_spacing + + self.stringified_quantum_register_y_spacing + + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + option, self.quantum_register_layout_info_text_font_size ) + ) + total_qreg_contents_text_height: int = n_qregs * qreg_contents_text_height + total_simulation_run_group_box_height = ( + self.simulation_run_contents_padding_size + + group_box_title_height + + self.stringified_quantum_register_y_spacing + + total_qreg_contents_text_height + self.simulation_run_contents_padding_size - + self.simulation_run_group_box_y_spacing ) - quantum_register_content_width: int = self._get_estimated_quantum_register_contents_column_width( + qreg_name_and_layout_info_column_width: int = self._get_estimated_quantum_register_name_column_width( + option, index, self.simulation_run_group_box_title_font_size + ) + + max_qreg_content_column_width: int = self._get_estimated_quantum_register_contents_column_width( option, index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), self.simulation_run_group_box_content_font_size, True, ) - simulation_run_content_width = ( + max_per_qreg_content_column_width_with_spacing: int = ( + self.stringified_quantum_register_x_spacing + + max_qreg_content_column_width + + self.stringified_quantum_register_x_spacing + + max_qreg_content_column_width + ) + + total_simulation_run_group_box_width = ( self.simulation_run_contents_padding_size - + self._get_estimated_quantum_register_name_column_width( - option, index, self.simulation_run_group_box_content_font_size - ) - + (2 * quantum_register_content_width) + + qreg_name_and_layout_info_column_width + + max_per_qreg_content_column_width_with_spacing + + max_per_qreg_content_column_width_with_spacing + self.simulation_run_contents_padding_size ) return QtCore.QSize( - min(simulation_run_content_width, option.rect.bottomRight().x()), - max(simulation_run_content_height, option.rect.topRight().y()), + min(total_simulation_run_group_box_width, option.rect.bottomRight().x()), + max(total_simulation_run_group_box_height, option.rect.topRight().y()), ) def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 @@ -190,144 +204,137 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, associated_input_output_mapping: InputOutputStateMapping = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) painter.save() - estimated_simulation_run_container_size: QtCore.QSize = self._get_estimated_bounding_rect(option, index) - # simulation_run_container_rect = QtCore.QRect(option.rect.topLeft().x(), (index.row() * estimated_simulation_run_container_size.height()) + option.rect.topLeft().y(), estimated_simulation_run_container_size.width(), estimated_simulation_run_container_size.height()) - simulation_run_container_rect = QtCore.QRect( - option.rect.topLeft().x(), - option.rect.topLeft().y() + self.simulation_run_group_box_y_spacing, - estimated_simulation_run_container_size.width(), - estimated_simulation_run_container_size.height() - 2 * self.simulation_run_group_box_y_spacing, + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, option.rect, 5, QtCore.Qt.GlobalColor.cyan, index ) + painter.drawRoundedRect(option.rect, 3, 3) - if QtWidgets.QStyle.StateFlag.State_Selected in option.state: - # print(str(index.row()) + " selected!") - painter.fillRect(simulation_run_container_rect, option.palette.highlight()) - painter.setBrush(option.palette.highlightedText()) - - group_box_opt = QtWidgets.QStyleOptionGroupBox() - group_box_opt.rect = simulation_run_container_rect - group_box_opt.text = "Simulation run #" + str(index.row()) - group_box_opt.color = QtCore.Qt.GlobalColor.black - group_box_opt.textAlignment = QtCore.Qt.AlignmentFlag.AlignLeft - group_box_opt.subControls = ( - QtWidgets.QStyle.SubControl.SC_GroupBoxFrame | QtWidgets.QStyle.SubControl.SC_GroupBoxLabel + simulation_run_contents_rect = option.rect.adjusted( + self.simulation_run_contents_padding_size, + self.simulation_run_contents_padding_size, + -self.simulation_run_contents_padding_size, + -self.simulation_run_contents_padding_size, + ) + SimulationRunModelStyledItemDelegate._paint_rect_edge_points( + painter, simulation_run_contents_rect, 5, QtCore.Qt.GlobalColor.red, index ) - group_box_opt.state = QtWidgets.QStyle.StateFlag.State_Raised - group_box_opt.features = QtWidgets.QStyleOptionFrame.FrameFeature.Rounded - # 2. Draw the control using the current application style - # Using the widget's style ensures it respects OS themes + if QtWidgets.QStyle.StateFlag.State_Selected in option.state: + painter.fillRect(option.rect, option.palette.highlight()) + painter.setBrush(option.palette.highlightedText()) - # 3. Draw the GroupBox - app_style = QtWidgets.QApplication.style() - app_style.drawComplexControl(QtWidgets.QStyle.ComplexControl.CC_GroupBox, group_box_opt, painter) + painter.save() + header_font = QtGui.QFont(painter.font().family(), self.simulation_run_group_box_title_font_size) + header_font.setBold(True) + painter.setFont(header_font) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, simulation_run_container_rect, 5, QtCore.Qt.GlobalColor.darkMagenta, index + header_title = "Simulation run #" + str(index.row() + 1) + header_title_height: int = SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + option, self.simulation_run_group_box_title_font_size ) + painter.drawText(simulation_run_contents_rect.x(), simulation_run_contents_rect.y(), header_title) + painter.restore() - # 3. Calculate where the content inside the box should go - # We use subControlRect to find where the frame actually is - # 4. Calculate Content Area - # This returns a rect relative to the group_box_opt.rect - relative_group_box_content_rect = app_style.subControlRect( - QtWidgets.QStyle.ComplexControl.CC_GroupBox, - group_box_opt, - QtWidgets.QStyle.SubControl.SC_GroupBoxContents, - None, - ) - # The calculation for the position of the contents of the group box does not seems to set the y-coordinate correctly while the x coordinate, width and height are correctly set? Return value of function call might be relative to parent? - relative_group_box_content_rect.setTop( - simulation_run_container_rect.top() + relative_group_box_content_rect.top() - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, relative_group_box_content_rect, 5, QtCore.Qt.GlobalColor.magenta, index + header_text_rect = QtCore.QRect( + simulation_run_contents_rect.topLeft().x(), + simulation_run_contents_rect.topLeft().y(), + 0, + header_title_height + self.stringified_quantum_register_y_spacing, ) - quantum_register_name_column_width: int = self._get_estimated_quantum_register_name_column_width( + qreg_name_column_width: int = self._get_estimated_quantum_register_name_column_width( option, index, self.simulation_run_group_box_content_font_size ) - quantum_register_name_column_start_x: int = ( - relative_group_box_content_rect.topLeft().x() + self.simulation_run_contents_padding_size - ) - initial_column_one_rect = QtCore.QRect( - quantum_register_name_column_start_x + self.simulation_run_contents_padding_size, - relative_group_box_content_rect.topLeft().y() + self.stringified_quantum_register_y_spacing, - quantum_register_name_column_width, - SimulationRunModelStyledItemDelegate._get_vertical_text_width( + header_row_column_one_rect = QtCore.QRect( + header_text_rect.bottomLeft().x(), + header_text_rect.bottomLeft().y(), + qreg_name_column_width, + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_content_font_size ), ) + header_row_column_one_text_rect = header_row_column_one_rect.adjusted( + self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 + ) + # TODO: What if header text is larger than contents? painter.drawText( - initial_column_one_rect, + header_row_column_one_text_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "Quantum register", ) - quantum_register_content_width_without_padding: int = ( - self._get_estimated_quantum_register_contents_column_width( - option, - index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - self.simulation_run_group_box_content_font_size, - False, - ) + max_qreg_content_width: int = self._get_estimated_quantum_register_contents_column_width( + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + self.simulation_run_group_box_content_font_size, + False, ) - quantum_register_input_values_start_x: int = ( - initial_column_one_rect.topRight().x() + self.stringified_quantum_register_x_spacing + header_row_column_two_rect = QtCore.QRect( + header_row_column_one_rect.topRight().x(), + header_row_column_one_rect.topRight().y(), + max_qreg_content_width, + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + option, self.simulation_run_group_box_content_font_size + ), ) - - initial_column_two_rect = QtCore.QRect( - quantum_register_input_values_start_x, - initial_column_one_rect.topLeft().y(), - quantum_register_content_width_without_padding, - initial_column_one_rect.height(), + header_row_column_two_text_rect = header_row_column_two_rect.adjusted( + self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 ) + # TODO: What if header text is larger than contents? painter.drawText( - initial_column_two_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "INPUT" + header_row_column_two_text_rect, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + "INPUT", ) - quantum_register_output_values_start_x: int = ( - initial_column_two_rect.topRight().x() + self.stringified_quantum_register_x_spacing + header_row_column_three_rect = QtCore.QRect( + header_row_column_two_rect.topRight().x(), + header_row_column_two_rect.topRight().y(), + max_qreg_content_width, + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + option, self.simulation_run_group_box_content_font_size + ), ) - initial_column_three_rect = QtCore.QRect( - quantum_register_output_values_start_x, - initial_column_one_rect.topLeft().y(), - quantum_register_content_width_without_padding, - initial_column_two_rect.height(), + header_row_column_three_text_rect = header_row_column_three_rect.adjusted( + self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 ) + # TODO: What if header text is larger than contents? painter.drawText( - initial_column_three_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, "OUTPUT" + header_row_column_three_text_rect, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + "OUTPUT", ) SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, initial_column_one_rect, 5, QtCore.Qt.GlobalColor.red, index + painter, header_row_column_one_rect, 5, QtCore.Qt.GlobalColor.red, index ) SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, initial_column_two_rect, 5, QtCore.Qt.GlobalColor.blue, index + painter, header_row_column_two_rect, 5, QtCore.Qt.GlobalColor.blue, index ) SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, initial_column_three_rect, 5, QtCore.Qt.GlobalColor.green, index + painter, header_row_column_three_rect, 5, QtCore.Qt.GlobalColor.green, index ) row_idx: int = 1 - row_i_y_offset: int = ( + per_row_y_offset: int = ( self.stringified_quantum_register_y_spacing - + SimulationRunModelStyledItemDelegate._get_vertical_text_width( + + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_content_font_size ) ) for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): - curr_row_y_offset: int = row_idx * row_i_y_offset + curr_row_y_offset: int = row_idx * per_row_y_offset - row_i_column_one = initial_column_one_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + row_i_column_one = header_row_column_one_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) painter.drawText( row_i_column_one, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, qreg_layout.quantum_register_name, ) - row_i_column_two = initial_column_two_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + row_i_column_two = header_row_column_two_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) painter.drawText( row_i_column_two, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, @@ -338,7 +345,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, ), ) - row_i_column_three = initial_column_three_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + row_i_column_three = header_row_column_three_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) painter.drawText( row_i_column_three, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, @@ -358,7 +365,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter.setPen(QtCore.Qt.GlobalColor.gray) painter.setFont(quantum_layout_info_text_font) - row_i_plus_column_one = row_i_column_one.adjusted(0, row_i_y_offset, 0, row_i_y_offset) + row_i_plus_column_one = row_i_column_one.adjusted(0, per_row_y_offset, 0, per_row_y_offset) painter.drawText( row_i_plus_column_one, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, From 3051605d2782c3e280e0a910dd64fc1302f52f45 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 6 Jan 2026 00:06:02 +0100 Subject: [PATCH 12/88] Replaced InputOutputStateMapping with SimulationRunModel containing further model properties required for other Qt 'components' --- .../quantum_circuit_simulation_dialog.py | 21 ++-- .../qt_simulation_run_model.py | 118 +++++++++++++----- .../qt_simulation_run_styled_item_delegate.py | 54 +++++--- 3 files changed, 129 insertions(+), 64 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 8076bade..276154bd 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -14,7 +14,7 @@ from mqt import syrec -from .simulation_view.qt_simulation_run_model import InputOutputStateMapping, QtSimulationRunModel +from .simulation_view.qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel from .simulation_view.qt_simulation_run_styled_item_delegate import SimulationRunModelStyledItemDelegate LOADED_FROM_FILE_INPUT_FIELD_NAME: Final[str] = "load_from_file_input_field" @@ -205,9 +205,10 @@ def handle_simulation_run_selection_change( delete_simulation_run_btn.setEnabled(is_list_item_selected) def handle_simulation_run_add_btn_click(self) -> None: - if not self.simulation_runs_model.add_simulation_run( - InputOutputStateMapping( - input_state=syrec.n_bit_values_container(self.expected_input_output_state_size), output_state=None + if not self.simulation_runs_model.add_simulation_run_model( + SimulationRunModel( + input_state=syrec.n_bit_values_container(self.expected_input_output_state_size), + expected_output_state=None, ) ): return @@ -241,7 +242,7 @@ def handle_simulation_run_delete_btn_click(self) -> None: if simulation_runs_list_view is None: return - if not self.simulation_runs_model.delete_simulation_run(simulation_runs_list_view.currentIndex()): + if not self.simulation_runs_model.delete_simulation_run_model(simulation_runs_list_view.currentIndex()): return self.set_enabled_state_of_simulation_runs_execution_controls( @@ -278,15 +279,15 @@ def generate_some_simulation_runs( ) -> None: for i in range(self): in_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) - out_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) + expected_out_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) - in_out_state_mapping: InputOutputStateMapping | None = None + sim_run_model: SimulationRunModel | None = None if i < 2: - in_out_state_mapping = InputOutputStateMapping(in_state, None) + sim_run_model = SimulationRunModel(in_state, None) else: - in_out_state_mapping = InputOutputStateMapping(in_state, out_state) + sim_run_model = SimulationRunModel(in_state, expected_out_state) - shared_simulation_runs_model.add_simulation_run(in_out_state_mapping) + shared_simulation_runs_model.add_simulation_run_model(sim_run_model) def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index b1aabff1..567dd245 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -43,32 +43,82 @@ class QuantumRegisterLayout: quantum_register_size: int -@dataclass -class InputOutputStateMapping: +class SimulationRunModel: input_state: syrec.n_bit_values_container - output_state: syrec.n_bit_values_container | None + expected_output_state: syrec.n_bit_values_container | None = None + actual_output_state: syrec.n_bit_values_container | None = None + do_expected_and_actual_outputs_match: bool | None = None + execution_runtime_in_ms: float | None = None - def initialize_output_state_as_copy_of_input_state(self) -> bool: - if self.output_state is not None: - return False - - self.output_state = syrec.n_bit_values_container(self.input_state.size()) - for i in range(self.output_state.size()): - self.output_state.set(self.input_state.test(i)) - return True - - def update_input_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: - if qubit < 0 or qubit >= self.input_state.size(): + def __init__( + self, + input_state: syrec.n_bit_values_container, + expected_output_state: syrec.n_bit_values_container | None = None, + ): + if expected_output_state is not None and input_state.size() != expected_output_state.size(): + msg = f"Expected output state size (n_qubits = {expected_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" + raise ValueError(msg) + + self.input_state = input_state + self.expected_output_state = expected_output_state + + def initialize_expected_output_state_as_copy_of_input_state(self) -> None: + if self.expected_output_state is not None: + return + + self.expected_output_state = syrec.n_bit_values_container(self.input_state.size()) + for i in range(self.expected_output_state.size()): + self.expected_output_state.set(self.input_state.test(i)) + + def reset_result_of_execution(self) -> None: + self.actual_output_state = None + self.do_expected_and_actual_outputs_match = None + self.execution_runtime_in_ms = None + + def set_result_of_simulation_execution( + self, actual_output_state: syrec.n_bit_values_container, execution_runtime_in_ms: float + ) -> None: + if actual_output_state.size() != self.input_state.size(): + msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match input state size (n_qubits = {self.input_state.size()})" + raise ValueError(msg) + if self.expected_output_state is None: + msg = "Tried to set actual output state when expected output state was not set!" + raise ValueError(msg) + if self.expected_output_state.size() != actual_output_state.size(): + msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match expected output state size (n_qubits = {self.expected_output_state.size()})" + raise ValueError(msg) + if execution_runtime_in_ms < 0: + msg = f"Invalid execution runtime value {execution_runtime_in_ms}" + raise ValueError(msg) + + if self.actual_output_state is None: + self.actual_output_state = actual_output_state + else: + self.actual_output_state = syrec.n_bit_values_container(self.input_state.size()) + for i in range(self.expected_output_state.size()): + self.actual_output_state.set(actual_output_state.test(i)) + + self.execution_runtime_in_ms = execution_runtime_in_ms + + def update_input_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: + return SimulationRunModel._update_n_bit_values_container_qubit_value(self.input_state, qubit, new_qubit_value) + + def update_expected_output_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: + if self.expected_output_state is None: return False - self.input_state.set(qubit, qubit_value) - return True + return SimulationRunModel._update_n_bit_values_container_qubit_value( + self.expected_output_state, qubit, new_qubit_value + ) - def update_output_state_qubit_value(self, qubit: int, qubit_value: bool) -> bool: - if self.output_state is None or qubit < 0 or qubit >= self.output_state.size(): + @staticmethod + def _update_n_bit_values_container_qubit_value( + n_bit_values_container: syrec.n_bit_values_container, qubit: int, new_qubit_value: bool + ) -> bool: + if qubit < 0 or qubit >= n_bit_values_container.size(): return False - self.output_state.set(qubit, qubit_value) + n_bit_values_container.set(qubit, new_qubit_value) return True @@ -78,7 +128,7 @@ def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtCore.QObject = None ): super().__init__(parent) - self.input_output_state_mappings: list[InputOutputStateMapping] = [] + self.simulation_run_models: list[SimulationRunModel] = [] self.quantum_register_layouts: list[QuantumRegisterLayout] = ( QtSimulationRunModel.__record_quantum_register_layouts(annotatable_quantum_computation) ) @@ -120,14 +170,14 @@ def __record_quantum_register_layouts( return quantum_register_layouts def rowCount(self, parent: QtCore.QModelIndex) -> int: # noqa: N802 - return 0 if parent.isValid() else len(self.input_output_state_mappings) + return 0 if parent.isValid() else len(self.simulation_run_models) def data(self, index: QtCore.QModelIndex, role: int) -> object: if not index.isValid(): return None if role == SIMULATION_RUN_IO_STATE_QT_ROLE: - return self.input_output_state_mappings[index.row()] + return self.simulation_run_models[index.row()] if role == QUANTUM_REGISTER_LAYOUT_QT_ROLE: return self.quantum_register_layouts @@ -144,18 +194,18 @@ def data(self, index: QtCore.QModelIndex, role: int) -> object: return None # TODO: Check for duplicates? - def add_simulation_run(self, input_output_state_mapping: InputOutputStateMapping) -> bool: - n_simulation_runs: int = len(self.input_output_state_mappings) + def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> bool: + n_simulation_runs: int = len(self.simulation_run_models) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) - self.input_output_state_mappings.append(input_output_state_mapping) + self.simulation_run_models.append(simulation_run_model) self.endInsertRows() return True - def delete_simulation_run(self, index: QtCore.QModelIndex) -> bool: + def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: self.beginRemoveRows(QtCore.QModelIndex(), index.row(), index.row()) if self.is_model_index_valid(index): - self.input_output_state_mappings.pop(index.row()) + self.simulation_run_models.pop(index.row()) self.endRemoveRows() # self.layoutChanged.emit() return True @@ -165,21 +215,21 @@ def delete_simulation_run(self, index: QtCore.QModelIndex) -> bool: # TODO: Check for duplicates? def update_input_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: - if self.is_model_index_valid(index) and self.input_output_state_mappings[ - index.row() - ].update_input_state_qubit_value(qubit, qubit_value): + if self.is_model_index_valid(index) and self.simulation_run_models[index.row()].update_input_state_qubit_value( + qubit, qubit_value + ): self.dataChanged.emit(index, index) return True return False # TODO: Check for duplicates? def update_output_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: - if self.is_model_index_valid(index) and self.input_output_state_mappings[ - index.row() - ].update_output_state_qubit_value(qubit, qubit_value): + if self.is_model_index_valid(index) and self.simulation_run_models[index.row()].update_output_state_qubit_value( + qubit, qubit_value + ): self.dataChanged.emit(index, index) return True return False def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: - return index.isValid() and index.row() >= 0 and index.row() < len(self.input_output_state_mappings) # type: ignore[no-any-return] + return index.isValid() and index.row() >= 0 and index.row() < len(self.simulation_run_models) # type: ignore[no-any-return] diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py index aeb0e207..0a48e9b0 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py @@ -24,7 +24,7 @@ from mqt import syrec from .qt_simulation_run_model import ( - InputOutputStateMapping, + SimulationRunModel, ) @@ -37,6 +37,7 @@ def __init__(self, parent=None): self.simulation_run_group_box_title_font_size: Final[int] = 14 self.simulation_run_group_box_content_font_size: Final[int] = 10 self.quantum_register_layout_info_text_font_size: Final[int] = 8 + self.simulation_run_title_bottom_margin_y: Final[int] = 8 self.stringified_quantum_register_y_spacing: Final[int] = 4 self.stringified_quantum_register_x_spacing: Final[int] = 6 self.simulation_run_contents_padding_size: Final[int] = 20 @@ -46,6 +47,7 @@ def __init__(self, parent=None): self.quantum_register_name_column_header = "Quantum register" self.input_state_value_column_header = "INPUT" self.output_state_value_column_header = "OUTPUT" + self.unknown_output_state_value_placeholder = "" @staticmethod def _get_text_width_for_font_size(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: @@ -109,11 +111,8 @@ def _get_estimated_bounding_rect( # Quantum register contents are displayed as two rows containing the following information: # R0: # R1: - group_box_title_height: int = ( - self.stringified_quantum_register_y_spacing - + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_title_font_size - ) + group_box_title_height: int = SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + option, self.simulation_run_group_box_title_font_size ) qreg_contents_text_height: int = ( @@ -130,7 +129,7 @@ def _get_estimated_bounding_rect( total_simulation_run_group_box_height = ( self.simulation_run_contents_padding_size + group_box_title_height - + self.stringified_quantum_register_y_spacing + + self.simulation_run_title_bottom_margin_y + total_qreg_contents_text_height + self.simulation_run_contents_padding_size ) @@ -139,12 +138,19 @@ def _get_estimated_bounding_rect( option, index, self.simulation_run_group_box_title_font_size ) - max_qreg_content_column_width: int = self._get_estimated_quantum_register_contents_column_width( + qreg_content_header_width: int = self._get_text_width_for_font_size( + self.input_state_value_column_header, option, self.simulation_run_group_box_content_font_size + ) + + max_qreg_qubits_column_width: int = self._get_estimated_quantum_register_contents_column_width( option, index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), self.simulation_run_group_box_content_font_size, True, ) + + max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) + max_per_qreg_content_column_width_with_spacing: int = ( self.stringified_quantum_register_x_spacing + max_qreg_content_column_width @@ -197,11 +203,18 @@ def _stringify_some_qubits_of_n_bit_values_container( "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) ]) + # TODO: + # @staticmethod + # def _get_elided_text_for_pixel_width( + # text: str, text_font: QtGui.QFontMetrics, available_pixel_width_for_text: int + # ) -> str: + # return text_font.elidedText(text, QtCore.Qt.TextElideMode.Qt.ElideRight, available_pixel_width_for_text) + def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid(): return - associated_input_output_mapping: InputOutputStateMapping = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) painter.save() painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) @@ -239,8 +252,10 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, header_text_rect = QtCore.QRect( simulation_run_contents_rect.topLeft().x(), simulation_run_contents_rect.topLeft().y(), - 0, - header_title_height + self.stringified_quantum_register_y_spacing, + SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( + header_title, option, self.simulation_run_group_box_title_font_size + ), + header_title_height, ) qreg_name_column_width: int = self._get_estimated_quantum_register_name_column_width( @@ -248,8 +263,8 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, ) header_row_column_one_rect = QtCore.QRect( - header_text_rect.bottomLeft().x(), - header_text_rect.bottomLeft().y(), + header_text_rect.topLeft().x(), + header_text_rect.topLeft().y() + 2 * self.simulation_run_title_bottom_margin_y, qreg_name_column_width, SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_content_font_size @@ -262,7 +277,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter.drawText( header_row_column_one_text_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - "Quantum register", + self.quantum_register_name_column_header, ) max_qreg_content_width: int = self._get_estimated_quantum_register_contents_column_width( @@ -286,7 +301,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter.drawText( header_row_column_two_text_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - "INPUT", + self.input_state_value_column_header, ) header_row_column_three_rect = QtCore.QRect( @@ -304,7 +319,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter.drawText( header_row_column_three_text_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - "OUTPUT", + self.output_state_value_column_header, ) SimulationRunModelStyledItemDelegate._paint_rect_edge_points( @@ -350,12 +365,12 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, row_i_column_three, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.output_state, + associated_input_output_mapping.expected_output_state, qreg_layout.first_qubit_of_quantum_register, qreg_layout.quantum_register_size, ) - if associated_input_output_mapping.output_state is not None - else "", + if associated_input_output_mapping.expected_output_state is not None + else self.unknown_output_state_value_placeholder, ) painter.save() @@ -377,4 +392,3 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, row_idx += 2 painter.restore() - return From 04ee7e21cc59c40d86ca9141f740f40f2eb266de Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 6 Jan 2026 20:17:51 +0100 Subject: [PATCH 13/88] Refactored existing simulation run editor dialog to be opened when clicking edit selected item in simulation run overview. However, dialog does not update internal model. --- .../quantum_circuit_simulation_dialog.py | 45 ++- ....py => qt_simulation_run_editor_dialog.py} | 269 ++++++++---------- .../qt_simulation_run_model.py | 75 +++-- .../qt_simulation_run_styled_item_delegate.py | 12 +- 4 files changed, 215 insertions(+), 186 deletions(-) rename python/mqt/syrec/simulation_view/{qt_edit_simulation_run_editor.py => qt_simulation_run_editor_dialog.py} (73%) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 276154bd..0931def5 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -14,6 +14,7 @@ from mqt import syrec +from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog from .simulation_view.qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel from .simulation_view.qt_simulation_run_styled_item_delegate import SimulationRunModelStyledItemDelegate @@ -44,6 +45,7 @@ def __init__( self.height = 800 self.setGeometry(self.left, self.top, self.width, self.height) + self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) @@ -80,6 +82,7 @@ def initialize_simulation_runs_tab_widget( tab_wrapper_widget = QtWidgets.QFrame() tab_wrapper_widget_layout = QtWidgets.QVBoxLayout() tab_wrapper_widget.setLayout(tab_wrapper_widget_layout) + tab_wrapper_widget.setAutoFillBackground(True) manual_y_space_size: Final[int] = 35 if create_load_from_file_controls: @@ -124,7 +127,7 @@ def initialize_simulation_runs_tab_widget( objectName=EDIT_SIM_RUN_BTN_NAME, ) edit_simulation_run_button.setEnabled(False) - edit_simulation_run_button.clicked.connect(QuantumCircuitSimulationDialog.handle_simulation_run_edit_btn_click) + edit_simulation_run_button.clicked.connect(self.handle_simulation_run_edit_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(edit_simulation_run_button) delete_simulation_run_button = QtWidgets.QPushButton( @@ -227,9 +230,43 @@ def handle_simulation_run_add_btn_click(self) -> None: simulation_runs_list_view.scrollToBottom() - @staticmethod - def handle_simulation_run_edit_btn_click() -> None: - return + def handle_simulation_run_edit_btn_click(self) -> None: + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + return + + simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( + QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME + ) + if simulation_runs_list_view is None: + return + + self.simulation_run_editor_dialog = SimulationRunEditorDialog(simulation_runs_list_view.currentIndex(), self) + self.simulation_run_editor_dialog.finished.connect(self.handle_simulation_run_editor_dialog_close) + self.simulation_run_editor_dialog.show() + + def handle_simulation_run_editor_dialog_close(self, result: int) -> None: + # This should not happen but is checked nevertheless + if self.simulation_run_editor_dialog is None or result == QtWidgets.QDialog.DialogCode.Rejected: + return + + try: + self.simulation_runs_model.update_simulation_run_model( + self.simulation_run_editor_dialog.simulation_run_model_index, + self.simulation_run_editor_dialog.edited_simulation_run_model, + ) + except ValueError as err: + pressed_message_box_button: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.critical( + self, + "Simulation run model update error!", + f"Update of simulation run model {self.simulation_run_editor_dialog.simulation_run_model_index.row()} failed due to an error!\nReason: {err}", + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + if pressed_message_box_button == QtWidgets.QMessageBox.StandardButton: + pass + finally: + self.simulation_run_editor_dialog = None def handle_simulation_run_delete_btn_click(self) -> None: curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() diff --git a/python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py similarity index 73% rename from python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py rename to python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index ec69d82f..e3598f82 100644 --- a/python/mqt/syrec/simulation_view/qt_edit_simulation_run_editor.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -15,11 +15,13 @@ from mqt import syrec if TYPE_CHECKING: - from .qt_simulation_run_model import InputOutputStateMapping + from .qt_simulation_run_model import QuantumRegisterLayout, SimulationRunModel - -def does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: - return qubit_label.startswith("__q") +from .qt_simulation_run_model import ( + ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE, + QUANTUM_REGISTER_LAYOUT_QT_ROLE, + SIMULATION_RUN_IO_STATE_QT_ROLE, +) def stringify_some_qubits_of_n_bit_values_container( @@ -31,40 +33,36 @@ def stringify_some_qubits_of_n_bit_values_container( return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) -class InputOutputStateMappingDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] - input_state_qubit_value_change = QtCore.pyqtSignal( - int, - int, - bool, - arguments=["simulation_run_number", "qubit", "new_qubit_value"], - name="inputStateQubitValueChanged", - ) - output_state_qubit_value_change = QtCore.pyqtSignal( - int, - int, - bool, - arguments=["simulation_run_number", "qubit", "new_qubit_value"], - name="outputStateQubitValueChanged", - ) - requested_simulation_run_deletion = QtCore.pyqtSignal( - int, arguments=["simulation_run_number"], name="simulationRunDeleted" - ) - request_output_state_initialization = QtCore.pyqtSignal(name="requestedOutputStateInitialization") - request_output_state_reset = QtCore.pyqtSignal(name="requestedOutputStateReset") - - def __init__( - self, - simulation_run_number: int, - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - input_output_state_mapping: InputOutputStateMapping, - is_input_state_readonly: bool = False, - ) -> None: - # parent: QtWidgets.QWidget) -> None: - super().__init__() +# TODO: Input or expected output state are not updated +class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] + def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWidgets.QWidget): + super().__init__(parent) + self.simulation_run_model_index: QtCore.QModelIndex = simulation_run_model_index + self.edited_simulation_run_model: SimulationRunModel = simulation_run_model_index.data( + SIMULATION_RUN_IO_STATE_QT_ROLE + ) + + self.qreg_layouts: list[QuantumRegisterLayout] = simulation_run_model_index.data( + QUANTUM_REGISTER_LAYOUT_QT_ROLE + ) + self.annotatable_quantum_computation: syrec.annotatable_quantum_computation = simulation_run_model_index.data( + ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE + ) + + initial_input_state: syrec.n_bit_values_container = self.edited_simulation_run_model.input_state + initial_expected_output_state: syrec.n_bit_values_container | None = ( + self.edited_simulation_run_model.expected_output_state + ) - self.simulation_run_number = simulation_run_number - self.annotatable_quantum_computation = annotatable_quantum_computation - self.is_input_state_readonly = is_input_state_readonly + self.setModal(True) + self.setWindowTitle("Edit qubit values of quantum registers for simulation run") + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + self.simulation_run_wrapper_box = QtWidgets.QGroupBox( + "Simulation run #" + str(simulation_run_model_index.row()) + ) + main_layout.addWidget(self.simulation_run_wrapper_box) # TODO: Validation that input and output state have same size (validate all input parameters) # TODO: Define validator for input and output state inputs @@ -82,12 +80,8 @@ def __init__( self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" - main_layout = QtWidgets.QVBoxLayout() - self.setLayout(main_layout) - self.simulation_run_wrapper_box = QtWidgets.QGroupBox("Simulation run #" + str(self.simulation_run_number)) - # TODO: How can we determine whether qubits are readonly - self.are_qubits_values_readonly: bool = input_output_state_mapping.input_state.size() == 0 + self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 self.edit_of_qubit_values_enabled: bool = False # TODO: Add validators @@ -113,11 +107,6 @@ def __init__( quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - if not self.is_input_state_readonly: - simulation_run_delete_button = QtWidgets.QPushButton("Delete simulation run") - simulation_run_delete_button.clicked.connect(self.handle_simulation_run_deletion_button_click) - quantum_register_controls_grid_layout.addWidget(simulation_run_delete_button, 0, 5) - # Grid position component order is row followed by column input_column_label = QtWidgets.QLabel("Input") output_column_label = QtWidgets.QLabel("Output") @@ -135,43 +124,37 @@ def __init__( ) quantum_register_controls_grid_row: int = 2 - for qreg in annotatable_quantum_computation.qregs.values(): - first_qubit_of_qreg: int = qreg.start - n_qubits_of_qreg: int = qreg.size - - # Skip ancillary quantum registers (we assume that ancillary quantum registers only store ancillary qubits thus only checking the first qubit of the quantum register is sufficient) - # It is not sufficient to simply check via annotatable_quantum_computation.is_circuit_qubit_ancillary since this does not cover garbage qubits generated for local SyReC module variables. - if n_qubits_of_qreg == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( - annotatable_quantum_computation.get_qubit_label(first_qubit_of_qreg, syrec.qubit_label_type.internal) - ): - continue + for qreg_layout in self.qreg_layouts: + first_qubit_of_qreg: int = qreg_layout.first_qubit_of_qreg + n_qubits_of_qreg: int = qreg_layout.qreg_size + qreg_name: str = qreg_layout.qreg_name quantum_register_label = QtWidgets.QLabel( - "Quantum register: " + qreg.name, objectName=self.qreg_label_name_format.format(qreg_name=qreg.name) + "Quantum register: " + qreg_name, objectName=self.qreg_label_name_format.format(qreg_name=qreg_name) ) input_state_edit_field = QtWidgets.QLineEdit( - objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) ) input_state_edit_field.setText( stringify_some_qubits_of_n_bit_values_container( - input_output_state_mapping.input_state, first_qubit_of_qreg, n_qubits_of_qreg + initial_input_state, first_qubit_of_qreg, n_qubits_of_qreg ) ) - input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) + # input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) input_state_edit_field.setValidator(n_bit_values_container_contents_validator) input_state_edit_field.setMaxLength(n_qubits_of_qreg) output_state_edit_field = QtWidgets.QLineEdit( - objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) ) - if input_output_state_mapping.output_state is not None: + if initial_expected_output_state is not None: output_state_edit_field.setText( stringify_some_qubits_of_n_bit_values_container( - input_output_state_mapping.output_state, first_qubit_of_qreg, n_qubits_of_qreg + initial_expected_output_state, first_qubit_of_qreg, n_qubits_of_qreg ) ) - output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) + # output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) else: output_state_edit_field.setEnabled(False) output_state_edit_field.setPlaceholderText("-") @@ -181,11 +164,11 @@ def __init__( edit_qubit_values_toggle_button = QtWidgets.QPushButton( "Edit qubit values", - objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name), + objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name), ) # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton edit_qubit_values_toggle_button.clicked.connect( - lambda _, associated_qreg_name=qreg.name: self.handle_qreg_qubit_values_edit_toggle_button_click( + lambda _, associated_qreg_name=qreg_name: self.handle_qreg_qubit_values_edit_toggle_button_click( associated_qreg_name ) ) @@ -215,7 +198,7 @@ def __init__( # TODO: Scroll area input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( - "Qubit values", objectName=self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) + "Qubit values", objectName=self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg_name) ) input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) @@ -226,14 +209,14 @@ def __init__( qubit_search_layout.addWidget(qubit_search_label) qubit_search_input_field = QtWidgets.QLineEdit( - objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg.name) + objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg_name) ) qubit_search_input_field.setPlaceholderText("") qubit_search_layout.addWidget(qubit_search_input_field) qubit_search_trigger_button = QtWidgets.QPushButton("Search") qubit_search_trigger_button.clicked.connect( - lambda _, associated_qreg_name=qreg.name: self.handle_qubit_search_trigger_button_click( + lambda _, associated_qreg_name=qreg_name: self.handle_qubit_search_trigger_button_click( associated_qreg_name ) ) @@ -245,7 +228,7 @@ def __init__( for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): one_based_relative_qubit_idx_in_qreg: int = (qubit - first_qubit_of_qreg) + 1 - fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( + fetched_internal_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal ) qubit_label = QtWidgets.QLabel( @@ -263,27 +246,25 @@ def __init__( ) input_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value( - input_output_state_mapping.input_state.test(qubit) - ) + stringified_qubit_value=self.stringify_qubit_value(initial_input_state.test(qubit)) ) ) - if not self.is_input_state_readonly: - input_state_qubit_value_checkbox.checkStateChanged.connect( - lambda state, - associated_qreg_name=qreg.name, - associated_qubit=qubit, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - - 1: self.handle_input_state_qubit_value_checkbox_state_change( - associated_qreg_name, - associated_qubit, - relative_qubit_index_in_quantum_register, - state == QtCore.Qt.CheckState.Checked, - ) - ) - else: - input_state_qubit_value_checkbox.setEnabled(False) + # if not self.is_input_state_readonly: + # input_state_qubit_value_checkbox.checkStateChanged.connect( + # lambda state, + # associated_qreg_name=qreg_name, + # associated_qubit=qubit, + # relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg + # - 1: self.handle_input_state_qubit_value_checkbox_state_change( + # associated_qreg_name, + # associated_qubit, + # relative_qubit_index_in_quantum_register, + # state == QtCore.Qt.CheckState.Checked, + # ) + # ) + # else: + # input_state_qubit_value_checkbox.setEnabled(False) input_output_qubits_value_controls_groupbox_layout.addWidget( input_state_qubit_value_checkbox, @@ -298,15 +279,13 @@ def __init__( output_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( stringified_qubit_value=self.stringify_qubit_value( - None - if input_output_state_mapping.output_state is None - else input_output_state_mapping.output_state.test(qubit) + None if initial_expected_output_state is None else initial_expected_output_state.test(qubit) ) ) ) output_state_qubit_value_checkbox.checkStateChanged.connect( lambda state, - associated_qreg_name=qreg.name, + associated_qreg_name=qreg_name, associated_qubit=qubit, relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - 1: self.handle_output_state_qubit_value_checkbox_state_change( @@ -316,7 +295,7 @@ def __init__( state == QtCore.Qt.CheckState.Checked, ) ) - output_state_qubit_value_checkbox.setEnabled(input_output_state_mapping.output_state is not None) + output_state_qubit_value_checkbox.setEnabled(initial_expected_output_state is not None) input_output_qubits_value_controls_groupbox_layout.addWidget( output_state_qubit_value_checkbox, one_based_relative_qubit_idx_in_qreg, @@ -324,6 +303,7 @@ def __init__( alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) + # TODO: How can the column widths of the input fields and the checkbox columns be synced? input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 0) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) @@ -347,38 +327,50 @@ def __init__( ) quantum_register_controls_grid_row += 1 + # Add spacer item to take up remaining space between last quantum register elements and bottom of parent group box without stretching the spacing between the already added controls in the group box + quantum_register_controls_grid_layout.addItem( + QtWidgets.QSpacerItem(2, 2, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding), + quantum_register_controls_grid_row, + 0, + 1, + 5, + ) quantum_register_controls_grid_layout.setColumnStretch(0, 0) quantum_register_controls_grid_layout.setColumnStretch(1, 1) quantum_register_controls_grid_layout.setColumnStretch(2, 1) quantum_register_controls_grid_layout.setColumnStretch(3, 0) quantum_register_controls_grid_layout.setColumnStretch(4, 2) - quantum_register_controls_grid_layout.setColumnStretch(5, 0) - # simulation_run_scroll_area = QtWidgets.QScrollArea() - # simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) - # simulation_run_scroll_area.setWidgetResizable(True) - # main_layout.addWidget(simulation_run_scroll_area) - main_layout.addWidget(self.simulation_run_wrapper_box) + simulation_run_scroll_area = QtWidgets.QScrollArea() + simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) + simulation_run_scroll_area.setWidgetResizable(True) + main_layout.addWidget(simulation_run_scroll_area) - def handle_quantum_register_name_search(self) -> None: - for qreg in self.annotatable_quantum_computation.qregs.values(): - if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( - self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) - ): - continue + # Add dialog control buttons and link signals to slots of dialog + dialog_button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Save | QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + dialog_button_box.setCenterButtons(True) + dialog_button_box.accepted.connect(self.accept) + dialog_button_box.rejected.connect(self.reject) + + main_layout.addWidget(dialog_button_box) + def handle_quantum_register_name_search(self) -> None: + for qreg_layout in self.qreg_layouts: + qreg_name: str = qreg_layout.qreg_name qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg.name) + QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg_name) ) qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) ) qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) ) qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) ) ) @@ -393,7 +385,7 @@ def handle_quantum_register_name_search(self) -> None: should_control_be_visible: bool = ( self.quantum_register_search_input_field.text() is None - or qreg.name.startswith(self.quantum_register_search_input_field.text()) + or qreg_name.startswith(self.quantum_register_search_input_field.text()) ) qreg_name_label.setVisible(should_control_be_visible) qreg_input_state_input_field.setVisible(should_control_be_visible) @@ -471,14 +463,8 @@ def stringify_qubit_value(qubit_value: bool | None) -> str: return "HIGH" if qubit_value else "LOW" def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: - for qreg in self.annotatable_quantum_computation.qregs.values(): - if ( - qreg.size == 0 - or does_qubit_label_start_with_internal_qubit_label_prefix( - self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) - ) - or qreg.name != associated_quantum_register_name - ): + for qreg_layout in self.qreg_layouts: + if qreg_layout.qreg_name != associated_quantum_register_name: continue qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( @@ -497,7 +483,9 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n # TODO: This should not happen continue - for qubit in range(qreg.start, qreg.start + qreg.size): + for qubit in range( + qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size + ): qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( QtWidgets.QLabel, self.qubit_label_name_format.format(qubit=qubit) ) @@ -524,24 +512,20 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: is_qubit_values_edit_enabled_for_any_qreg: bool = False - for qreg in self.annotatable_quantum_computation.qregs.values(): - if qreg.size == 0 or does_qubit_label_start_with_internal_qubit_label_prefix( - self.annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) - ): - continue - + for qreg_layout in self.qreg_layouts: + qreg_name: str = qreg_layout.qreg_name # TODO: QtCore.Qt.FindDirectChildrenOnly qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg.name) + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) ) qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg.name) + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) ) qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg.name) + QtWidgets.QGroupBox, self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg_name) ) qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg.name) + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) ) if ( @@ -553,7 +537,7 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name # TODO: This should not happen continue - if qreg.name == associated_qreg_name and not qubit_values_groupbox.isVisible(): + if qreg_name == associated_qreg_name and not qubit_values_groupbox.isVisible(): is_qubit_values_edit_enabled_for_any_qreg = True qubit_values_groupbox.setVisible(True) qubit_values_toggle_button.setText("Toggle qubit values edit") @@ -562,29 +546,8 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText("Edit qubit values") - qreg_input_state_input_field.setEnabled(not self.is_input_state_readonly) + # qreg_input_state_input_field.setEnabled(not self.is_input_state_readonly) qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) - - def handle_simulation_run_deletion_button_click(self) -> None: - self.requested_simulation_run_deletion.emit(self.simulation_run_number) - - def handle_simulation_run_number_update(self, new_simulation_run_number: int) -> None: - self.simulation_run_number = new_simulation_run_number - self.simulation_run_wrapper_box.setText("Simulation run #" + str(self.simulation_run_number)) - - -class SimulationRunDefinitionWidget(QtWidgets.QWidget): # type: ignore[misc] - def __init__( - self, - simulation_run_idx: int, # noqa: ARG002 - annotatable_quantum_computation: syrec.annotatable_quantum_computation, # noqa: ARG002 - is_delete_action_enabled: bool, # noqa: ARG002 - parent: QtWidgets.QWidget, # noqa: ARG002 - ) -> None: - super().__init__() - - self.layout = QtWidgets.QVBoxLayout() - self.setLayout(self.layout) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 567dd245..ee3cdfad 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -34,13 +34,14 @@ LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE: Final[int] = QUANTUM_REGISTER_LAYOUT_QT_ROLE + 1 LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE: Final[int] = LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE + 1 LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: Final[int] = LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE + 1 +ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE: Final[int] = LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE + 1 @dataclass(frozen=True) class QuantumRegisterLayout: - quantum_register_name: str - first_qubit_of_quantum_register: int - quantum_register_size: int + qreg_name: str + first_qubit_of_qreg: int + qreg_size: int class SimulationRunModel: @@ -135,21 +136,20 @@ def __init__( self.longest_quantum_register_name: str = "" self.largest_quantum_register_size: int = 0 self.largest_first_qubit_of_quantum_registers: int = 0 + self.annotatable_quantum_computation = annotatable_quantum_computation for qreg_layout in self.quantum_register_layouts: self.longest_quantum_register_name = ( - qreg_layout.quantum_register_name - if len(qreg_layout.quantum_register_name) > len(self.longest_quantum_register_name) + qreg_layout.qreg_name + if len(qreg_layout.qreg_name) > len(self.longest_quantum_register_name) else self.longest_quantum_register_name ) - self.largest_quantum_register_size = max( - qreg_layout.quantum_register_size, self.largest_quantum_register_size - ) + self.largest_quantum_register_size = max(qreg_layout.qreg_size, self.largest_quantum_register_size) if len(self.quantum_register_layouts) > 0: self.largest_first_qubit_of_quantum_registers = self.quantum_register_layouts[ len(self.quantum_register_layouts) - 1 - ].first_qubit_of_quantum_register + ].first_qubit_of_qreg @staticmethod def _does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: @@ -191,6 +191,9 @@ def data(self, index: QtCore.QModelIndex, role: int) -> object: if role == LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: return self.largest_first_qubit_of_quantum_registers + if role == ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE: + return self.annotatable_quantum_computation + return None # TODO: Check for duplicates? @@ -213,23 +216,49 @@ def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: self.endRemoveRows() return False - # TODO: Check for duplicates? - def update_input_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: - if self.is_model_index_valid(index) and self.simulation_run_models[index.row()].update_input_state_qubit_value( - qubit, qubit_value + # TODO: Check that no duplicate input or expected output_state is added + # TODO: Add custom error messages if validation fails + def update_simulation_run_model( + self, index: QtCore.QModelIndex, updated_simulation_run_model: SimulationRunModel + ) -> None: + if not self.is_model_index_valid(index): + msg = "Invalid model index!" + raise ValueError(msg) + + to_be_updated_simulation_run_model: SimulationRunModel = self.simulation_run_models[index.row()] + if updated_simulation_run_model.input_state.size() != to_be_updated_simulation_run_model.input_state.size(): + msg = "Input state sizes did not match" + raise ValueError(msg) + + if ( + updated_simulation_run_model.expected_output_state is not None + and updated_simulation_run_model.actual_output_state is not None + and updated_simulation_run_model.expected_output_state.size() + != updated_simulation_run_model.actual_output_state.size() ): - self.dataChanged.emit(index, index) - return True - return False + msg = "Actual and expected output state sizes did not match in updated model" + raise ValueError(msg) - # TODO: Check for duplicates? - def update_output_state_qubit_value(self, index: QtCore.QModelIndex, qubit: int, qubit_value: bool) -> bool: - if self.is_model_index_valid(index) and self.simulation_run_models[index.row()].update_output_state_qubit_value( - qubit, qubit_value + if ( + updated_simulation_run_model.expected_output_state is not None + and to_be_updated_simulation_run_model.expected_output_state is not None + and updated_simulation_run_model.expected_output_state.size() + != to_be_updated_simulation_run_model.expected_output_state.size() ): - self.dataChanged.emit(index, index) - return True - return False + msg = "Expected output state sizes did not match between currently stored model and updated model" + raise ValueError(msg) + + if ( + updated_simulation_run_model.actual_output_state is not None + and to_be_updated_simulation_run_model.actual_output_state is not None + and updated_simulation_run_model.actual_output_state.size() + != to_be_updated_simulation_run_model.actual_output_state.size() + ): + msg = "Actual output state sizes did not match between currently stored model and updated model" + raise ValueError(msg) + + self.simulation_run_models[index.row()] = updated_simulation_run_model + self.dataChanged.emit(index, index) def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: return index.isValid() and index.row() >= 0 and index.row() < len(self.simulation_run_models) # type: ignore[no-any-return] diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py index 0a48e9b0..3c461078 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py @@ -346,7 +346,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter.drawText( row_i_column_one, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - qreg_layout.quantum_register_name, + qreg_layout.qreg_name, ) row_i_column_two = header_row_column_two_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) @@ -355,8 +355,8 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( associated_input_output_mapping.input_state, - qreg_layout.first_qubit_of_quantum_register, - qreg_layout.quantum_register_size, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, ), ) @@ -366,8 +366,8 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( associated_input_output_mapping.expected_output_state, - qreg_layout.first_qubit_of_quantum_register, - qreg_layout.quantum_register_size, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, ) if associated_input_output_mapping.expected_output_state is not None else self.unknown_output_state_value_placeholder, @@ -385,7 +385,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, row_i_plus_column_one, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, self.quantum_register_layout_text_format.format( - first_qubit=qreg_layout.first_qubit_of_quantum_register, n_qubits=qreg_layout.quantum_register_size + first_qubit=qreg_layout.first_qubit_of_qreg, n_qubits=qreg_layout.qreg_size ), ) painter.restore() From a5e87faaf3e912ab89708ae3a0ef8c6457ee6296 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 6 Jan 2026 22:40:33 +0100 Subject: [PATCH 14/88] Internal simulation run model is now updated in edit dialog and added completers to search controls of the latter --- .../quantum_circuit_simulation_dialog.py | 1 + .../qt_simulation_run_editor_dialog.py | 153 ++++++++++++------ .../qt_simulation_run_model.py | 2 + python/mqt/syrec/syrec_editor.py | 2 + 4 files changed, 112 insertions(+), 46 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 0931def5..0165f6cd 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -73,6 +73,7 @@ def __init__( self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.simulation_runs_tab_widget) self.setLayout(self.layout) + self.setSizeGripEnabled(True) # TODO: Load from file controls diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index e3598f82..d4c35460 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -33,7 +33,6 @@ def stringify_some_qubits_of_n_bit_values_container( return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) -# TODO: Input or expected output state are not updated class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWidgets.QWidget): super().__init__(parent) @@ -55,6 +54,7 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid ) self.setModal(True) + self.setSizeGripEnabled(True) self.setWindowTitle("Edit qubit values of quantum registers for simulation run") main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) @@ -97,6 +97,10 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid quantum_register_name_validator = QtGui.QRegularExpressionValidator(quantum_register_name_regular_expr, self) self.quantum_register_search_input_field.setValidator(quantum_register_name_validator) + qreg_name_search_completer = QtWidgets.QCompleter([qreg_layout.qreg_name for qreg_layout in self.qreg_layouts]) + qreg_name_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) + self.quantum_register_search_input_field.setCompleter(qreg_name_search_completer) + self.quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") self.quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) @@ -212,6 +216,15 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg_name) ) qubit_search_input_field.setPlaceholderText("") + + qubit_search_completer = QtWidgets.QCompleter( + SimulationRunEditorDialog.get_internal_qubit_labels_for_qreg( + self.annotatable_quantum_computation, first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg + ) + ) + qubit_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) + qubit_search_input_field.setCompleter(qubit_search_completer) + qubit_search_layout.addWidget(qubit_search_input_field) qubit_search_trigger_button = QtWidgets.QPushButton("Search") @@ -246,25 +259,21 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid ) input_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value(initial_input_state.test(qubit)) + stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + initial_input_state.test(qubit), return_as_high_low_state=True + ) ) ) - # if not self.is_input_state_readonly: - # input_state_qubit_value_checkbox.checkStateChanged.connect( - # lambda state, - # associated_qreg_name=qreg_name, - # associated_qubit=qubit, - # relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - # - 1: self.handle_input_state_qubit_value_checkbox_state_change( - # associated_qreg_name, - # associated_qubit, - # relative_qubit_index_in_quantum_register, - # state == QtCore.Qt.CheckState.Checked, - # ) - # ) - # else: - # input_state_qubit_value_checkbox.setEnabled(False) + input_state_qubit_value_checkbox.checkStateChanged.connect( + lambda _, + associated_qreg_name=qreg_name, + associated_qubit=qubit, + relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg + - 1: self.handle_input_state_qubit_value_checkbox_state_change( + associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + ) + ) input_output_qubits_value_controls_groupbox_layout.addWidget( input_state_qubit_value_checkbox, @@ -278,21 +287,21 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid ) output_state_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format( - stringified_qubit_value=self.stringify_qubit_value( - None if initial_expected_output_state is None else initial_expected_output_state.test(qubit) + stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + None + if initial_expected_output_state is None + else initial_expected_output_state.test(qubit), + return_as_high_low_state=True, ) ) ) output_state_qubit_value_checkbox.checkStateChanged.connect( - lambda state, + lambda _, associated_qreg_name=qreg_name, associated_qubit=qubit, relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - 1: self.handle_output_state_qubit_value_checkbox_state_change( - associated_qreg_name, - associated_qubit, - relative_qubit_index_in_quantum_register, - state == QtCore.Qt.CheckState.Checked, + associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register ) ) output_state_qubit_value_checkbox.setEnabled(initial_expected_output_state is not None) @@ -392,13 +401,8 @@ def handle_quantum_register_name_search(self) -> None: qreg_output_state_input_field.setVisible(should_control_be_visible) qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) - # TODO: Update n_bit_values_container and parent textfield def handle_input_state_qubit_value_checkbox_state_change( - self, - associated_qreg_name: str, - associated_qubit: int, - relative_qubit_index_in_quantum_register: int, - qubit_value: bool, + self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int ) -> None: associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, @@ -410,27 +414,37 @@ def handle_input_state_qubit_value_checkbox_state_change( ) if associated_qubit_value_checkbox is None or qreg_input_state_input_field is None: - # TODO: This should not happen + self.show_error_msg_dialog( + title="Failed to updated qubit value", + error_msg=f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in input state!", + ) + return + + updated_qubit_value: bool = associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked + stringified_updated_qubit_value: str = SimulationRunEditorDialog.stringify_qubit_value( + updated_qubit_value, return_as_high_low_state=True + ) + + if not self.edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): + self.show_error_msg_dialog( + title="Failed to updated qubit value", + error_msg=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in input state to new value{stringified_updated_qubit_value}!", + ) return associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) ) curr_stringified_input_state: str = qreg_input_state_input_field.text() qreg_input_state_input_field.setText( curr_stringified_input_state[:relative_qubit_index_in_quantum_register] - + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") + + SimulationRunEditorDialog.stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] ) - # TODO: Update n_bit_values_container and parent textfield def handle_output_state_qubit_value_checkbox_state_change( - self, - associated_qreg_name: str, - associated_qubit: int, - relative_qubit_index_in_quantum_register: int, - qubit_value: bool, + self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int ) -> None: associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, @@ -442,25 +456,60 @@ def handle_output_state_qubit_value_checkbox_state_change( ) if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: - # TODO: This should not happen + self.show_error_msg_dialog( + title="Failed to updated qubit value", + error_msg=f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in output state!", + ) + return + + updated_qubit_value: bool = associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked + stringified_updated_qubit_value: str = SimulationRunEditorDialog.stringify_qubit_value( + updated_qubit_value, return_as_high_low_state=True + ) + + if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( + associated_qubit, updated_qubit_value + ): + self.show_error_msg_dialog( + title="Failed to updated qubit value", + error_msg=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in output state to new value{stringified_updated_qubit_value}!", + ) return associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=self.stringify_qubit_value(qubit_value)) + self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) ) curr_stringified_output_state: str = qreg_output_state_input_field.text() qreg_output_state_input_field.setText( curr_stringified_output_state[:relative_qubit_index_in_quantum_register] - + ("1" if associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked else "0") + + SimulationRunEditorDialog.stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] ) + def get_internal_qubit_labels_for_qreg( + self: syrec.annotatable_quantum_computation, first_qubit_of_qreg: int, n_qubits_in_qreg: int + ) -> list[str]: + internal_qubit_labels: list[str] = [] + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_in_qreg): + fetched_internal_qubit_label: str | None = self.get_qubit_label(qubit, syrec.qubit_label_type.internal) + if fetched_internal_qubit_label is None: + continue + internal_qubit_labels.append(fetched_internal_qubit_label) + return internal_qubit_labels + + def show_error_msg_dialog(self, title: str, error_msg: str) -> None: + QtWidgets.QMessageBox.critical(self, title, error_msg, defaultButton=QtWidgets.QMessageBox.StandardButton.Ok) + @staticmethod - def stringify_qubit_value(qubit_value: bool | None) -> str: + def stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bool) -> str: if qubit_value is None: - return "UNKNOWN" - return "HIGH" if qubit_value else "LOW" + return "UNKNOWN" if return_as_high_low_state else "-" + + if qubit_value is True: + return "HIGH" if return_as_high_low_state else "1" + + return "LOW" if return_as_high_low_state else "0" def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: for qreg_layout in self.qreg_layouts: @@ -528,11 +577,21 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) ) + qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( + qubit_values_groupbox.findChild( + QtWidgets.QLineEdit, self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg_name) + ) + if qubit_values_groupbox is not None + else None + ) + if ( qreg_input_state_input_field is None or qreg_output_state_input_field is None or qubit_values_groupbox is None or qubit_values_toggle_button is None + or qubit_values_toggle_button is None + or qubit_values_groupbox_qubit_search_field is None ): # TODO: This should not happen continue @@ -546,8 +605,10 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText("Edit qubit values") - # qreg_input_state_input_field.setEnabled(not self.is_input_state_readonly) + qreg_input_state_input_field.setEnabled(True) qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 + qubit_values_groupbox_qubit_search_field.setText("") + self.handle_qubit_search_trigger_button_click(associated_qreg_name) self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index ee3cdfad..60a9627c 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -167,6 +167,8 @@ def __record_quantum_register_layouts( continue quantum_register_layouts.append(QuantumRegisterLayout(qreg.name, qreg.start, qreg.size)) + + quantum_register_layouts.sort(key=lambda qreg_layout: qreg_layout.first_qubit_of_qreg) return quantum_register_layouts def rowCount(self, parent: QtCore.QModelIndex) -> int: # noqa: N802 diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 72d33f29..481b9bb4 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -1353,6 +1353,8 @@ def update_circuit_view_and_qubit_information( self.qubits_information_lookup.set_lookup_information(annotatable_quantum_computation) # TODO: Check other calls of dialog.exec(), see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QDialog.html#PySide6.QtWidgets.QDialog.exec + # TODO: Store internal dialog instance as dialog: T | None and use show() instead of exec() call since the latter start a separate event loop that might not be supported by all platforms + # TODO: Move setting modal declaration to QuantumCircuitSimulationDialog dialog = QuantumCircuitSimulationDialog(annotatable_quantum_computation, parent=self) dialog.modal = True dialog.setWindowTitle("Update configurable options") From dedb20a366426360022618c8477fe05256214654 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 7 Jan 2026 12:36:24 +0100 Subject: [PATCH 15/88] Added controls to init and reset expected output state in simulation run editor dialog --- .../qt_simulation_run_editor_dialog.py | 148 +++++++++++++++--- 1 file changed, 125 insertions(+), 23 deletions(-) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index d4c35460..48bf1e93 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -75,10 +75,12 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name:s}_qubit_values_groupbox" self.qreg_label_name_format = "qreg_{qreg_name:s}_lbl" - self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_inputState" - self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_outputState" + self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_input_state" + self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_output_state" self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" + self.qreg_expected_output_state_init_name_format = "output_state_value_toggle" + self.qreg_name_search_input_field_name = "qreg_name_search_input_field" # TODO: How can we determine whether qubits are readonly self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 @@ -90,27 +92,42 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid quantum_register_search_controls_layout = QtWidgets.QHBoxLayout() quantum_register_search_label = QtWidgets.QLabel("Quantum register:") - self.quantum_register_search_input_field = QtWidgets.QLineEdit() - self.quantum_register_search_input_field.setPlaceholderText("") + quantum_register_search_input_field = QtWidgets.QLineEdit(objectName=self.qreg_name_search_input_field_name) + quantum_register_search_input_field.setPlaceholderText("") quantum_register_name_regular_expr = QtCore.QRegularExpression(R"(^([_A-Za-z]\w*)?$)") quantum_register_name_validator = QtGui.QRegularExpressionValidator(quantum_register_name_regular_expr, self) - self.quantum_register_search_input_field.setValidator(quantum_register_name_validator) + quantum_register_search_input_field.setValidator(quantum_register_name_validator) qreg_name_search_completer = QtWidgets.QCompleter([qreg_layout.qreg_name for qreg_layout in self.qreg_layouts]) qreg_name_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) - self.quantum_register_search_input_field.setCompleter(qreg_name_search_completer) + quantum_register_search_input_field.setCompleter(qreg_name_search_completer) - self.quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") - self.quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) + quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") + quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) quantum_register_search_controls_layout.addWidget(quantum_register_search_label) - quantum_register_search_controls_layout.addWidget(self.quantum_register_search_input_field) - quantum_register_search_controls_layout.addWidget(self.quantum_register_search_trigger_button) + quantum_register_search_controls_layout.addWidget(quantum_register_search_input_field) + quantum_register_search_controls_layout.addWidget(quantum_register_search_trigger_button) quantum_register_controls_grid_layout.addLayout( quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) + init_expected_output_state_button = QtWidgets.QPushButton( + "Init output state" + if self.edited_simulation_run_model.expected_output_state is None + else "Clear output state", + objectName=self.qreg_expected_output_state_init_name_format, + ) + init_expected_output_state_button.clicked.connect(self.handle_init_expected_output_state_button_click) + + output_state_value_toggle_controls_layout = QtWidgets.QHBoxLayout() + output_state_value_toggle_controls_layout.addWidget(QtWidgets.QLabel("Modify output state value:")) + output_state_value_toggle_controls_layout.addWidget(init_expected_output_state_button) + quantum_register_controls_grid_layout.addLayout( + output_state_value_toggle_controls_layout, 0, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + # Grid position component order is row followed by column input_column_label = QtWidgets.QLabel("Input") output_column_label = QtWidgets.QLabel("Output") @@ -332,7 +349,7 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid 2, 2, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) quantum_register_controls_grid_layout.addItem( - quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 4 + quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 5 ) quantum_register_controls_grid_row += 1 @@ -345,8 +362,8 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid 5, ) quantum_register_controls_grid_layout.setColumnStretch(0, 0) - quantum_register_controls_grid_layout.setColumnStretch(1, 1) - quantum_register_controls_grid_layout.setColumnStretch(2, 1) + quantum_register_controls_grid_layout.setColumnStretch(1, 0) + quantum_register_controls_grid_layout.setColumnStretch(2, 0) quantum_register_controls_grid_layout.setColumnStretch(3, 0) quantum_register_controls_grid_layout.setColumnStretch(4, 2) @@ -368,6 +385,9 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid def handle_quantum_register_name_search(self) -> None: for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name + qreg_name_search_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_name_search_input_field_name + ) qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg_name) ) @@ -384,7 +404,8 @@ def handle_quantum_register_name_search(self) -> None: ) if ( - qreg_name_label is None + qreg_name_search_input_field is None + or qreg_name_label is None or qreg_input_state_input_field is None or qreg_output_state_input_field is None or qreg_edit_qubit_values_toggle_button is None @@ -392,9 +413,8 @@ def handle_quantum_register_name_search(self) -> None: # TODO: This should not happen continue - should_control_be_visible: bool = ( - self.quantum_register_search_input_field.text() is None - or qreg_name.startswith(self.quantum_register_search_input_field.text()) + should_control_be_visible: bool = qreg_name_search_input_field.text() is None or qreg_name.startswith( + qreg_name_search_input_field.text() ) qreg_name_label.setVisible(should_control_be_visible) qreg_input_state_input_field.setVisible(should_control_be_visible) @@ -467,6 +487,15 @@ def handle_output_state_qubit_value_checkbox_state_change( updated_qubit_value, return_as_high_low_state=True ) + if self.edited_simulation_run_model.expected_output_state is None: + stringified_updated_qubit_value = SimulationRunEditorDialog.stringify_qubit_value( + None, return_as_high_low_state=True + ) + associated_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) + ) + return + if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( associated_qubit, updated_qubit_value ): @@ -479,7 +508,6 @@ def handle_output_state_qubit_value_checkbox_state_change( associated_qubit_value_checkbox.setText( self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) ) - curr_stringified_output_state: str = qreg_output_state_input_field.text() qreg_output_state_input_field.setText( curr_stringified_output_state[:relative_qubit_index_in_quantum_register] @@ -560,7 +588,6 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: - is_qubit_values_edit_enabled_for_any_qreg: bool = False for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name # TODO: QtCore.Qt.FindDirectChildrenOnly @@ -576,7 +603,12 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) ) - + expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + self.qreg_expected_output_state_init_name_format.format(qreg_name=associated_qreg_name), + ) + ) qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( qubit_values_groupbox.findChild( QtWidgets.QLineEdit, self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg_name) @@ -591,24 +623,94 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name or qubit_values_groupbox is None or qubit_values_toggle_button is None or qubit_values_toggle_button is None + or expected_output_state_value_toggle_button is None or qubit_values_groupbox_qubit_search_field is None ): # TODO: This should not happen continue if qreg_name == associated_qreg_name and not qubit_values_groupbox.isVisible(): - is_qubit_values_edit_enabled_for_any_qreg = True qubit_values_groupbox.setVisible(True) qubit_values_toggle_button.setText("Toggle qubit values edit") qreg_input_state_input_field.setEnabled(False) qreg_output_state_input_field.setEnabled(False) + expected_output_state_value_toggle_button.setEnabled(False) else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText("Edit qubit values") + expected_output_state_value_toggle_button.setEnabled(True) qreg_input_state_input_field.setEnabled(True) qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 qubit_values_groupbox_qubit_search_field.setText("") self.handle_qubit_search_trigger_button_click(associated_qreg_name) - self.quantum_register_search_input_field.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) - self.quantum_register_search_trigger_button.setEnabled(not is_qubit_values_edit_enabled_for_any_qreg) + def handle_init_expected_output_state_button_click(self, associated_qreg_name: str) -> None: + expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + self.qreg_expected_output_state_init_name_format.format(qreg_name=associated_qreg_name), + ) + ) + + if expected_output_state_value_toggle_button is None: + return + + should_reset_output_state: bool = self.edited_simulation_run_model.expected_output_state is not None + if should_reset_output_state: + self.edited_simulation_run_model.expected_output_state = None + expected_output_state_value_toggle_button.setText("Init output state") + else: + self.edited_simulation_run_model.initialize_expected_output_state_as_copy_of_input_state() + expected_output_state_value_toggle_button.setText("Clear output state") + + for qreg_layout in self.qreg_layouts: + qreg_name: str = qreg_layout.qreg_name + qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + ) + + qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + ) + + if qreg_input_state_input_field is None or qreg_output_state_input_field is None: + # TODO: This should not happen + return + + if should_reset_output_state: + qreg_output_state_input_field.setText("") + qreg_output_state_input_field.setEnabled(False) + else: + qreg_output_state_input_field.setText( + stringify_some_qubits_of_n_bit_values_container( + self.edited_simulation_run_model.expected_output_state, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, + ) + ) + qreg_output_state_input_field.setEnabled(qreg_input_state_input_field.isEnabled()) + + for qubit in range( + qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size + ): + associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format(qubit=qubit) + ) + if associated_qubit_value_checkbox is None: + # TODO: This should not happen + return + + qubit_value: bool | None = ( + self.edited_simulation_run_model.expected_output_state.test(qubit) # type: ignore[union-attr] + if not should_reset_output_state + else None + ) + associated_qubit_value_checkbox.setChecked(qubit_value if qubit_value is not None else False) + associated_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + qubit_value, return_as_high_low_state=True + ) + ) + ) + associated_qubit_value_checkbox.setEnabled(not should_reset_output_state) From 3775c90f638832295c8ab5c26003b325d4896a0f Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 7 Jan 2026 16:32:17 +0100 Subject: [PATCH 16/88] Added quantum register layout info label, quantum register content validation and automatic input field resizing --- .../qt_simulation_run_editor_dialog.py | 179 ++++++++++++++++-- .../qt_simulation_run_model.py | 2 +- 2 files changed, 164 insertions(+), 17 deletions(-) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index 48bf1e93..d9504b51 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -33,6 +33,24 @@ def stringify_some_qubits_of_n_bit_values_container( return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) +class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] + def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = None): + super().__init__(parent) + self.expected_max_num_characters = expected_max_num_characters + self.setMaxLength(expected_max_num_characters) + self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + + # Make the widget greedy: whenever the layout offers more + # than the nominal width, grab it. + def sizeHint(self) -> QtCore.QSize: # noqa: N802 + sh = super().sizeHint() + fm = QtGui.QFontMetrics(self.font()) + nominal = fm.boundingRect("W" * self.expected_max_num_characters).width() + # use the offered width + preferred = min(nominal, self.width()) + return QtCore.QSize(preferred, sh.height()) + + class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWidgets.QWidget): super().__init__(parent) @@ -75,6 +93,7 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name:s}_qubit_values_groupbox" self.qreg_label_name_format = "qreg_{qreg_name:s}_lbl" + self.qreg_layout_info_label_name_format = "qreg_{qreg_name:s}_layout_info_lbl" self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_input_state" self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_output_state" self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" @@ -139,7 +158,7 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid output_column_label, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^(\b)?$") + n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^[0-1]*$") n_bit_values_container_contents_validator = QtGui.QRegularExpressionValidator( n_bit_values_container_contents_validator_regular_expr, self ) @@ -154,20 +173,37 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid "Quantum register: " + qreg_name, objectName=self.qreg_label_name_format.format(qreg_name=qreg_name) ) - input_state_edit_field = QtWidgets.QLineEdit( - objectName=self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + quantum_register_layout_info_label = QtWidgets.QLabel( + f"(First qubit: {first_qubit_of_qreg} - Num. qubits: {n_qubits_of_qreg})", + objectName=self.qreg_layout_info_label_name_format.format(qreg_name=qreg_name), + ) + quantum_register_layout_info_label.setStyleSheet("QLabel { color : grey; }") + + input_state_edit_field = LineEditWithDynamicWidth(n_qubits_of_qreg) + input_state_edit_field.setObjectName( + self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) ) input_state_edit_field.setText( stringify_some_qubits_of_n_bit_values_container( initial_input_state, first_qubit_of_qreg, n_qubits_of_qreg ) ) + input_state_edit_field.setCursorPosition(0) + input_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) # input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) input_state_edit_field.setValidator(n_bit_values_container_contents_validator) - input_state_edit_field.setMaxLength(n_qubits_of_qreg) + input_state_edit_field.textEdited.connect( + lambda _, + associated_qreg_name=qreg_name, + expected_text_length=n_qubits_of_qreg, + is_editing_input_state=True: self.handle_input_or_output_state_text_change( + associated_qreg_name, expected_text_length, is_editing_input_state + ) + ) - output_state_edit_field = QtWidgets.QLineEdit( - objectName=self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + output_state_edit_field = LineEditWithDynamicWidth(n_qubits_of_qreg) + output_state_edit_field.setObjectName( + self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) ) if initial_expected_output_state is not None: output_state_edit_field.setText( @@ -180,8 +216,17 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid output_state_edit_field.setEnabled(False) output_state_edit_field.setPlaceholderText("-") + output_state_edit_field.setCursorPosition(0) + output_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) output_state_edit_field.setValidator(n_bit_values_container_contents_validator) - output_state_edit_field.setMaxLength(n_qubits_of_qreg) + output_state_edit_field.textEdited.connect( + lambda _, + associated_qreg_name=qreg_name, + expected_text_length=n_qubits_of_qreg, + is_editing_input_state=False: self.handle_input_or_output_state_text_change( + associated_qreg_name, expected_text_length, is_editing_input_state + ) + ) edit_qubit_values_toggle_button = QtWidgets.QPushButton( "Edit qubit values", @@ -215,6 +260,10 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid quantum_register_controls_grid_layout.addWidget( edit_qubit_values_toggle_button, quantum_register_controls_grid_row, 3 ) + quantum_register_controls_grid_layout.addWidget( + quantum_register_layout_info_label, quantum_register_controls_grid_row + 1, 0 + ) + quantum_register_controls_grid_row += 1 n_cols_in_quantum_register_controls_grid_layout: int = 3 # TODO: Scroll area @@ -296,7 +345,7 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid input_state_qubit_value_checkbox, one_based_relative_qubit_idx_in_qreg, 1, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) output_state_qubit_value_checkbox = QtWidgets.QCheckBox( @@ -326,11 +375,11 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid output_state_qubit_value_checkbox, one_based_relative_qubit_idx_in_qreg, 2, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) # TODO: How can the column widths of the input fields and the checkbox columns be synced? - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 0) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 1) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) @@ -341,7 +390,7 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid quantum_register_controls_grid_row, 0, 1, - n_cols_in_quantum_register_controls_grid_layout, + n_cols_in_quantum_register_controls_grid_layout + 1, ) # Add a spacer item that will take the remaining horizontal space in the grid layout for each quantum register while vertical resizing should only take the minimum required spacing. @@ -362,10 +411,10 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid 5, ) quantum_register_controls_grid_layout.setColumnStretch(0, 0) - quantum_register_controls_grid_layout.setColumnStretch(1, 0) - quantum_register_controls_grid_layout.setColumnStretch(2, 0) + quantum_register_controls_grid_layout.setColumnStretch(1, 1) + quantum_register_controls_grid_layout.setColumnStretch(2, 1) quantum_register_controls_grid_layout.setColumnStretch(3, 0) - quantum_register_controls_grid_layout.setColumnStretch(4, 2) + quantum_register_controls_grid_layout.setColumnStretch(4, 1) simulation_run_scroll_area = QtWidgets.QScrollArea() simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) @@ -391,6 +440,9 @@ def handle_quantum_register_name_search(self) -> None: qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg_name) ) + qreg_layout_info_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLabel, self.qreg_layout_info_label_name_format.format(qreg_name=qreg_name) + ) qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) ) @@ -406,6 +458,7 @@ def handle_quantum_register_name_search(self) -> None: if ( qreg_name_search_input_field is None or qreg_name_label is None + or qreg_layout_info_label is None or qreg_input_state_input_field is None or qreg_output_state_input_field is None or qreg_edit_qubit_values_toggle_button is None @@ -417,6 +470,7 @@ def handle_quantum_register_name_search(self) -> None: qreg_name_search_input_field.text() ) qreg_name_label.setVisible(should_control_be_visible) + qreg_layout_info_label.setVisible(should_control_be_visible) qreg_input_state_input_field.setVisible(should_control_be_visible) qreg_output_state_input_field.setVisible(should_control_be_visible) qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) @@ -478,7 +532,7 @@ def handle_output_state_qubit_value_checkbox_state_change( if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: self.show_error_msg_dialog( title="Failed to updated qubit value", - error_msg=f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in output state!", + error_msg=f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in output state!", ) return @@ -501,7 +555,7 @@ def handle_output_state_qubit_value_checkbox_state_change( ): self.show_error_msg_dialog( title="Failed to updated qubit value", - error_msg=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in output state to new value{stringified_updated_qubit_value}!", + error_msg=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in output state to new value{stringified_updated_qubit_value}!", ) return @@ -714,3 +768,96 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s ) ) associated_qubit_value_checkbox.setEnabled(not should_reset_output_state) + + def handle_input_or_output_state_text_change( + self, associated_qreg_name: str, expected_qreg_size: int, is_editing_input_state: bool + ) -> None: + input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=associated_qreg_name) + ) + + output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=associated_qreg_name) + ) + + qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=associated_qreg_name), + ) + + expected_output_state_init_button: QtWidgets.QPushButton | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, self.qreg_expected_output_state_init_name_format + ) + + if ( + input_state_text_field is None + or output_state_text_field is None + or qreg_qubit_values_edit_toggle_button is None + or expected_output_state_init_button is None + ): + # TODO: This should not happen + return + + are_stringified_qreg_contents_valid: bool = False + if is_editing_input_state: + are_stringified_qreg_contents_valid = ( + input_state_text_field.hasAcceptableInput() and len(input_state_text_field.text()) == expected_qreg_size + ) + else: + are_stringified_qreg_contents_valid = ( + output_state_text_field.hasAcceptableInput() + and len(output_state_text_field.text()) == expected_qreg_size + ) + + qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) + expected_output_state_init_button.setEnabled(are_stringified_qreg_contents_valid) + + if is_editing_input_state: + output_state_text_field.setEnabled( + are_stringified_qreg_contents_valid + if self.edited_simulation_run_model.expected_output_state is not None + else False + ) + else: + input_state_text_field.setEnabled( + are_stringified_qreg_contents_valid + if self.edited_simulation_run_model.expected_output_state is not None + else False + ) + + # TODO: Disable dialog save button + + for qreg_layout in self.qreg_layouts: + if qreg_layout.qreg_name == associated_qreg_name: + continue + + qreg_name: str = qreg_layout.qreg_name + not_edited_input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + ) + + not_edited_output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + ) + + not_edited_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) + ) + ) + + if ( + not_edited_input_state_text_field is None + or not_edited_output_state_text_field is None + or not_edited_qreg_qubit_values_edit_toggle_button is None + ): + # TODO: This should not happen + return + + not_edited_input_state_text_field.setEnabled(are_stringified_qreg_contents_valid) + not_edited_output_state_text_field.setEnabled( + are_stringified_qreg_contents_valid + if self.edited_simulation_run_model.expected_output_state is not None + else False + ) + not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 60a9627c..1bfa42d0 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -69,7 +69,7 @@ def initialize_expected_output_state_as_copy_of_input_state(self) -> None: self.expected_output_state = syrec.n_bit_values_container(self.input_state.size()) for i in range(self.expected_output_state.size()): - self.expected_output_state.set(self.input_state.test(i)) + self.expected_output_state.set(i, self.input_state.test(i)) def reset_result_of_execution(self) -> None: self.actual_output_state = None From 8e9899fdd226c3ff7284806510807b95c11bcbe9 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 7 Jan 2026 18:56:53 +0100 Subject: [PATCH 17/88] Invalid values for qubits of quantum register will no disable controls of remaining quantum registers as well as disabling the save button of the edit dialog --- .../qt_simulation_run_editor_dialog.py | 150 ++++++++++++++++-- 1 file changed, 137 insertions(+), 13 deletions(-) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index d9504b51..0fb6ff21 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -34,6 +34,8 @@ def stringify_some_qubits_of_n_bit_values_container( class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] + focus_out = QtCore.pyqtSignal(name="focusOut") + def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = None): super().__init__(parent) self.expected_max_num_characters = expected_max_num_characters @@ -50,7 +52,12 @@ def sizeHint(self) -> QtCore.QSize: # noqa: N802 preferred = min(nominal, self.width()) return QtCore.QSize(preferred, sh.height()) + def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 + super().focusOutEvent(ev) + self.focus_out.emit() + +# TODO: Replace 'simple' returns with QDialog.reject("") to indicate fatal errors and stop editing simulation run but also reject changes made in parent widget that opened dialog class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWidgets.QWidget): super().__init__(parent) @@ -100,6 +107,8 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" self.qreg_expected_output_state_init_name_format = "output_state_value_toggle" self.qreg_name_search_input_field_name = "qreg_name_search_input_field" + self.qreg_values_validation_error_lbl_name = "qreg_values_validation_err_lbl" + self.qreg_values_validation_error_format = "Qubit values of quantum register '{qreg_name:s}' can only be defined as a combination of '0' or '1' literals. Additionally, the value of all qubits of the quantum register (n={expected_num_qubit_values:d}) must be specified but only {actual_num_qubit_values:d} were defined in the {input_or_output_state_ident:s} state!" # TODO: How can we determine whether qubits are readonly self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 @@ -192,9 +201,20 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid input_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) # input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) input_state_edit_field.setValidator(n_bit_values_container_contents_validator) - input_state_edit_field.textEdited.connect( - lambda _, - associated_qreg_name=qreg_name, + # The QLineEdit editingFinished signal is only triggered when its input satisfies the set inputmask/validator or when return/enter is pressed or if the QLineEdit loses focus. + # However, during testing entering an invalid quantum register state in the input/output state validation seems to not trigger after entering an invalid value into an QLineEdit + # and moving focus to another item. Only after entering a second invalid state the validation is triggered. Only when confirming the changes with enter/return is the validation triggered immediately. + # One could in a custom overwrite of the QLineEdit class use the focusOutEvent to emit a custom signal when the QLineEdit element looses focus or use the application-level focusChanged signal to determine + # whether the QLineEdit widget lost focus. + input_state_edit_field.editingFinished.connect( + lambda associated_qreg_name=qreg_name, + expected_text_length=n_qubits_of_qreg, + is_editing_input_state=True: self.handle_input_or_output_state_text_change( + associated_qreg_name, expected_text_length, is_editing_input_state + ) + ) + input_state_edit_field.focusOut.connect( + lambda associated_qreg_name=qreg_name, expected_text_length=n_qubits_of_qreg, is_editing_input_state=True: self.handle_input_or_output_state_text_change( associated_qreg_name, expected_text_length, is_editing_input_state @@ -219,9 +239,15 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid output_state_edit_field.setCursorPosition(0) output_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) output_state_edit_field.setValidator(n_bit_values_container_contents_validator) - output_state_edit_field.textEdited.connect( - lambda _, - associated_qreg_name=qreg_name, + output_state_edit_field.editingFinished.connect( + lambda associated_qreg_name=qreg_name, + expected_text_length=n_qubits_of_qreg, + is_editing_input_state=False: self.handle_input_or_output_state_text_change( + associated_qreg_name, expected_text_length, is_editing_input_state + ) + ) + output_state_edit_field.focusOut.connect( + lambda associated_qreg_name=qreg_name, expected_text_length=n_qubits_of_qreg, is_editing_input_state=False: self.handle_input_or_output_state_text_change( associated_qreg_name, expected_text_length, is_editing_input_state @@ -421,15 +447,19 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid simulation_run_scroll_area.setWidgetResizable(True) main_layout.addWidget(simulation_run_scroll_area) + qreg_values_validation_error_label = QtWidgets.QLabel(objectName=self.qreg_values_validation_error_lbl_name) + qreg_values_validation_error_label.setStyleSheet("QLabel { color : red; }") + main_layout.addWidget(qreg_values_validation_error_label) + # Add dialog control buttons and link signals to slots of dialog - dialog_button_box = QtWidgets.QDialogButtonBox( + self.dialog_button_box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Save | QtWidgets.QDialogButtonBox.StandardButton.Cancel ) - dialog_button_box.setCenterButtons(True) - dialog_button_box.accepted.connect(self.accept) - dialog_button_box.rejected.connect(self.reject) + self.dialog_button_box.setCenterButtons(True) + self.dialog_button_box.accepted.connect(self.accept) + self.dialog_button_box.rejected.connect(self.reject) - main_layout.addWidget(dialog_button_box) + main_layout.addWidget(self.dialog_button_box) def handle_quantum_register_name_search(self) -> None: for qreg_layout in self.qreg_layouts: @@ -789,11 +819,20 @@ def handle_input_or_output_state_text_change( QtWidgets.QPushButton, self.qreg_expected_output_state_init_name_format ) + dialog_save_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Save + ) + qreg_values_validation_error_lbl: QtWidgets.QLabel | None = self.findChild( + QtWidgets.QLabel, self.qreg_values_validation_error_lbl_name + ) + if ( input_state_text_field is None or output_state_text_field is None or qreg_qubit_values_edit_toggle_button is None or expected_output_state_init_button is None + or dialog_save_button is None + or qreg_values_validation_error_lbl is None ): # TODO: This should not happen return @@ -811,6 +850,7 @@ def handle_input_or_output_state_text_change( qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) expected_output_state_init_button.setEnabled(are_stringified_qreg_contents_valid) + dialog_save_button.setEnabled(are_stringified_qreg_contents_valid) if is_editing_input_state: output_state_text_field.setEnabled( @@ -818,6 +858,18 @@ def handle_input_or_output_state_text_change( if self.edited_simulation_run_model.expected_output_state is not None else False ) + + if not are_stringified_qreg_contents_valid: + qreg_values_validation_error_lbl.setText( + self.qreg_values_validation_error_format.format( + qreg_name=associated_qreg_name, + expected_num_qubit_values=expected_qreg_size, + actual_num_qubit_values=len(input_state_text_field.text()), + input_or_output_state_ident="input", + ) + ) + else: + qreg_values_validation_error_lbl.setText("") else: input_state_text_field.setEnabled( are_stringified_qreg_contents_valid @@ -825,11 +877,83 @@ def handle_input_or_output_state_text_change( else False ) - # TODO: Disable dialog save button + if not are_stringified_qreg_contents_valid: + qreg_values_validation_error_lbl.setText( + self.qreg_values_validation_error_format.format( + qreg_name=associated_qreg_name, + expected_num_qubit_values=expected_qreg_size, + actual_num_qubit_values=len(output_state_text_field.text()), + input_or_output_state_ident="output", + ) + ) + else: + qreg_values_validation_error_lbl.setText("") for qreg_layout in self.qreg_layouts: if qreg_layout.qreg_name == associated_qreg_name: - continue + if not are_stringified_qreg_contents_valid: + continue + + first_qubit_of_qreg: int = qreg_layout.first_qubit_of_qreg + n_qubits_of_qreg: int = qreg_layout.qreg_size + + qubit_in_input_or_output_state: int + new_qubit_value: bool + if is_editing_input_state: + for relative_qubit_idx_in_qreg in range(n_qubits_of_qreg): + qubit_in_input_or_output_state = first_qubit_of_qreg + relative_qubit_idx_in_qreg + associated_input_state_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, + self.input_state_qubit_checkbox_name_format.format( + qubit=qubit_in_input_or_output_state + ), + ) + ) + + if associated_input_state_qubit_value_checkbox is None: + # TODO: This should not happen + return + + new_qubit_value = input_state_text_field.text()[relative_qubit_idx_in_qreg] == "1" + self.edited_simulation_run_model.update_input_state_qubit_value( + qubit_in_input_or_output_state, new_qubit_value + ) + associated_input_state_qubit_value_checkbox.setChecked(new_qubit_value) + associated_input_state_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + new_qubit_value, return_as_high_low_state=True + ) + ) + ) + else: + for relative_qubit_idx_in_qreg in range(n_qubits_of_qreg): + qubit_in_input_or_output_state = first_qubit_of_qreg + relative_qubit_idx_in_qreg + associated_output_state_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, + self.output_state_qubit_checkbox_name_format.format( + qubit=qubit_in_input_or_output_state + ), + ) + ) + if associated_output_state_qubit_value_checkbox is None: + # TODO: This should not happen + return + + new_qubit_value = output_state_text_field.text()[relative_qubit_idx_in_qreg] == "1" + self.edited_simulation_run_model.update_expected_output_state_qubit_value( + qubit_in_input_or_output_state, new_qubit_value + ) + associated_output_state_qubit_value_checkbox.setChecked(new_qubit_value) + associated_output_state_qubit_value_checkbox.setText( + self.stringified_qubit_value_format.format( + stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + new_qubit_value, return_as_high_low_state=True + ) + ) + ) qreg_name: str = qreg_layout.qreg_name not_edited_input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( From 823d9b99744fb9d503cbaac880e3f75415ba772b Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 7 Jan 2026 22:52:31 +0100 Subject: [PATCH 18/88] Added warnings for switching between simulation run tabs widget tabs and functionality to clear model as well as generate all possible input state combinations in model --- .../quantum_circuit_simulation_dialog.py | 67 +++++++++++++++++-- .../qt_simulation_run_model.py | 27 ++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 0165f6cd..730169f3 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -35,6 +35,9 @@ def __init__( super().__init__() self.parent = parent self.annotatable_quantum_computation = annotatable_quantum_computation + self.some_sim_runs_tab_widget_name = "some_sim_runs_tab" + self.all_sim_runs_tab_widget_name = "all_sim_runs_tab" + self.load_sim_runs_from_file_tab_widget_name = "load_sim_runs_from_file_tab" self.title = "Define simulation runs for quantum computation" self.setWindowTitle(self.title) @@ -52,15 +55,19 @@ def __init__( # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self.simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self.simulation_runs_model), + self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, self.some_sim_runs_tab_widget_name), "Check some input-output mapping combinations", ) self.simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self.simulation_runs_model), + self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, self.all_sim_runs_tab_widget_name), "Check all input-output mapping combinations", ) self.simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, create_load_from_file_controls=True), + self.initialize_simulation_runs_tab_widget( + self.simulation_runs_model, + self.load_sim_runs_from_file_tab_widget_name, + create_load_from_file_controls=True, + ), "Check input-output mapping combinations from file", ) self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) @@ -78,9 +85,12 @@ def __init__( # TODO: Load from file controls def initialize_simulation_runs_tab_widget( - self, shared_simulation_runs_model: QtSimulationRunModel, create_load_from_file_controls: bool = False + self, + shared_simulation_runs_model: QtSimulationRunModel, + tab_widget_object_name: str, + create_load_from_file_controls: bool = False, ) -> QtWidgets.QWidget: - tab_wrapper_widget = QtWidgets.QFrame() + tab_wrapper_widget = QtWidgets.QFrame(objectName=tab_widget_object_name) tab_wrapper_widget_layout = QtWidgets.QVBoxLayout() tab_wrapper_widget.setLayout(tab_wrapper_widget_layout) tab_wrapper_widget.setAutoFillBackground(True) @@ -328,6 +338,53 @@ def generate_some_simulation_runs( shared_simulation_runs_model.add_simulation_run_model(sim_run_model) def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: + if self.simulation_runs_tab_widget.currentIndex() == clicked_on_tab_index: + self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) + return + + if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: + pressed_message_box_button_in_tab_switch_warning: QtWidgets.QMessageBox.StandardButton = ( + QtWidgets.QMessageBox.warning( + self, + "Existing simulation runs detected!", + "Switching tabs will delete all existing simulation runs. Do you want to continue?", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + ) + + if pressed_message_box_button_in_tab_switch_warning == QtWidgets.QMessageBox.StandardButton.Cancel: + self.simulation_runs_tab_widget.currentIndex(self.simulation_runs_tab_widget.currentIndex()) + return + self.simulation_runs_model.delete_all_simulation_run_models() + + to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + clicked_on_tab_index + ) + if to_be_switched_to_tab_widget is None: + self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) + return + + if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: + n_input_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits + pressed_message_box_button_in_all_sim_run_generation_warning: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( + self, + "Generating all possible input state combinations!", + f"Are you sure that you want to generate {n_input_combinations} simulation runs, one for each input state combination?", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + if ( + pressed_message_box_button_in_all_sim_run_generation_warning + == QtWidgets.QMessageBox.StandardButton.Cancel + ): + self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) + return + + # TODO: Can we ignore return value? + self.simulation_runs_model.add_all_possible_simulation_run_models() + self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) self.set_enabled_state_of_simulation_runs_execution_controls(False) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 1bfa42d0..e4ce2015 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -129,6 +129,7 @@ def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtCore.QObject = None ): super().__init__(parent) + self.n_data_qubits: int = annotatable_quantum_computation.num_data_qubits self.simulation_run_models: list[SimulationRunModel] = [] self.quantum_register_layouts: list[QuantumRegisterLayout] = ( QtSimulationRunModel.__record_quantum_register_layouts(annotatable_quantum_computation) @@ -218,6 +219,32 @@ def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: self.endRemoveRows() return False + def delete_all_simulation_run_models(self) -> None: + self.beginResetModel() + self.simulation_run_models.clear() + self.endResetModel() + + def add_all_possible_simulation_run_models(self) -> bool: + if self.rowCount(QtCore.QModelIndex()) > 0: + return False + + self.beginInsertRows(QtCore.QModelIndex(), 0, 0) + for i in range(2**self.n_data_qubits): + binary_string_of_i = format(i, "b") + input_state = syrec.n_bit_values_container(self.n_data_qubits) + + n_qubits_to_process_in_binary_string: int = min(self.n_data_qubits, len(binary_string_of_i)) + qubit_idx_in_binary_string: int = n_qubits_to_process_in_binary_string - 1 + for qubit in range(n_qubits_to_process_in_binary_string): + qubit_value: bool = binary_string_of_i[qubit_idx_in_binary_string] == "1" + input_state.set(qubit, qubit_value) + qubit_idx_in_binary_string -= 1 + + output_state: syrec.n_bit_values_container | None = None + self.simulation_run_models.append(SimulationRunModel(input_state, output_state)) + self.endInsertRows() + return True + # TODO: Check that no duplicate input or expected output_state is added # TODO: Add custom error messages if validation fails def update_simulation_run_model( From b28c0a096c21c1a06dcfaab1df68a04b578a0a77 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 8 Jan 2026 15:35:25 +0100 Subject: [PATCH 19/88] Implemented basic thread communication between UI and simulation run execution thread --- .../quantum_circuit_simulation_dialog.py | 102 +++++++++++----- .../qt_simulation_run_dialog.py | 115 ++++++++++++++++++ .../qt_simulation_run_model.py | 5 + .../simulation_view/qt_simulation_worker.py | 112 +++++++++++++++++ 4 files changed, 301 insertions(+), 33 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_worker.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 730169f3..37525720 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -14,6 +14,7 @@ from mqt import syrec +from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog from .simulation_view.qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel from .simulation_view.qt_simulation_run_styled_item_delegate import SimulationRunModelStyledItemDelegate @@ -51,6 +52,7 @@ def __init__( self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) + self.simulation_run_dialog: SimulationRunDialog | None = None # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) @@ -172,6 +174,7 @@ def initialize_simulation_runs_tab_widget( objectName=RUN_SIM_RUNS_BTN_NAME, ) run_simulation_runs_button.setEnabled(False) + run_simulation_runs_button.clicked.connect(self.handle_run_all_simulation_runs_button_click) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_button) run_simulation_runs_stop_at_first_failure_button = QtWidgets.QPushButton( @@ -180,6 +183,9 @@ def initialize_simulation_runs_tab_widget( objectName=RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME, ) run_simulation_runs_stop_at_first_failure_button.setEnabled(False) + run_simulation_runs_stop_at_first_failure_button.clicked.connect( + self.handle_run_all_simulation_runs_stop_at_first_failure_button_click + ) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_stop_at_first_failure_button) simulation_runs_execution_buttons_layout.addStretch() @@ -217,6 +223,9 @@ def handle_simulation_run_selection_change( add_simulation_run_btn.setEnabled(not is_list_item_selected) edit_simulation_run_btn.setEnabled(is_list_item_selected) delete_simulation_run_btn.setEnabled(is_list_item_selected) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, not is_list_item_selected + ) def handle_simulation_run_add_btn_click(self) -> None: if not self.simulation_runs_model.add_simulation_run_model( @@ -227,12 +236,14 @@ def handle_simulation_run_add_btn_click(self) -> None: ): return - self.set_enabled_state_of_simulation_runs_execution_controls(True) - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() if curr_active_tab_widget is None: return + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, True + ) + simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME ) @@ -293,8 +304,12 @@ def handle_simulation_run_delete_btn_click(self) -> None: if not self.simulation_runs_model.delete_simulation_run_model(simulation_runs_list_view.currentIndex()): return - self.set_enabled_state_of_simulation_runs_execution_controls( - self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0 + # Deletion of an element should only be enabled when an item in the QListView is selected. After the deletion + # and the subsequent update of the backing model of the QListView selection will switch to the element the index + # of the previously selected element thus the simulation run execution controls should not be enabled after an element + # is deleted + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, False ) @staticmethod @@ -342,6 +357,12 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) return + curr_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + if curr_tab_widget is None: + return + if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: pressed_message_box_button_in_tab_switch_warning: QtWidgets.QMessageBox.StandardButton = ( QtWidgets.QMessageBox.warning( @@ -365,6 +386,10 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) return + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_tab_widget, False + ) + if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: n_input_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits pressed_message_box_button_in_all_sim_run_generation_warning: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( @@ -381,25 +406,56 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index ): self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) return - + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + to_be_switched_to_tab_widget, True + ) # TODO: Can we ignore return value? self.simulation_runs_model.add_all_possible_simulation_run_models() + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_tab_widget, False + ) + self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) - self.set_enabled_state_of_simulation_runs_execution_controls(False) - def set_enabled_state_of_simulation_runs_execution_controls(self, should_controls_be_enabled: bool) -> None: - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: + def handle_run_all_simulation_runs_button_click(self) -> None: + if self.simulation_run_dialog is not None: + # TODO: Error logging? return - run_simulation_runs_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) + self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) + self.simulation_run_dialog.start_simulations( + self.annotatable_quantum_computation, stop_at_first_output_state_mismatch=False + ) + self.simulation_run_dialog.show() + + def handle_run_all_simulation_runs_stop_at_first_failure_button_click(self) -> None: + if self.simulation_run_dialog is not None: + # TODO: Error logging? + return + + self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) + self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) + self.simulation_run_dialog.start_simulations( + self.annotatable_quantum_computation, stop_at_first_output_state_mismatch=True + ) + self.simulation_run_dialog.show() + + def handle_simulation_runs_dialog_close(self) -> None: + self.simulation_run_dialog = None + + @staticmethod + def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + tab_widget: QtWidgets.QWidget, should_controls_be_enabled: bool + ) -> None: + run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_NAME ) - run_simulation_runs_stop_at_first_failure_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + run_simulation_runs_stop_at_first_failure_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME ) - save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, SAVE_SIM_RUNS_TO_FILE_BTN_NAME ) @@ -412,25 +468,5 @@ def set_enabled_state_of_simulation_runs_execution_controls(self, should_control run_simulation_runs_btn.setEnabled(should_controls_be_enabled) run_simulation_runs_stop_at_first_failure_btn.setEnabled(should_controls_be_enabled) + # TODO: Button should only be enabled if all simulation runs have their expected output state set? save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) - - def handle_simulation_run_input_state_qubit_value_change( - self, simulation_run_number: int, qubit: int, new_qubit_value: bool - ) -> None: - pass - - def handle_simulation_run_output_state_qubit_value_change( - self, simulation_run_number: int, qubit: int, new_qubit_value: bool - ) -> None: - pass - - def handle_simulation_run_deletion_request(self, simulation_run_number: int) -> None: - if simulation_run_number < 0 or simulation_run_number >= len(self.defined_simulation_runs): - # TODO: Log error? - return - - current_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if current_tab_widget is None: - # TODO: This should not happen - return - # self.defined_simulation_runs.pop(simulation_run_number) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py new file mode 100644 index 00000000..cc6c30d0 --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -0,0 +1,115 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PyQt6 import QtCore, QtWidgets + +if TYPE_CHECKING: + from mqt import syrec + + from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel + from .qt_simulation_worker import SimulationRunResult + +from .qt_simulation_worker import SimulationWorker, ToBeExecutedSimulationRun + + +class SimulationRunDialog(QtWidgets.QDialog): # type: ignore[misc] + def __init__(self, simulation_run_model: QtSimulationRunModel, parent: QtWidgets.QWidget): + super().__init__(parent) + + self.setModal(True) + self.setSizeGripEnabled(True) + self.setWindowTitle("Executing simulation runs") + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + # TODO: Member variable could also be initialized in start_simulations + self.simulation_run_model = simulation_run_model + self.worker_thread: QtCore.QThread | None = None + self.worker: SimulationWorker | None = None + + def start_simulations( + self, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + stop_at_first_output_state_mismatch: bool, + ) -> None: + self.worker_thread = QtCore.QThread() + self.worker = SimulationWorker(annotatable_quantum_computation, stop_at_first_output_state_mismatch) + + # TODO: It is recommended in the official documentation to mark slots explicitly via the QtCore.pyqtSlot() decorator: + # see https://doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html#the-slot-class + + # Do not block the UI thread by the potentially long running operations of the worker a new thread is started (which also has its own event loop) + # and the worker operation moved to the latter. We also do not want to block the UI thread by executing the slots of said worker in the UI thread but + # instead want to simply send the events to the event queue of its thread thus the QueuedConnection between the signal (here the UI thread) and the receiver (worker thread) + # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). + self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.allSimulationsDone.connect( + self.handle_all_simulation_runs_done, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker.simulationRunCompleted.connect( + self.handle_simulation_run_done, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker.simulationRunMismatchBetweenOutputStates.connect( + self.handle_simulation_runs_stopped_after_first_failure, QtCore.Qt.ConnectionType.QueuedConnection + ) + + self.worker_thread.finished.connect(self.worker_thread.deleteLater) + self.worker_thread.finished.connect(self.reset_workers) + + self.worker.moveToThread(self.worker_thread) + self.worker_thread.start() + self.enqueue_next_simulation_run(0) + + # TODO: Mark remaining member functions as private via underscore prefix? + def handle_all_simulation_runs_done(self) -> None: + if self.worker_thread is not None: + self.worker_thread.quit() + self.worker_thread.wait() + + def handle_simulation_runs_stopped_after_first_failure( + self, + _: SimulationRunResult, + ) -> None: + + if self.worker is not None: + self.worker.request_cancellation() + + def closeEvent(self, _): # noqa: N802 + if self.worker is not None: + self.worker.request_cancellation() + + # if self.worker_thread is not None: + # self.worker_thread.quit() + # self.worker_thread.wait() + + def handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: + self.enqueue_next_simulation_run(simulation_run_result.simulation_run_number + 1) + + def enqueue_next_simulation_run(self, simulation_run_number: int) -> None: + next_simulation_run: SimulationRunModel | None = self.simulation_run_model.get_simulation_run_model( + simulation_run_number + ) + + if self.worker is None: + return + if next_simulation_run is None: + self.worker.request_cancellation() + else: + self.worker.queue_new_simulation_run( + ToBeExecutedSimulationRun( + simulation_run_number, next_simulation_run.input_state, next_simulation_run.expected_output_state + ) + ) + + def reset_workers(self) -> None: + self.worker_thread = None + self.worker = None diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index e4ce2015..18c68c33 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -199,6 +199,11 @@ def data(self, index: QtCore.QModelIndex, role: int) -> object: return None + def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: + if index >= 0 and index < len(self.simulation_run_models): + return self.simulation_run_models[index] + return None + # TODO: Check for duplicates? def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> bool: n_simulation_runs: int = len(self.simulation_run_models) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_worker.py b/python/mqt/syrec/simulation_view/qt_simulation_worker.py new file mode 100644 index 00000000..5889783c --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_worker.py @@ -0,0 +1,112 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import queue +import time +from dataclasses import dataclass + +from PyQt6 import QtCore + +from mqt import syrec + +# One could simplify the signal and slot declarations by defining the import: from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot + + +@dataclass(frozen=True) +class ToBeExecutedSimulationRun: + simulation_run_number: int + input_state: syrec.n_bit_values_container + expected_output_state: syrec.n_bit_values_container | None + + +@dataclass(frozen=True) +class SimulationRunResult: + simulation_run_number: int + expected_output_state: syrec.n_bit_values_container | None + actual_output_state: syrec.n_bit_values_container + do_expected_and_actual_outputs_match: bool | None + execution_runtime_in_ms: float + + +class SimulationWorker(QtCore.QObject): # type: ignore[misc] + simulation_run_completed = QtCore.pyqtSignal(SimulationRunResult, name="simulationRunCompleted") + simulation_run_mismatch_between_output_states = QtCore.pyqtSignal( + SimulationRunResult, name="simulationRunMismatchBetweenOutputStates" + ) + all_simulations_done = QtCore.pyqtSignal(name="allSimulationsDone") + + def __init__( + self, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + stop_at_first_output_state_mismatch: bool, + ): + super().__init__() + + self.annotatable_quantum_computation = annotatable_quantum_computation + self.cancellation_requested: bool = False + self.simulation_run_queue: queue.SimpleQueue[ToBeExecutedSimulationRun | None] = queue.SimpleQueue() + self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch + + # TODO: Error handling + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_simulations(self) -> None: + while not self.cancellation_requested: + dequeued_element: ToBeExecutedSimulationRun | None = self.simulation_run_queue.get() + if dequeued_element is None: + break + + actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) + simulation_execution_start_time_in_seconds: float = time.time() + # TODO: Do something + simulation_execution_end_time_in_seconds: float = time.time() + + do_expected_and_actual_input_states_match: bool | None = False + simulation_execution_runtime_in_ms: float = ( + simulation_execution_end_time_in_seconds - simulation_execution_start_time_in_seconds + ) / 1000 + + simulation_run_result = SimulationRunResult( + dequeued_element.simulation_run_number, + dequeued_element.expected_output_state, + actual_output_state, + do_expected_and_actual_input_states_match, + simulation_execution_runtime_in_ms, + ) + if self.should_stop_at_first_output_state_mismatch and not do_expected_and_actual_input_states_match: + self.simulation_run_mismatch_between_output_states.emit(simulation_run_result) + else: + self.simulation_run_completed.emit(simulation_run_result) + # time.sleep(1) + + self.all_simulations_done.emit() + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def request_cancellation(self) -> None: + self.cancellation_requested = True + self.simulation_run_queue.put(None) + + # TODO: Throw exceptions on validation errors? + @QtCore.pyqtSlot(ToBeExecutedSimulationRun) # type: ignore[untyped-decorator] + def queue_new_simulation_run(self, to_be_executed_simulation_run: ToBeExecutedSimulationRun) -> bool: + if self.cancellation_requested: + return False + + if to_be_executed_simulation_run.simulation_run_number < 0: + return False + + if ( + to_be_executed_simulation_run.expected_output_state is not None + and to_be_executed_simulation_run.expected_output_state.size() + != to_be_executed_simulation_run.input_state.size() + ): + return False + + self.simulation_run_queue.put(to_be_executed_simulation_run) + return True From 067fb591f4f7cf1170bb1a45646e6642fc2e1aaf Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 8 Jan 2026 22:43:23 +0100 Subject: [PATCH 20/88] Added progress controls and cancellation controls to qt simulation run dialog, fixed extraction of qubit values from n_bit_values_container and added dynamic column width to simulation run QStyledItemDelegate --- .../quantum_circuit_simulation_dialog.py | 17 +- .../qt_simulation_run_dialog.py | 99 ++++++- .../qt_simulation_run_editor_dialog.py | 2 +- .../qt_simulation_run_styled_item_delegate.py | 253 +++++++++++------- .../simulation_view/qt_simulation_worker.py | 68 +++-- 5 files changed, 293 insertions(+), 146 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 37525720..10b677c4 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -419,26 +419,21 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) def handle_run_all_simulation_runs_button_click(self) -> None: - if self.simulation_run_dialog is not None: - # TODO: Error logging? - return - - self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) - self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) - self.simulation_run_dialog.start_simulations( - self.annotatable_quantum_computation, stop_at_first_output_state_mismatch=False - ) - self.simulation_run_dialog.show() + self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=False) def handle_run_all_simulation_runs_stop_at_first_failure_button_click(self) -> None: + self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=True) + + def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_mismatch: bool) -> None: if self.simulation_run_dialog is not None: # TODO: Error logging? return + total_num_simulation_runs: Final[int] = 2**self.annotatable_quantum_computation.num_data_qubits self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) self.simulation_run_dialog.start_simulations( - self.annotatable_quantum_computation, stop_at_first_output_state_mismatch=True + self.annotatable_quantum_computation, total_num_simulation_runs, stop_at_first_output_state_mismatch ) self.simulation_run_dialog.show() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index cc6c30d0..4babdfed 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from PyQt6 import QtCore, QtWidgets @@ -20,27 +20,74 @@ from .qt_simulation_worker import SimulationWorker, ToBeExecutedSimulationRun +TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS: Final[int] = 1000 +TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = "Total runtime [in seconds]: {total_runtime_in_seconds:f}" + class SimulationRunDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__(self, simulation_run_model: QtSimulationRunModel, parent: QtWidgets.QWidget): super().__init__(parent) + # TODO: Member variable could also be initialized in start_simulations + self.simulation_run_model = simulation_run_model + self.worker_thread: QtCore.QThread | None = None + self.worker: SimulationWorker | None = None + + self.num_completed_simulation_runs: int = 0 + self.expected_total_num_simulation_runs: int = 0 + self.setModal(True) self.setSizeGripEnabled(True) self.setWindowTitle("Executing simulation runs") + left = 0 + top = 0 + width = 400 + height = 400 + self.setGeometry(left, top, width, height) + + simulation_progress_layout = QtWidgets.QHBoxLayout() + # self.simulation_run_total_runtime_timer = QtWidgets.QTimer(self) + # self.simulation_run_total_runtime_timer.timeout.connect(self.) + # self.simulation_run_total_runtime_info_label = QtWidgets.QLabel(TOTAL_RUNTIME_TEXT_FORMAT.format(total_runtime_in_seconds=0)) + self.simulation_run_progress_bar = QtWidgets.QProgressBar() + # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format + self.simulation_run_progress_bar.setFormat("Executing simulation run %v of %m") + self.simulation_run_progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.simulation_run_progress_text = QtWidgets.QLabel("") + + # simulation_progress_layout.addWidget(self.simulation_run_total_runtime_info_label) + simulation_progress_layout.addWidget(self.simulation_run_progress_bar) + simulation_progress_layout.addWidget(self.simulation_run_progress_text) + simulation_progress_layout.addStretch() + main_layout = QtWidgets.QVBoxLayout() + main_layout.addStretch() + main_layout.addLayout(simulation_progress_layout) + + # TODO: One could also offer a close button in the dialog (that warns the user when closing the dialog during a simulation run execution)? + self.dialog_button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Cancel) + self.dialog_button_box.setCenterButtons(True) + self.dialog_button_box.rejected.connect(self.request_worker_cancellation) + main_layout.addWidget(self.dialog_button_box) self.setLayout(main_layout) - # TODO: Member variable could also be initialized in start_simulations - self.simulation_run_model = simulation_run_model - self.worker_thread: QtCore.QThread | None = None - self.worker: SimulationWorker | None = None - def start_simulations( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, + expected_total_num_simulation_runs: int, stop_at_first_output_state_mismatch: bool, ) -> None: + self.expected_total_num_simulation_runs = expected_total_num_simulation_runs + self.num_completed_simulation_runs = 0 + + self.simulation_run_progress_text.setText("") + self.simulation_run_progress_bar.setMinimum(0) + self.simulation_run_progress_bar.setMaximum(expected_total_num_simulation_runs - 1) + self.simulation_run_progress_bar.setValue(0) + self.simulation_run_progress_bar.setVisible(True) + + # self.simulation_run_total_runtime_timer.start(TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS) + self.worker_thread = QtCore.QThread() self.worker = SimulationWorker(annotatable_quantum_computation, stop_at_first_output_state_mismatch) @@ -68,30 +115,59 @@ def start_simulations( self.worker.moveToThread(self.worker_thread) self.worker_thread.start() self.enqueue_next_simulation_run(0) + self.change_dialog_cancellation_button_enable_state(True) # TODO: Mark remaining member functions as private via underscore prefix? def handle_all_simulation_runs_done(self) -> None: + # self.simulation_run_total_runtime_timer.stop() + if self.worker_thread is not None: self.worker_thread.quit() self.worker_thread.wait() + self.change_dialog_cancellation_button_enable_state(False) + self.simulation_run_progress_bar.setVisible(False) + + if self.num_completed_simulation_runs == self.expected_total_num_simulation_runs: + self.simulation_run_progress_text.setText( + f"Finished all {self.expected_total_num_simulation_runs} simulation runs!" + ) + else: + self.simulation_run_progress_text.setText( + f"Finished {self.num_completed_simulation_runs} out of all {self.expected_total_num_simulation_runs} simulation runs!" + ) def handle_simulation_runs_stopped_after_first_failure( self, _: SimulationRunResult, ) -> None: + self.request_worker_cancellation() + # self.simulation_run_total_runtime_timer.stop() + def request_worker_cancellation(self) -> None: if self.worker is not None: self.worker.request_cancellation() + self.change_dialog_cancellation_button_enable_state(False) + self.simulation_run_progress_bar.setVisible(False) + + def change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + + if dialog_cancel_button is None: + return + + dialog_cancel_button.setEnabled(should_button_be_enabled) def closeEvent(self, _): # noqa: N802 - if self.worker is not None: - self.worker.request_cancellation() + self.request_worker_cancellation() # if self.worker_thread is not None: # self.worker_thread.quit() # self.worker_thread.wait() def handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: + self.update_progress_controls(simulation_run_result.simulation_run_number) self.enqueue_next_simulation_run(simulation_run_result.simulation_run_number + 1) def enqueue_next_simulation_run(self, simulation_run_number: int) -> None: @@ -102,7 +178,7 @@ def enqueue_next_simulation_run(self, simulation_run_number: int) -> None: if self.worker is None: return if next_simulation_run is None: - self.worker.request_cancellation() + self.request_worker_cancellation() else: self.worker.queue_new_simulation_run( ToBeExecutedSimulationRun( @@ -113,3 +189,8 @@ def enqueue_next_simulation_run(self, simulation_run_number: int) -> None: def reset_workers(self) -> None: self.worker_thread = None self.worker = None + + def update_progress_controls(self, completed_simulation_run: int) -> None: + self.simulation_run_progress_text.setText(f"Completed simulation run {completed_simulation_run}") + self.simulation_run_progress_bar.setValue(self.num_completed_simulation_runs) + self.num_completed_simulation_runs += 1 diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index 0fb6ff21..808e931c 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -27,7 +27,7 @@ def stringify_some_qubits_of_n_bit_values_container( n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int ) -> str: - if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): + if first_qubit >= n_bit_values_container.size() or first_qubit + (n_qubits - 1) >= n_bit_values_container.size(): return "" return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py index 3c461078..fc6172e9 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py @@ -87,21 +87,20 @@ def _get_estimated_quantum_register_name_column_width( ), ) + @staticmethod def _get_estimated_quantum_register_contents_column_width( - self, - option: QtWidgets.QStyleOptionViewItem, - largest_quantum_register_size_in_qubits: int, - font_size: int, - with_leading_whitespace: bool, + option: QtWidgets.QStyleOptionViewItem, largest_quantum_register_size_in_qubits: int, font_size: int ) -> int: - return ( - 2 * self.stringified_quantum_register_x_spacing if with_leading_whitespace else 0 - ) + SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( + text_width_for_largest_qreg: int = SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( "".join(["0" for i in range(largest_quantum_register_size_in_qubits)]), option, font_size ) + text_width_for_unknown_qreg_content: int = SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( + "", option, font_size + ) + # We can ignore the text width of the headers of the INPUT and OUTPUT columns since the placeholder text for unknown quantum register contents is larger than both header texts + return max(text_width_for_largest_qreg, text_width_for_unknown_qreg_content) - # TODO: Long quantum registers that cause the total width to be larger than the containing bounding rect should be truncated (i.e. with a text ellipsis) with total estimated content width truncated to max. width of containing bounding rectangle? - def _get_estimated_bounding_rect( + def _get_required_size_for_content( self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex ) -> QtCore.QSize: if not index.isValid(): @@ -142,36 +141,34 @@ def _get_estimated_bounding_rect( self.input_state_value_column_header, option, self.simulation_run_group_box_content_font_size ) - max_qreg_qubits_column_width: int = self._get_estimated_quantum_register_contents_column_width( - option, - index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - self.simulation_run_group_box_content_font_size, - True, + max_qreg_qubits_column_width: int = ( + SimulationRunModelStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + self.simulation_run_group_box_content_font_size, + ) ) max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) - - max_per_qreg_content_column_width_with_spacing: int = ( - self.stringified_quantum_register_x_spacing - + max_qreg_content_column_width - + self.stringified_quantum_register_x_spacing - + max_qreg_content_column_width - ) - total_simulation_run_group_box_width = ( self.simulation_run_contents_padding_size + qreg_name_and_layout_info_column_width - + max_per_qreg_content_column_width_with_spacing - + max_per_qreg_content_column_width_with_spacing + + self.stringified_quantum_register_x_spacing + + max_qreg_content_column_width + + self.stringified_quantum_register_x_spacing + + self.stringified_quantum_register_x_spacing + + max_qreg_content_column_width + + self.stringified_quantum_register_x_spacing + self.simulation_run_contents_padding_size ) - return QtCore.QSize( - min(total_simulation_run_group_box_width, option.rect.bottomRight().x()), - max(total_simulation_run_group_box_height, option.rect.topRight().y()), - ) + return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - return self._get_estimated_bounding_rect(option, index) + required_content_size: QtCore.QSize = self._get_required_size_for_content(option, index) + return QtCore.QSize( + min(option.rect.bottomRight().x(), required_content_size.width()), + max(option.rect.bottomRight().y(), required_content_size.height()), + ) @staticmethod def _paint_rect_edge_points( @@ -196,24 +193,97 @@ def _paint_rect_edge_points( def _stringify_some_qubits_of_n_bit_values_container( n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int ) -> str: - if first_qubit >= n_bit_values_container.size() or first_qubit + n_qubits >= n_bit_values_container.size(): + if ( + first_qubit >= n_bit_values_container.size() + or first_qubit + (n_qubits - 1) >= n_bit_values_container.size() + ): return "" return "".join([ "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) ]) - # TODO: - # @staticmethod - # def _get_elided_text_for_pixel_width( - # text: str, text_font: QtGui.QFontMetrics, available_pixel_width_for_text: int - # ) -> str: - # return text_font.elidedText(text, QtCore.Qt.TextElideMode.Qt.ElideRight, available_pixel_width_for_text) + @staticmethod + def _get_column_width_scaled_by_ratio_to_total_available_width( + required_column_width: int, total_required_width: int, total_available_width: int + ) -> int: + return int(float(required_column_width / total_required_width) * total_available_width) + + def _draw_elided_text( + self: QtGui.QPainter, text: str, text_rect: QtCore.QRect, draw_as_bold_text: bool = False + ) -> None: + if draw_as_bold_text: + self.save() + bold_font = QtGui.QFont(self.font().family(), self.font().pointSize()) + bold_font.setBold(True) + self.setFont(bold_font) + + font_metrics: QtCore.QFontMetrics = self.fontMetrics() + available_column_width: int = text_rect.width() + elided_text: str = font_metrics.elidedText(text, QtCore.Qt.TextElideMode.ElideRight, available_column_width) + + self.drawText( + text_rect, + QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + elided_text, + ) + + if draw_as_bold_text: + self.restore() def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: - if not index.isValid(): + if not index.isValid() or option.rect.width() == 0: return + required_size_for_content: QtCore.QSize = self._get_required_size_for_content(option, index) + available_rect_for_content: QtCore.QRect = option.rect.adjusted( + self.simulation_run_contents_padding_size, + self.simulation_run_contents_padding_size, + -self.simulation_run_contents_padding_size, + -self.simulation_run_contents_padding_size, + ) + + qreg_name_and_layout_info_column_width: int = ( + self._get_estimated_quantum_register_name_column_width( + option, index, self.simulation_run_group_box_content_font_size + ) + + self.stringified_quantum_register_x_spacing + ) + input_state_qreg_content_column_width: int = ( + self.stringified_quantum_register_x_spacing + + SimulationRunModelStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + self.simulation_run_group_box_content_font_size, + ) + + self.stringified_quantum_register_x_spacing + ) + output_state_qreg_content_column_width: int = input_state_qreg_content_column_width + + if required_size_for_content.width() > available_rect_for_content.topRight().x(): + total_required_width_for_content: int = required_size_for_content.width() + available_width_for_content: int = available_rect_for_content.topRight().x() + + qreg_name_and_layout_info_column_width = ( + SimulationRunModelStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( + qreg_name_and_layout_info_column_width, + total_required_width_for_content, + available_width_for_content, + ) + ) + input_state_qreg_content_column_width = ( + SimulationRunModelStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( + input_state_qreg_content_column_width, total_required_width_for_content, available_width_for_content + ) + ) + output_state_qreg_content_column_width = ( + SimulationRunModelStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( + output_state_qreg_content_column_width, + total_required_width_for_content, + available_width_for_content, + ) + ) + associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) painter.save() @@ -221,51 +291,33 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, SimulationRunModelStyledItemDelegate._paint_rect_edge_points( painter, option.rect, 5, QtCore.Qt.GlobalColor.cyan, index ) - painter.drawRoundedRect(option.rect, 3, 3) - - simulation_run_contents_rect = option.rect.adjusted( - self.simulation_run_contents_padding_size, - self.simulation_run_contents_padding_size, - -self.simulation_run_contents_padding_size, - -self.simulation_run_contents_padding_size, - ) SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, simulation_run_contents_rect, 5, QtCore.Qt.GlobalColor.red, index + painter, available_rect_for_content, 5, QtCore.Qt.GlobalColor.red, index ) + painter.drawRoundedRect(option.rect, 3, 3) if QtWidgets.QStyle.StateFlag.State_Selected in option.state: painter.fillRect(option.rect, option.palette.highlight()) painter.setBrush(option.palette.highlightedText()) + # BEGIN Draw card header painter.save() header_font = QtGui.QFont(painter.font().family(), self.simulation_run_group_box_title_font_size) header_font.setBold(True) painter.setFont(header_font) header_title = "Simulation run #" + str(index.row() + 1) - header_title_height: int = SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_title_font_size ) - painter.drawText(simulation_run_contents_rect.x(), simulation_run_contents_rect.y(), header_title) + painter.drawText(available_rect_for_content.x(), available_rect_for_content.y(), header_title) painter.restore() - - header_text_rect = QtCore.QRect( - simulation_run_contents_rect.topLeft().x(), - simulation_run_contents_rect.topLeft().y(), - SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( - header_title, option, self.simulation_run_group_box_title_font_size - ), - header_title_height, - ) - - qreg_name_column_width: int = self._get_estimated_quantum_register_name_column_width( - option, index, self.simulation_run_group_box_content_font_size - ) + # END Draw card header header_row_column_one_rect = QtCore.QRect( - header_text_rect.topLeft().x(), - header_text_rect.topLeft().y() + 2 * self.simulation_run_title_bottom_margin_y, - qreg_name_column_width, + available_rect_for_content.topLeft().x(), + available_rect_for_content.topLeft().y() + 2 * self.simulation_run_title_bottom_margin_y, + qreg_name_and_layout_info_column_width, SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_content_font_size ), @@ -273,23 +325,14 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, header_row_column_one_text_rect = header_row_column_one_rect.adjusted( self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 ) - # TODO: What if header text is larger than contents? - painter.drawText( - header_row_column_one_text_rect, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - self.quantum_register_name_column_header, + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, self.quantum_register_name_column_header, header_row_column_one_text_rect, draw_as_bold_text=True ) - max_qreg_content_width: int = self._get_estimated_quantum_register_contents_column_width( - option, - index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - self.simulation_run_group_box_content_font_size, - False, - ) header_row_column_two_rect = QtCore.QRect( header_row_column_one_rect.topRight().x(), header_row_column_one_rect.topRight().y(), - max_qreg_content_width, + input_state_qreg_content_column_width, SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_content_font_size ), @@ -297,17 +340,14 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, header_row_column_two_text_rect = header_row_column_two_rect.adjusted( self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 ) - # TODO: What if header text is larger than contents? - painter.drawText( - header_row_column_two_text_rect, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - self.input_state_value_column_header, + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, self.input_state_value_column_header, header_row_column_two_text_rect, draw_as_bold_text=True ) header_row_column_three_rect = QtCore.QRect( header_row_column_two_rect.topRight().x(), header_row_column_two_rect.topRight().y(), - max_qreg_content_width, + output_state_qreg_content_column_width, SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( option, self.simulation_run_group_box_content_font_size ), @@ -315,11 +355,8 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, header_row_column_three_text_rect = header_row_column_three_rect.adjusted( self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 ) - # TODO: What if header text is larger than contents? - painter.drawText( - header_row_column_three_text_rect, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - self.output_state_value_column_header, + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, self.output_state_value_column_header, header_row_column_three_text_rect, draw_as_bold_text=True ) SimulationRunModelStyledItemDelegate._paint_rect_edge_points( @@ -342,28 +379,37 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): curr_row_y_offset: int = row_idx * per_row_y_offset - row_i_column_one = header_row_column_one_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) + row_i_column_one_rect: QtCore.QRect = header_row_column_one_text_rect.adjusted( + 0, curr_row_y_offset, 0, curr_row_y_offset + ) + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, qreg_layout.qreg_name, row_i_column_one_rect + ) + painter.drawText( - row_i_column_one, + row_i_column_one_rect, QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, qreg_layout.qreg_name, ) - row_i_column_two = header_row_column_two_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) - painter.drawText( - row_i_column_two, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + row_i_column_two_rect: QtCore.QRect = header_row_column_two_text_rect.adjusted( + 0, curr_row_y_offset, 0, curr_row_y_offset + ) + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( associated_input_output_mapping.input_state, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, ), + row_i_column_two_rect, ) - row_i_column_three = header_row_column_three_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset) - painter.drawText( - row_i_column_three, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + row_i_column_three_rect: QtCore.QRect = header_row_column_three_text_rect.adjusted( + 0, curr_row_y_offset, 0, curr_row_y_offset + ) + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( associated_input_output_mapping.expected_output_state, qreg_layout.first_qubit_of_qreg, @@ -371,6 +417,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, ) if associated_input_output_mapping.expected_output_state is not None else self.unknown_output_state_value_placeholder, + row_i_column_three_rect, ) painter.save() @@ -380,13 +427,15 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter.setPen(QtCore.Qt.GlobalColor.gray) painter.setFont(quantum_layout_info_text_font) - row_i_plus_column_one = row_i_column_one.adjusted(0, per_row_y_offset, 0, per_row_y_offset) - painter.drawText( - row_i_plus_column_one, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, + row_i_plus_column_one_rect: QtCore.QRect = row_i_column_one_rect.adjusted( + 0, per_row_y_offset, 0, per_row_y_offset + ) + SimulationRunModelStyledItemDelegate._draw_elided_text( + painter, self.quantum_register_layout_text_format.format( first_qubit=qreg_layout.first_qubit_of_qreg, n_qubits=qreg_layout.qreg_size ), + row_i_plus_column_one_rect, ) painter.restore() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_worker.py b/python/mqt/syrec/simulation_view/qt_simulation_worker.py index 5889783c..305de03b 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_worker.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_worker.py @@ -41,6 +41,7 @@ class SimulationWorker(QtCore.QObject): # type: ignore[misc] SimulationRunResult, name="simulationRunMismatchBetweenOutputStates" ) all_simulations_done = QtCore.pyqtSignal(name="allSimulationsDone") + simulation_run_failed = QtCore.pyqtSignal(ToBeExecutedSimulationRun, name="simulationRunFailed") def __init__( self, @@ -54,7 +55,6 @@ def __init__( self.simulation_run_queue: queue.SimpleQueue[ToBeExecutedSimulationRun | None] = queue.SimpleQueue() self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch - # TODO: Error handling @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: while not self.cancellation_requested: @@ -62,28 +62,50 @@ def start_simulations(self) -> None: if dequeued_element is None: break - actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) - simulation_execution_start_time_in_seconds: float = time.time() - # TODO: Do something - simulation_execution_end_time_in_seconds: float = time.time() - - do_expected_and_actual_input_states_match: bool | None = False - simulation_execution_runtime_in_ms: float = ( - simulation_execution_end_time_in_seconds - simulation_execution_start_time_in_seconds - ) / 1000 - - simulation_run_result = SimulationRunResult( - dequeued_element.simulation_run_number, - dequeued_element.expected_output_state, - actual_output_state, - do_expected_and_actual_input_states_match, - simulation_execution_runtime_in_ms, - ) - if self.should_stop_at_first_output_state_mismatch and not do_expected_and_actual_input_states_match: - self.simulation_run_mismatch_between_output_states.emit(simulation_run_result) - else: - self.simulation_run_completed.emit(simulation_run_result) - # time.sleep(1) + try: + simulation_execution_start_time_in_seconds: float = time.time() + actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) + # simulation_run_execution_statistics = syrec.statistics() + + actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) + # syrec.simple_simulation(actual_output_state, self.annotatable_quantum_computation, dequeued_element.input_state, simulation_run_execution_statistics) + syrec.simple_simulation( + actual_output_state, self.annotatable_quantum_computation, dequeued_element.input_state + ) + + do_expected_and_actual_input_states_match: bool | None = None + if dequeued_element.expected_output_state is not None: + for qubit in range(actual_output_state.size()): + do_expected_and_actual_input_states_match |= actual_output_state.test( + qubit + ) == dequeued_element.expected_output_state.test(qubit) + if not do_expected_and_actual_input_states_match: + break + + simulation_execution_end_time_in_seconds: float = time.time() + simulation_execution_runtime_in_ms: float = ( + simulation_execution_end_time_in_seconds - simulation_execution_start_time_in_seconds + ) / 1000 + + simulation_run_result = SimulationRunResult( + dequeued_element.simulation_run_number, + dequeued_element.expected_output_state, + actual_output_state, + do_expected_and_actual_input_states_match, + simulation_execution_runtime_in_ms, + ) + if ( + self.should_stop_at_first_output_state_mismatch + and do_expected_and_actual_input_states_match is not None + and not do_expected_and_actual_input_states_match + ): + self.simulation_run_mismatch_between_output_states.emit(simulation_run_result) + else: + self.simulation_run_completed.emit(simulation_run_result) + # time.sleep(1) + except Exception: + self.simulation_run_failed.emit(dequeued_element) + break self.all_simulations_done.emit() From ce68abe1c0c9c49c56b91bb0ec058d9c90a6e718 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 9 Jan 2026 14:24:53 +0100 Subject: [PATCH 21/88] Refactored simulation run overview delegate by moving shared code into base class and 'promoted' constants from class member variables to free variables --- .../quantum_circuit_simulation_dialog.py | 6 +- .../qt_simulation_run_styled_item_delegate.py | 443 ------------------ ...ase_simulation_run_styled_item_delegate.py | 134 ++++++ ...ation_run_overview_styled_item_delegate.py | 373 +++++++++++++++ 4 files changed, 511 insertions(+), 445 deletions(-) delete mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py create mode 100644 python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py create mode 100644 python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 10b677c4..41f49690 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -17,7 +17,9 @@ from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog from .simulation_view.qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel -from .simulation_view.qt_simulation_run_styled_item_delegate import SimulationRunModelStyledItemDelegate +from .simulation_view.styled_item_delegates.qt_simulation_run_overview_styled_item_delegate import ( + SimulationRunOverviewStyledItemDelegate, +) LOADED_FROM_FILE_INPUT_FIELD_NAME: Final[str] = "load_from_file_input_field" ADD_SIM_RUN_BTN_NAME: Final[str] = "add_sim_run_btn" @@ -107,7 +109,7 @@ def initialize_simulation_runs_tab_widget( # BEGIN: Create simulation runs list view Qt elements simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView(objectName=SIMULATION_RUNS_LIST_VIEW_NAME) simulation_runs_list_view.setModel(shared_simulation_runs_model) - simulation_runs_list_view.setItemDelegate(SimulationRunModelStyledItemDelegate()) # type: ignore[no-untyped-call] + simulation_runs_list_view.setItemDelegate(SimulationRunOverviewStyledItemDelegate()) # type: ignore[no-untyped-call] simulation_runs_list_view.setUniformItemSizes(True) simulation_runs_list_view.setAutoFillBackground(False) simulation_runs_list_view.setSpacing(5) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py deleted file mode 100644 index fc6172e9..00000000 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_styled_item_delegate.py +++ /dev/null @@ -1,443 +0,0 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -from __future__ import annotations - -from typing import TYPE_CHECKING, Final - -from PyQt6 import QtCore, QtGui, QtWidgets - -from .qt_simulation_run_model import ( - LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, - LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, - LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE, - QUANTUM_REGISTER_LAYOUT_QT_ROLE, - SIMULATION_RUN_IO_STATE_QT_ROLE, -) - -if TYPE_CHECKING: - from mqt import syrec - - from .qt_simulation_run_model import ( - SimulationRunModel, - ) - - -# Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html -class SimulationRunModelStyledItemDelegate(QtWidgets.QStyledItemDelegate): # type: ignore[misc] - def __init__(self, parent=None): - super().__init__(parent) - - # TODO: Mark as const: https://stackoverflow.com/a/57596202 - self.simulation_run_group_box_title_font_size: Final[int] = 14 - self.simulation_run_group_box_content_font_size: Final[int] = 10 - self.quantum_register_layout_info_text_font_size: Final[int] = 8 - self.simulation_run_title_bottom_margin_y: Final[int] = 8 - self.stringified_quantum_register_y_spacing: Final[int] = 4 - self.stringified_quantum_register_x_spacing: Final[int] = 6 - self.simulation_run_contents_padding_size: Final[int] = 20 - self.simulation_run_group_box_y_spacing: Final[int] = 10 - - self.quantum_register_layout_text_format = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" - self.quantum_register_name_column_header = "Quantum register" - self.input_state_value_column_header = "INPUT" - self.output_state_value_column_header = "OUTPUT" - self.unknown_output_state_value_placeholder = "" - - @staticmethod - def _get_text_width_for_font_size(text: str, options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: - return int( - QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).horizontalAdvance( - text - ) - ) - - @staticmethod - def _get_text_height_for_font_size(options: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: - return int(QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height()) - - def _get_estimated_quantum_register_name_column_width( - self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int - ) -> int: - if not index.isValid(): - return 0 - - index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) - largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) - largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) - - return (2 * self.stringified_quantum_register_x_spacing) + max( - SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( - self.quantum_register_name_column_header, option, font_size - ), - SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( - index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option, font_size - ), - SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( - self.quantum_register_layout_text_format.format( - first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size - ), - option, - font_size, - ), - ) - - @staticmethod - def _get_estimated_quantum_register_contents_column_width( - option: QtWidgets.QStyleOptionViewItem, largest_quantum_register_size_in_qubits: int, font_size: int - ) -> int: - text_width_for_largest_qreg: int = SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( - "".join(["0" for i in range(largest_quantum_register_size_in_qubits)]), option, font_size - ) - text_width_for_unknown_qreg_content: int = SimulationRunModelStyledItemDelegate._get_text_width_for_font_size( - "", option, font_size - ) - # We can ignore the text width of the headers of the INPUT and OUTPUT columns since the placeholder text for unknown quantum register contents is larger than both header texts - return max(text_width_for_largest_qreg, text_width_for_unknown_qreg_content) - - def _get_required_size_for_content( - self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex - ) -> QtCore.QSize: - if not index.isValid(): - return QtCore.QSize(0, 0) - - n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) - # Quantum register contents are displayed as two rows containing the following information: - # R0: - # R1: - group_box_title_height: int = SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_title_font_size - ) - - qreg_contents_text_height: int = ( - self.stringified_quantum_register_y_spacing - + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_content_font_size - ) - + self.stringified_quantum_register_y_spacing - + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.quantum_register_layout_info_text_font_size - ) - ) - total_qreg_contents_text_height: int = n_qregs * qreg_contents_text_height - total_simulation_run_group_box_height = ( - self.simulation_run_contents_padding_size - + group_box_title_height - + self.simulation_run_title_bottom_margin_y - + total_qreg_contents_text_height - + self.simulation_run_contents_padding_size - ) - - qreg_name_and_layout_info_column_width: int = self._get_estimated_quantum_register_name_column_width( - option, index, self.simulation_run_group_box_title_font_size - ) - - qreg_content_header_width: int = self._get_text_width_for_font_size( - self.input_state_value_column_header, option, self.simulation_run_group_box_content_font_size - ) - - max_qreg_qubits_column_width: int = ( - SimulationRunModelStyledItemDelegate._get_estimated_quantum_register_contents_column_width( - option, - index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - self.simulation_run_group_box_content_font_size, - ) - ) - - max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) - total_simulation_run_group_box_width = ( - self.simulation_run_contents_padding_size - + qreg_name_and_layout_info_column_width - + self.stringified_quantum_register_x_spacing - + max_qreg_content_column_width - + self.stringified_quantum_register_x_spacing - + self.stringified_quantum_register_x_spacing - + max_qreg_content_column_width - + self.stringified_quantum_register_x_spacing - + self.simulation_run_contents_padding_size - ) - return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) - - def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - required_content_size: QtCore.QSize = self._get_required_size_for_content(option, index) - return QtCore.QSize( - min(option.rect.bottomRight().x(), required_content_size.width()), - max(option.rect.bottomRight().y(), required_content_size.height()), - ) - - @staticmethod - def _paint_rect_edge_points( - painter: QtGui.QPainter, rect: QtCore.QRect, font_size: int, color: QtGui.QColor, index: QtCore.QModelIndex - ) -> None: - painter.save() - custom_pen = QtGui.QPen(color) - custom_pen.setWidth(font_size) - painter.setPen(custom_pen) - - painter.drawPoint(QtCore.QPoint(rect.topLeft())) - painter.drawText(rect.topLeft().x(), rect.topLeft().y(), str(index.row()) + "-TL") - painter.drawPoint(QtCore.QPoint(rect.topRight())) - painter.drawText(rect.topRight().x(), rect.topRight().y(), str(index.row()) + "-TR") - painter.drawPoint(QtCore.QPoint(rect.bottomLeft())) - painter.drawText(rect.bottomLeft().x(), rect.bottomLeft().y(), str(index.row()) + "-BL") - painter.drawPoint(QtCore.QPoint(rect.bottomRight())) - painter.drawText(rect.bottomRight().x(), rect.bottomRight().y(), str(index.row()) + "-BR") - painter.restore() - - @staticmethod - def _stringify_some_qubits_of_n_bit_values_container( - n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int - ) -> str: - if ( - first_qubit >= n_bit_values_container.size() - or first_qubit + (n_qubits - 1) >= n_bit_values_container.size() - ): - return "" - - return "".join([ - "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) - ]) - - @staticmethod - def _get_column_width_scaled_by_ratio_to_total_available_width( - required_column_width: int, total_required_width: int, total_available_width: int - ) -> int: - return int(float(required_column_width / total_required_width) * total_available_width) - - def _draw_elided_text( - self: QtGui.QPainter, text: str, text_rect: QtCore.QRect, draw_as_bold_text: bool = False - ) -> None: - if draw_as_bold_text: - self.save() - bold_font = QtGui.QFont(self.font().family(), self.font().pointSize()) - bold_font.setBold(True) - self.setFont(bold_font) - - font_metrics: QtCore.QFontMetrics = self.fontMetrics() - available_column_width: int = text_rect.width() - elided_text: str = font_metrics.elidedText(text, QtCore.Qt.TextElideMode.ElideRight, available_column_width) - - self.drawText( - text_rect, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - elided_text, - ) - - if draw_as_bold_text: - self.restore() - - def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: - if not index.isValid() or option.rect.width() == 0: - return - - required_size_for_content: QtCore.QSize = self._get_required_size_for_content(option, index) - available_rect_for_content: QtCore.QRect = option.rect.adjusted( - self.simulation_run_contents_padding_size, - self.simulation_run_contents_padding_size, - -self.simulation_run_contents_padding_size, - -self.simulation_run_contents_padding_size, - ) - - qreg_name_and_layout_info_column_width: int = ( - self._get_estimated_quantum_register_name_column_width( - option, index, self.simulation_run_group_box_content_font_size - ) - + self.stringified_quantum_register_x_spacing - ) - input_state_qreg_content_column_width: int = ( - self.stringified_quantum_register_x_spacing - + SimulationRunModelStyledItemDelegate._get_estimated_quantum_register_contents_column_width( - option, - index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - self.simulation_run_group_box_content_font_size, - ) - + self.stringified_quantum_register_x_spacing - ) - output_state_qreg_content_column_width: int = input_state_qreg_content_column_width - - if required_size_for_content.width() > available_rect_for_content.topRight().x(): - total_required_width_for_content: int = required_size_for_content.width() - available_width_for_content: int = available_rect_for_content.topRight().x() - - qreg_name_and_layout_info_column_width = ( - SimulationRunModelStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( - qreg_name_and_layout_info_column_width, - total_required_width_for_content, - available_width_for_content, - ) - ) - input_state_qreg_content_column_width = ( - SimulationRunModelStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( - input_state_qreg_content_column_width, total_required_width_for_content, available_width_for_content - ) - ) - output_state_qreg_content_column_width = ( - SimulationRunModelStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( - output_state_qreg_content_column_width, - total_required_width_for_content, - available_width_for_content, - ) - ) - - associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) - - painter.save() - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, option.rect, 5, QtCore.Qt.GlobalColor.cyan, index - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, available_rect_for_content, 5, QtCore.Qt.GlobalColor.red, index - ) - painter.drawRoundedRect(option.rect, 3, 3) - - if QtWidgets.QStyle.StateFlag.State_Selected in option.state: - painter.fillRect(option.rect, option.palette.highlight()) - painter.setBrush(option.palette.highlightedText()) - - # BEGIN Draw card header - painter.save() - header_font = QtGui.QFont(painter.font().family(), self.simulation_run_group_box_title_font_size) - header_font.setBold(True) - painter.setFont(header_font) - - header_title = "Simulation run #" + str(index.row() + 1) - SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_title_font_size - ) - painter.drawText(available_rect_for_content.x(), available_rect_for_content.y(), header_title) - painter.restore() - # END Draw card header - - header_row_column_one_rect = QtCore.QRect( - available_rect_for_content.topLeft().x(), - available_rect_for_content.topLeft().y() + 2 * self.simulation_run_title_bottom_margin_y, - qreg_name_and_layout_info_column_width, - SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_content_font_size - ), - ) - header_row_column_one_text_rect = header_row_column_one_rect.adjusted( - self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, self.quantum_register_name_column_header, header_row_column_one_text_rect, draw_as_bold_text=True - ) - - header_row_column_two_rect = QtCore.QRect( - header_row_column_one_rect.topRight().x(), - header_row_column_one_rect.topRight().y(), - input_state_qreg_content_column_width, - SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_content_font_size - ), - ) - header_row_column_two_text_rect = header_row_column_two_rect.adjusted( - self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, self.input_state_value_column_header, header_row_column_two_text_rect, draw_as_bold_text=True - ) - - header_row_column_three_rect = QtCore.QRect( - header_row_column_two_rect.topRight().x(), - header_row_column_two_rect.topRight().y(), - output_state_qreg_content_column_width, - SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_content_font_size - ), - ) - header_row_column_three_text_rect = header_row_column_three_rect.adjusted( - self.stringified_quantum_register_x_spacing, 0, -self.stringified_quantum_register_x_spacing, 0 - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, self.output_state_value_column_header, header_row_column_three_text_rect, draw_as_bold_text=True - ) - - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, header_row_column_one_rect, 5, QtCore.Qt.GlobalColor.red, index - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, header_row_column_two_rect, 5, QtCore.Qt.GlobalColor.blue, index - ) - SimulationRunModelStyledItemDelegate._paint_rect_edge_points( - painter, header_row_column_three_rect, 5, QtCore.Qt.GlobalColor.green, index - ) - - row_idx: int = 1 - per_row_y_offset: int = ( - self.stringified_quantum_register_y_spacing - + SimulationRunModelStyledItemDelegate._get_text_height_for_font_size( - option, self.simulation_run_group_box_content_font_size - ) - ) - for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): - curr_row_y_offset: int = row_idx * per_row_y_offset - - row_i_column_one_rect: QtCore.QRect = header_row_column_one_text_rect.adjusted( - 0, curr_row_y_offset, 0, curr_row_y_offset - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, qreg_layout.qreg_name, row_i_column_one_rect - ) - - painter.drawText( - row_i_column_one_rect, - QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, - qreg_layout.qreg_name, - ) - - row_i_column_two_rect: QtCore.QRect = header_row_column_two_text_rect.adjusted( - 0, curr_row_y_offset, 0, curr_row_y_offset - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, - SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.input_state, - qreg_layout.first_qubit_of_qreg, - qreg_layout.qreg_size, - ), - row_i_column_two_rect, - ) - - row_i_column_three_rect: QtCore.QRect = header_row_column_three_text_rect.adjusted( - 0, curr_row_y_offset, 0, curr_row_y_offset - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, - SimulationRunModelStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.expected_output_state, - qreg_layout.first_qubit_of_qreg, - qreg_layout.qreg_size, - ) - if associated_input_output_mapping.expected_output_state is not None - else self.unknown_output_state_value_placeholder, - row_i_column_three_rect, - ) - - painter.save() - quantum_layout_info_text_font = QtGui.QFont( - painter.font().family(), self.quantum_register_layout_info_text_font_size - ) - painter.setPen(QtCore.Qt.GlobalColor.gray) - painter.setFont(quantum_layout_info_text_font) - - row_i_plus_column_one_rect: QtCore.QRect = row_i_column_one_rect.adjusted( - 0, per_row_y_offset, 0, per_row_y_offset - ) - SimulationRunModelStyledItemDelegate._draw_elided_text( - painter, - self.quantum_register_layout_text_format.format( - first_qubit=qreg_layout.first_qubit_of_qreg, n_qubits=qreg_layout.qreg_size - ), - row_i_plus_column_one_rect, - ) - painter.restore() - - row_idx += 2 - painter.restore() diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py new file mode 100644 index 00000000..f1a69d66 --- /dev/null +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -0,0 +1,134 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore, QtGui + +if TYPE_CHECKING: + from PyQt6 import QtWidgets + + from mqt import syrec + +DEFAULT_QREG_NAME_COLUMN_HEADER: Final[str] = "Quantum register" +DEFAULT_QREG_LAYOUT_TEXT_FORMAT: Final[str] = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" +DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT: Final[str] = "Simulation run #{simulation_run_number:d}" +DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "INPUT" +DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "OUTPUT" +DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT: Final[str] = "" + + +class BaseSimulationRunStyledItemDelegate: + @staticmethod + def _get_pixel_width_of_text(text: str, font_used_to_draw_text: QtGui.QFont, expected_font_size: int) -> int: + return int( + QtGui.QFontMetrics( + QtGui.QFont(font_used_to_draw_text.family(), expected_font_size, font_used_to_draw_text.weight()) + ).horizontalAdvance(text) + ) + + @staticmethod + def _get_pixel_height_of_text(font_used_to_draw_text: QtGui.QFont, expected_font_size: int) -> int: + return int( + QtGui.QFontMetrics( + QtGui.QFont(font_used_to_draw_text.family(), expected_font_size, font_used_to_draw_text.weight()) + ).height() + ) + + @staticmethod + def _stringify_some_qubits_of_n_bit_values_container( + n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int + ) -> str: + last_qubit_of_qreg: int = first_qubit + (n_qubits - 1) + + if first_qubit >= n_bit_values_container.size() or last_qubit_of_qreg >= n_bit_values_container.size(): + return "" + + return "".join([ + "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, last_qubit_of_qreg + 1) + ]) + + @staticmethod + def _get_estimated_quantum_register_contents_column_width( + option: QtWidgets.QStyleOptionViewItem, largest_quantum_register_size_in_qubits: int, font_size: int + ) -> int: + text_width_for_largest_qreg: int = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + "".join(["0" for i in range(largest_quantum_register_size_in_qubits)]), option.font, font_size + ) + text_width_for_unknown_qreg_content: int = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + "", option.font, font_size + ) + # We can ignore the text width of the headers of the INPUT and OUTPUT columns since the placeholder text for unknown quantum register contents is larger than both header texts + return max(text_width_for_largest_qreg, text_width_for_unknown_qreg_content) + + @staticmethod + def _get_column_width_scaled_by_ratio_to_total_available_width( + required_column_width: int, total_required_width: int, total_available_width: int + ) -> int: + return int(float(required_column_width / total_required_width) * total_available_width) + + @staticmethod + def _scale_column_widths_based_on_ratio_to_total_available_width( + required_column_widths: list[int], total_available_width: int + ) -> list[int]: + total_required_column_widths: int = sum(required_column_widths) + return [ + BaseSimulationRunStyledItemDelegate._get_column_width_scaled_by_ratio_to_total_available_width( + r_col_width, total_required_column_widths, total_available_width + ) + for r_col_width in required_column_widths + ] + + @staticmethod + def _draw_elided_text( + painter: QtGui.QPainter, + text: str, + text_rect: QtCore.QRect, + font_size: int, + draw_bold_text: bool = False, + text_alignment: QtCore.Qt.AlignmentFlag = QtCore.Qt.AlignmentFlag.AlignTop + | QtCore.Qt.AlignmentFlag.AlignCenter, + text_color: QtCore.Qt.GlobalColor = QtCore.Qt.GlobalColor.black, + ) -> None: + painter.save() + bold_font = QtGui.QFont(painter.font().family(), font_size) + bold_font.setBold(draw_bold_text) + painter.setFont(bold_font) + painter.setPen(text_color) + + font_metrics: QtCore.QFontMetrics = painter.fontMetrics() + available_column_width: int = text_rect.width() + elided_text: str = font_metrics.elidedText(text, QtCore.Qt.TextElideMode.ElideRight, available_column_width) + + painter.drawText( + text_rect, + text_alignment, + elided_text, + ) + painter.restore() + + @staticmethod + def _paint_rect_edge_points( + painter: QtGui.QPainter, rect: QtCore.QRect, font_size: int, color: QtGui.QColor, simulation_run_number: int + ) -> None: + painter.save() + custom_pen = QtGui.QPen(color) + custom_pen.setWidth(font_size) + painter.setPen(custom_pen) + + painter.drawPoint(QtCore.QPoint(rect.topLeft())) + painter.drawText(rect.topLeft().x(), rect.topLeft().y(), str(simulation_run_number) + "-TL") + painter.drawPoint(QtCore.QPoint(rect.topRight())) + painter.drawText(rect.topRight().x(), rect.topRight().y(), str(simulation_run_number) + "-TR") + painter.drawPoint(QtCore.QPoint(rect.bottomLeft())) + painter.drawText(rect.bottomLeft().x(), rect.bottomLeft().y(), str(simulation_run_number) + "-BL") + painter.drawPoint(QtCore.QPoint(rect.bottomRight())) + painter.drawText(rect.bottomRight().x(), rect.bottomRight().y(), str(simulation_run_number) + "-BR") + painter.restore() diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py new file mode 100644 index 00000000..d16c63cf --- /dev/null +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py @@ -0,0 +1,373 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore, QtGui, QtWidgets + +from ..qt_simulation_run_model import ( + LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, + LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, + LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE, + QUANTUM_REGISTER_LAYOUT_QT_ROLE, + SIMULATION_RUN_IO_STATE_QT_ROLE, +) +from .base_simulation_run_styled_item_delegate import ( + DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, + DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER, + DEFAULT_QREG_LAYOUT_TEXT_FORMAT, + DEFAULT_QREG_NAME_COLUMN_HEADER, + DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT, + DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, + BaseSimulationRunStyledItemDelegate, +) + +if TYPE_CHECKING: + from ..qt_simulation_run_model import ( + SimulationRunModel, + ) + +GROUP_BOX_TITLE_FONT_SIZE: Final[int] = 14 +GROUP_BOX_CONTENT_FONT_SIZE: Final[int] = 10 +QREG_LAYOUT_INFO_FONT_SIZE: Final[int] = 8 +GROUP_BOX_TITLE_BOTTOM_Y_MARGIN: Final[int] = 8 +QREG_CONTENT_Y_SPACING: Final[int] = 4 +QREG_CONTENT_X_SPACING: Final[int] = 6 +GROUP_BOX_CONTENT_PADDING: Final[int] = 20 + + +# Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html +class SimulationRunOverviewStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] + def __init__(self, parent=None): + super().__init__(parent) + + # TODO: Mark as const: https://stackoverflow.com/a/57596202 + + @staticmethod + def _get_required_qreg_name_and_layout_column_width( + option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int + ) -> int: + if not index.isValid(): + return 0 + + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) + largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) + largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) + + return (2 * QREG_CONTENT_X_SPACING) + max( + SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_QREG_NAME_COLUMN_HEADER, option.font, font_size + ), + SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option.font, font_size + ), + SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_QREG_LAYOUT_TEXT_FORMAT.format( + first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size + ), + option.font, + font_size, + ), + ) + + @staticmethod + def _get_required_size_for_content( + option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex + ) -> QtCore.QSize: + if not index.isValid(): + return QtCore.QSize(0, 0) + + n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + # Quantum register contents are displayed as two rows containing the following information: + # R0: + # R1: + group_box_title_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, GROUP_BOX_TITLE_FONT_SIZE + ) + + qreg_contents_text_height: int = ( + QREG_CONTENT_Y_SPACING + + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, GROUP_BOX_CONTENT_FONT_SIZE + ) + + QREG_CONTENT_Y_SPACING + + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, QREG_LAYOUT_INFO_FONT_SIZE) + ) + column_header_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, QREG_LAYOUT_INFO_FONT_SIZE + ) + total_qreg_contents_text_height: int = n_qregs * qreg_contents_text_height + total_simulation_run_group_box_height = ( + GROUP_BOX_CONTENT_PADDING + + group_box_title_height + + GROUP_BOX_TITLE_BOTTOM_Y_MARGIN + + column_header_height + + total_qreg_contents_text_height + + GROUP_BOX_CONTENT_PADDING + ) + + qreg_name_and_layout_info_column_width: int = ( + SimulationRunOverviewStyledItemDelegate._get_required_qreg_name_and_layout_column_width( + option, index, GROUP_BOX_TITLE_FONT_SIZE + ) + ) + + qreg_content_header_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, option.font, GROUP_BOX_CONTENT_FONT_SIZE + ) + + max_qreg_qubits_column_width: int = ( + SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + GROUP_BOX_CONTENT_FONT_SIZE, + ) + ) + + max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) + total_simulation_run_group_box_width = ( + GROUP_BOX_CONTENT_PADDING + + qreg_name_and_layout_info_column_width + + QREG_CONTENT_X_SPACING + + max_qreg_content_column_width + + QREG_CONTENT_X_SPACING + + QREG_CONTENT_X_SPACING + + max_qreg_content_column_width + + QREG_CONTENT_X_SPACING + + GROUP_BOX_CONTENT_PADDING + ) + return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) + + @staticmethod + def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 + required_content_size: QtCore.QSize = SimulationRunOverviewStyledItemDelegate._get_required_size_for_content( + option, index + ) + return QtCore.QSize( + min(option.rect.bottomRight().x(), required_content_size.width()), + min(option.rect.bottomRight().y(), required_content_size.height()), + ) + + def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: + if not index.isValid() or option.rect.width() == 0: + return + + associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) + available_rect_for_content: QtCore.QRect = option.rect.adjusted( + GROUP_BOX_CONTENT_PADDING, + GROUP_BOX_CONTENT_PADDING, + -GROUP_BOX_CONTENT_PADDING, + -GROUP_BOX_CONTENT_PADDING, + ) + + painter.save() + header_text_bottom_left_point: QtCore.QPoint = self._draw_card_border_and_header( + painter, option, simulation_run_number=index.row(), card_content_rect=available_rect_for_content + ) + + header_column_rects: list[QtCore.QRect] = self._draw_and_determine_column_headers( + painter, option, index, header_text_bottom_left_point, available_rect_for_content.width() + ) + header_row_column_one_text_rect = header_column_rects[0] + header_row_column_two_text_rect = header_column_rects[1] + header_row_column_three_text_rect = header_column_rects[2] + + row_idx: int = 1 + per_row_y_offset: int = ( + QREG_CONTENT_Y_SPACING + + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, GROUP_BOX_CONTENT_FONT_SIZE + ) + ) + for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): + curr_row_y_offset: int = row_idx * per_row_y_offset + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + qreg_layout.qreg_name, + header_row_column_one_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), + GROUP_BOX_CONTENT_FONT_SIZE, + ) + + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.input_state, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, + ), + header_row_column_two_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), + GROUP_BOX_CONTENT_FONT_SIZE, + ) + + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.expected_output_state, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, + ) + if associated_input_output_mapping.expected_output_state is not None + else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, + header_row_column_three_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), + GROUP_BOX_CONTENT_FONT_SIZE, + ) + + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_QREG_LAYOUT_TEXT_FORMAT.format( + first_qubit=qreg_layout.first_qubit_of_qreg, n_qubits=qreg_layout.qreg_size + ), + header_row_column_one_text_rect.adjusted( + 0, curr_row_y_offset + per_row_y_offset, 0, curr_row_y_offset + per_row_y_offset + ), + font_size=QREG_LAYOUT_INFO_FONT_SIZE, + text_color=QtCore.Qt.GlobalColor.gray, + ) + row_idx += 2 + painter.restore() + + @staticmethod + def _draw_card_border_and_header( + painter: QtGui.QPainter, + option: QtWidgets.QStyleOptionViewItem, + simulation_run_number: int, + card_content_rect: QtCore.QRect, + draw_rect_corners: bool = False, + ) -> QtCore.QPoint: + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + if draw_rect_corners: + SimulationRunOverviewStyledItemDelegate._paint_rect_edge_points( + painter, option.rect, 5, QtCore.Qt.GlobalColor.cyan, simulation_run_number + ) + SimulationRunOverviewStyledItemDelegate._paint_rect_edge_points( + painter, card_content_rect, 5, QtCore.Qt.GlobalColor.red, simulation_run_number + ) + painter.drawRoundedRect(option.rect, 3, 3) + + if QtWidgets.QStyle.StateFlag.State_Selected in option.state: + painter.fillRect(option.rect, option.palette.highlight()) + painter.setBrush(option.palette.highlightedText()) + + header_text: str = DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT.format(simulation_run_number=simulation_run_number) + header_text_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + header_text, option.font, GROUP_BOX_TITLE_FONT_SIZE + ) + header_text_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, GROUP_BOX_TITLE_FONT_SIZE + ) + header_text_rect = QtCore.QRect( + card_content_rect.x(), card_content_rect.y(), header_text_width + 10, header_text_height + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, header_text, header_text_rect, GROUP_BOX_TITLE_FONT_SIZE, draw_bold_text=True + ) + return header_text_rect.bottomLeft() + + def _draw_and_determine_column_headers( + self, + painter: QtGui.QPainter, + option: QtWidgets.QStyleOptionViewItem, + index: QtCore.QModelIndex, + header_text_bottom_left_point: QtCore.QPoint, + available_content_width: int, + draw_rect_corners: bool = False, + ) -> list[QtCore.QRect]: + qreg_name_and_layout_info_column_width: int = ( + self._get_required_qreg_name_and_layout_column_width(option, index, GROUP_BOX_CONTENT_FONT_SIZE) + + QREG_CONTENT_X_SPACING + ) + input_state_qreg_content_column_width: int = ( + QREG_CONTENT_X_SPACING + + SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + GROUP_BOX_CONTENT_FONT_SIZE, + ) + + QREG_CONTENT_X_SPACING + ) + output_state_qreg_content_column_width: int = input_state_qreg_content_column_width + + scaled_column_widths: list[int] = ( + SimulationRunOverviewStyledItemDelegate._scale_column_widths_based_on_ratio_to_total_available_width( + [ + qreg_name_and_layout_info_column_width, + input_state_qreg_content_column_width, + output_state_qreg_content_column_width, + ], + available_content_width, + ) + ) + qreg_name_and_layout_info_column_width = scaled_column_widths[0] + input_state_qreg_content_column_width = scaled_column_widths[1] + output_state_qreg_content_column_width = scaled_column_widths[2] + + header_row_column_one_rect = QtCore.QRect( + header_text_bottom_left_point.x(), + header_text_bottom_left_point.y() + 2 * GROUP_BOX_TITLE_BOTTOM_Y_MARGIN, + qreg_name_and_layout_info_column_width, + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, GROUP_BOX_CONTENT_FONT_SIZE), + ) + header_row_column_one_text_rect: QtCore.QRect = header_row_column_one_rect.adjusted( + QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_QREG_NAME_COLUMN_HEADER, + header_row_column_one_text_rect, + GROUP_BOX_CONTENT_FONT_SIZE, + draw_bold_text=True, + ) + + header_row_column_two_rect = QtCore.QRect( + header_row_column_one_rect.topRight().x(), + header_row_column_one_rect.topRight().y(), + input_state_qreg_content_column_width, + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, GROUP_BOX_CONTENT_FONT_SIZE), + ) + header_row_column_two_text_rect: QtCore.QRect = header_row_column_two_rect.adjusted( + QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, + header_row_column_two_text_rect, + GROUP_BOX_CONTENT_FONT_SIZE, + draw_bold_text=True, + ) + + header_row_column_three_rect = QtCore.QRect( + header_row_column_two_rect.topRight().x(), + header_row_column_two_rect.topRight().y(), + output_state_qreg_content_column_width, + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, GROUP_BOX_CONTENT_FONT_SIZE), + ) + header_row_column_three_text_rect: QtCore.QRect = header_row_column_three_rect.adjusted( + QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER, + header_row_column_three_text_rect, + GROUP_BOX_CONTENT_FONT_SIZE, + draw_bold_text=True, + ) + + if draw_rect_corners: + SimulationRunOverviewStyledItemDelegate._paint_rect_edge_points( + painter, header_row_column_one_rect, 5, QtCore.Qt.GlobalColor.red, index.row() + ) + SimulationRunOverviewStyledItemDelegate._paint_rect_edge_points( + painter, header_row_column_two_rect, 5, QtCore.Qt.GlobalColor.blue, index.row() + ) + SimulationRunOverviewStyledItemDelegate._paint_rect_edge_points( + painter, header_row_column_three_rect, 5, QtCore.Qt.GlobalColor.green, index.row() + ) + return [header_row_column_one_text_rect, header_row_column_two_text_rect, header_row_column_three_text_rect] From 4f29d10acbd2212b4feda841544762bcc9bab06d Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 11 Jan 2026 22:36:49 +0100 Subject: [PATCH 22/88] Added simulation run execution styled item delegate and small bugfixes regarding simulation run tab widget as well as card headers in styled item delegates --- .../quantum_circuit_simulation_dialog.py | 73 ++- .../qt_simulation_run_dialog.py | 146 +++-- .../qt_simulation_run_model.py | 87 ++- .../simulation_view/qt_simulation_worker.py | 24 +- ...ase_simulation_run_styled_item_delegate.py | 22 +- ...tion_run_execution_styled_item_delegate.py | 549 ++++++++++++++++++ ...ation_run_overview_styled_item_delegate.py | 124 ++-- 7 files changed, 858 insertions(+), 167 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 41f49690..dfa06048 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -8,6 +8,7 @@ from __future__ import annotations +import sys from typing import Final from PyQt6 import QtCore, QtGui, QtWidgets @@ -57,7 +58,9 @@ def __init__( self.simulation_run_dialog: SimulationRunDialog | None = None # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) + self.prev_active_simulation_runs_tab_idx: int = 0 self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) + self.simulation_runs_tab_widget.currentChanged.connect(self.handle_simulation_runs_tab_widget_tab_changed) self.simulation_runs_tab_widget.addTab( self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, self.some_sim_runs_tab_widget_name), "Check some input-output mapping combinations", @@ -74,7 +77,6 @@ def __init__( ), "Check input-output mapping combinations from file", ) - self.simulation_runs_tab_widget.tabBarClicked.connect(self.handle_simulation_runs_tab_widget_tab_bar_clicked) n_simulation_runs_to_add: Final[int] = 10 QuantumCircuitSimulationDialog.generate_some_simulation_runs( @@ -197,6 +199,7 @@ def initialize_simulation_runs_tab_widget( # END: Create simulation runs execution Qt elements return tab_wrapper_widget + # TODO: After edit of simulation run is finished via click on save button will cause the run simulations buttons to only be executed after selecting/deselecting the element def handle_simulation_run_selection_change( self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection ) -> None: @@ -226,7 +229,8 @@ def handle_simulation_run_selection_change( edit_simulation_run_btn.setEnabled(is_list_item_selected) delete_simulation_run_btn.setEnabled(is_list_item_selected) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, not is_list_item_selected + curr_active_tab_widget, + not is_list_item_selected and (self.simulation_runs_model.rowCount(QtCore.QModelIndex()) < sys.maxsize), ) def handle_simulation_run_add_btn_click(self) -> None: @@ -275,7 +279,7 @@ def handle_simulation_run_editor_dialog_close(self, result: int) -> None: return try: - self.simulation_runs_model.update_simulation_run_model( + self.simulation_runs_model.update_edited_simulation_run_model( self.simulation_run_editor_dialog.simulation_run_model_index, self.simulation_run_editor_dialog.edited_simulation_run_model, ) @@ -354,15 +358,25 @@ def generate_some_simulation_runs( shared_simulation_runs_model.add_simulation_run_model(sim_run_model) - def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index: int) -> None: - if self.simulation_runs_tab_widget.currentIndex() == clicked_on_tab_index: - self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) + # TODO: Check that number of generate simulation runs does not exceed sys.maxsize + def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: int) -> None: + if switched_to_tab_index == -1: return - curr_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + if switched_to_tab_index == self.prev_active_simulation_runs_tab_idx: + return + + prev_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.prev_active_simulation_runs_tab_idx ) - if curr_tab_widget is None: + if prev_active_tab_widget is None: + return + + to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + switched_to_tab_index + ) + if to_be_switched_to_tab_widget is None: + self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: @@ -377,23 +391,17 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index ) if pressed_message_box_button_in_tab_switch_warning == QtWidgets.QMessageBox.StandardButton.Cancel: - self.simulation_runs_tab_widget.currentIndex(self.simulation_runs_tab_widget.currentIndex()) + self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return self.simulation_runs_model.delete_all_simulation_run_models() - to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - clicked_on_tab_index - ) - if to_be_switched_to_tab_widget is None: - self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) - return - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_tab_widget, False + to_be_switched_to_tab_widget, False ) if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: n_input_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits + # TODO: A large number of state combinations will lag the UI thread since the generation runs on the UI thread pressed_message_box_button_in_all_sim_run_generation_warning: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( self, "Generating all possible input state combinations!", @@ -406,7 +414,7 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index pressed_message_box_button_in_all_sim_run_generation_warning == QtWidgets.QMessageBox.StandardButton.Cancel ): - self.simulation_runs_tab_widget.setCurrentIndex(self.simulation_runs_tab_widget.currentIndex()) + self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( to_be_switched_to_tab_widget, True @@ -415,10 +423,9 @@ def handle_simulation_runs_tab_widget_tab_bar_clicked(self, clicked_on_tab_index self.simulation_runs_model.add_all_possible_simulation_run_models() QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_tab_widget, False + prev_active_tab_widget, False ) - - self.simulation_runs_tab_widget.setCurrentIndex(clicked_on_tab_index) + self.prev_active_simulation_runs_tab_idx = switched_to_tab_index def handle_run_all_simulation_runs_button_click(self) -> None: self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=False) @@ -431,14 +438,32 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma # TODO: Error logging? return - total_num_simulation_runs: Final[int] = 2**self.annotatable_quantum_computation.num_data_qubits + num_simulation_runs: Final[int] = self.simulation_runs_model.rowCount(QtCore.QModelIndex()) + if num_simulation_runs >= sys.maxsize: + QtWidgets.QMessageBox.critical( + self, + "Number of simulation runs not supported!", + f"The maximum number of simulation runs is limited to {sys.maxsize} while you tried to execute {num_simulation_runs}!", + buttons=QtWidgets.QMessageBox.StandardButton.Ok, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + curr_tab_widget: QtWidgets.QWidget = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_tab_widget, False + ) + return + self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) self.simulation_run_dialog.start_simulations( - self.annotatable_quantum_computation, total_num_simulation_runs, stop_at_first_output_state_mismatch + self.annotatable_quantum_computation, num_simulation_runs, stop_at_first_output_state_mismatch ) self.simulation_run_dialog.show() + # TODO: Toggle state after edits in simulation runs were performed? def handle_simulation_runs_dialog_close(self) -> None: self.simulation_run_dialog = None diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index 4babdfed..63576eb9 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -19,22 +19,26 @@ from .qt_simulation_worker import SimulationRunResult from .qt_simulation_worker import SimulationWorker, ToBeExecutedSimulationRun +from .styled_item_delegates.qt_simulation_run_execution_styled_item_delegate import ( + SimulationRunExecutionStyledItemDelegate, +) TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS: Final[int] = 1000 TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = "Total runtime [in seconds]: {total_runtime_in_seconds:f}" class SimulationRunDialog(QtWidgets.QDialog): # type: ignore[misc] - def __init__(self, simulation_run_model: QtSimulationRunModel, parent: QtWidgets.QWidget): + def __init__(self, shared_simulation_run_model: QtSimulationRunModel, parent: QtWidgets.QWidget): super().__init__(parent) # TODO: Member variable could also be initialized in start_simulations - self.simulation_run_model = simulation_run_model + self.simulation_runs_model = shared_simulation_run_model self.worker_thread: QtCore.QThread | None = None self.worker: SimulationWorker | None = None self.num_completed_simulation_runs: int = 0 self.expected_total_num_simulation_runs: int = 0 + self.did_simulation_run_fail_due_to_failure: bool = False self.setModal(True) self.setSizeGripEnabled(True) @@ -45,7 +49,41 @@ def __init__(self, simulation_run_model: QtSimulationRunModel, parent: QtWidgets height = 400 self.setGeometry(left, top, width, height) - simulation_progress_layout = QtWidgets.QHBoxLayout() + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + simulation_runs_list_layout = QtWidgets.QHBoxLayout() + simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() + simulation_runs_list_view.setModel(self.simulation_runs_model) + simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) # type: ignore[no-untyped-call] + simulation_runs_list_view.setUniformItemSizes(True) + simulation_runs_list_view.setAutoFillBackground(False) + simulation_runs_list_view.setSpacing(5) + simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) + simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + # simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) + + simulation_runs_list_scrollarea = QtWidgets.QScrollArea() + simulation_runs_list_scrollarea.setAutoFillBackground(False) + simulation_runs_list_scrollarea.setWidget(simulation_runs_list_view) + simulation_runs_list_scrollarea.setWidgetResizable(True) + + simulation_runs_list_layout.addItem( + QtWidgets.QSpacerItem( + 2, 2, QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + ) + simulation_runs_list_layout.addWidget(simulation_runs_list_scrollarea) + simulation_runs_list_layout.addItem( + QtWidgets.QSpacerItem( + 2, 2, QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + ) + main_layout.addLayout(simulation_runs_list_layout) + + simulation_progress_controls_layout = QtWidgets.QVBoxLayout() + simulation_success_progress_layout = QtWidgets.QHBoxLayout() # self.simulation_run_total_runtime_timer = QtWidgets.QTimer(self) # self.simulation_run_total_runtime_timer.timeout.connect(self.) # self.simulation_run_total_runtime_info_label = QtWidgets.QLabel(TOTAL_RUNTIME_TEXT_FORMAT.format(total_runtime_in_seconds=0)) @@ -53,23 +91,24 @@ def __init__(self, simulation_run_model: QtSimulationRunModel, parent: QtWidgets # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format self.simulation_run_progress_bar.setFormat("Executing simulation run %v of %m") self.simulation_run_progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.simulation_run_progress_text = QtWidgets.QLabel("") + self.simulation_run_progress_lbl = QtWidgets.QLabel("") + self.simulation_run_err_lbl = QtWidgets.QLabel("") + self.simulation_run_err_lbl.setStyleSheet("QLabel { color : red; }") # simulation_progress_layout.addWidget(self.simulation_run_total_runtime_info_label) - simulation_progress_layout.addWidget(self.simulation_run_progress_bar) - simulation_progress_layout.addWidget(self.simulation_run_progress_text) - simulation_progress_layout.addStretch() + simulation_success_progress_layout.addWidget(self.simulation_run_progress_bar) + simulation_success_progress_layout.addWidget(self.simulation_run_progress_lbl) + simulation_progress_controls_layout.addLayout(simulation_success_progress_layout) + simulation_progress_controls_layout.addWidget(self.simulation_run_err_lbl) - main_layout = QtWidgets.QVBoxLayout() - main_layout.addStretch() - main_layout.addLayout(simulation_progress_layout) + # simulation_progress_layout.addStretch() + main_layout.addLayout(simulation_progress_controls_layout) # TODO: One could also offer a close button in the dialog (that warns the user when closing the dialog during a simulation run execution)? self.dialog_button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.rejected.connect(self.request_worker_cancellation) + self.dialog_button_box.rejected.connect(self._request_worker_cancellation) main_layout.addWidget(self.dialog_button_box) - self.setLayout(main_layout) def start_simulations( self, @@ -77,10 +116,11 @@ def start_simulations( expected_total_num_simulation_runs: int, stop_at_first_output_state_mismatch: bool, ) -> None: - self.expected_total_num_simulation_runs = expected_total_num_simulation_runs self.num_completed_simulation_runs = 0 + self.expected_total_num_simulation_runs = expected_total_num_simulation_runs + self.simulation_run_progress_lbl.setText("") + self.simulation_run_progress_lbl.setText("") - self.simulation_run_progress_text.setText("") self.simulation_run_progress_bar.setMinimum(0) self.simulation_run_progress_bar.setMaximum(expected_total_num_simulation_runs - 1) self.simulation_run_progress_bar.setValue(0) @@ -100,56 +140,65 @@ def start_simulations( # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) self.worker.allSimulationsDone.connect( - self.handle_all_simulation_runs_done, QtCore.Qt.ConnectionType.QueuedConnection + self._handle_all_simulation_runs_done, QtCore.Qt.ConnectionType.QueuedConnection ) self.worker.simulationRunCompleted.connect( - self.handle_simulation_run_done, QtCore.Qt.ConnectionType.QueuedConnection + self._handle_simulation_run_done, QtCore.Qt.ConnectionType.QueuedConnection ) self.worker.simulationRunMismatchBetweenOutputStates.connect( - self.handle_simulation_runs_stopped_after_first_failure, QtCore.Qt.ConnectionType.QueuedConnection + self._handle_simulation_runs_stopped_after_first_failure, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker.errDuringSimulationRun.connect( + self._handle_simulation_runs_stopped_due_to_err, QtCore.Qt.ConnectionType.QueuedConnection ) self.worker_thread.finished.connect(self.worker_thread.deleteLater) - self.worker_thread.finished.connect(self.reset_workers) + self.worker_thread.finished.connect(self._reset_workers) self.worker.moveToThread(self.worker_thread) self.worker_thread.start() - self.enqueue_next_simulation_run(0) - self.change_dialog_cancellation_button_enable_state(True) + self._enqueue_next_simulation_run(0) + self._change_dialog_cancellation_button_enable_state(True) # TODO: Mark remaining member functions as private via underscore prefix? - def handle_all_simulation_runs_done(self) -> None: + # TODO: Not all simulation runs are executed? (2 out of 10) but no error is printed to the console or shown in the GUI. + def _handle_all_simulation_runs_done(self) -> None: # self.simulation_run_total_runtime_timer.stop() if self.worker_thread is not None: self.worker_thread.quit() self.worker_thread.wait() - self.change_dialog_cancellation_button_enable_state(False) + self._change_dialog_cancellation_button_enable_state(False) self.simulation_run_progress_bar.setVisible(False) if self.num_completed_simulation_runs == self.expected_total_num_simulation_runs: - self.simulation_run_progress_text.setText( + self.simulation_run_progress_lbl.setText( f"Finished all {self.expected_total_num_simulation_runs} simulation runs!" ) else: - self.simulation_run_progress_text.setText( + self.simulation_run_progress_lbl.setText( f"Finished {self.num_completed_simulation_runs} out of all {self.expected_total_num_simulation_runs} simulation runs!" ) - def handle_simulation_runs_stopped_after_first_failure( - self, - _: SimulationRunResult, + def _handle_simulation_runs_stopped_due_to_err(self, simulation_run_num_that_failed: int, err: Exception) -> None: + self.simulation_run_err_lbl.setText( + f"Unexpected {err=}, {type(err)=} during execution of simulation run {simulation_run_num_that_failed}" + ) + self._request_worker_cancellation() + + def _handle_simulation_runs_stopped_after_first_failure( + self, simulation_run_causing_err: ToBeExecutedSimulationRun ) -> None: - self.request_worker_cancellation() - # self.simulation_run_total_runtime_timer.stop() + self._update_progress_controls(simulation_run_causing_err.simulation_run_number) + self._request_worker_cancellation() - def request_worker_cancellation(self) -> None: + def _request_worker_cancellation(self) -> None: if self.worker is not None: self.worker.request_cancellation() - self.change_dialog_cancellation_button_enable_state(False) + self._change_dialog_cancellation_button_enable_state(False) self.simulation_run_progress_bar.setVisible(False) - def change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: + def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( QtWidgets.QDialogButtonBox.StandardButton.Cancel ) @@ -160,25 +209,38 @@ def change_dialog_cancellation_button_enable_state(self, should_button_be_enable dialog_cancel_button.setEnabled(should_button_be_enabled) def closeEvent(self, _): # noqa: N802 - self.request_worker_cancellation() + self._request_worker_cancellation() # if self.worker_thread is not None: # self.worker_thread.quit() # self.worker_thread.wait() - def handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: - self.update_progress_controls(simulation_run_result.simulation_run_number) - self.enqueue_next_simulation_run(simulation_run_result.simulation_run_number + 1) + def _handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: + self._update_progress_controls(simulation_run_result.simulation_run_number) + try: + self.simulation_runs_model.update_model_using_simulation_run_result( + self.simulation_runs_model.index(simulation_run_result.simulation_run_number), + simulation_run_result.actual_output_state, + simulation_run_result.do_expected_and_actual_outputs_match, + simulation_run_result.execution_runtime_in_ms, + ) + except ValueError as err: + self.simulation_run_err_lbl.setText( + f"Unexpected {err=}, {type(err)=} during update of simulation run model after successful execution of simulation run {simulation_run_result.simulation_run_number}" + ) + self._request_worker_cancellation() + else: + self._enqueue_next_simulation_run(simulation_run_result.simulation_run_number + 1) - def enqueue_next_simulation_run(self, simulation_run_number: int) -> None: - next_simulation_run: SimulationRunModel | None = self.simulation_run_model.get_simulation_run_model( + def _enqueue_next_simulation_run(self, simulation_run_number: int) -> None: + next_simulation_run: SimulationRunModel | None = self.simulation_runs_model.get_simulation_run_model( simulation_run_number ) if self.worker is None: return if next_simulation_run is None: - self.request_worker_cancellation() + self._request_worker_cancellation() else: self.worker.queue_new_simulation_run( ToBeExecutedSimulationRun( @@ -186,11 +248,11 @@ def enqueue_next_simulation_run(self, simulation_run_number: int) -> None: ) ) - def reset_workers(self) -> None: + def _reset_workers(self) -> None: self.worker_thread = None self.worker = None - def update_progress_controls(self, completed_simulation_run: int) -> None: - self.simulation_run_progress_text.setText(f"Completed simulation run {completed_simulation_run}") + def _update_progress_controls(self, completed_simulation_run: int) -> None: + self.simulation_run_progress_lbl.setText(f"Completed simulation run {completed_simulation_run}") self.simulation_run_progress_bar.setValue(self.num_completed_simulation_runs) self.num_completed_simulation_runs += 1 diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 18c68c33..9bd7cbbd 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -35,6 +35,7 @@ LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE: Final[int] = LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE + 1 LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: Final[int] = LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE + 1 ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE: Final[int] = LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE + 1 +LARGEST_SIM_RUN_NUMBER_QT_ROLE: Final[int] = ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE + 1 @dataclass(frozen=True) @@ -112,6 +113,26 @@ def update_expected_output_state_qubit_value(self, qubit: int, new_qubit_value: self.expected_output_state, qubit, new_qubit_value ) + @staticmethod + def do_output_states_match( + expected_output_state: syrec.n_bit_values_container | None, actual_output_state: syrec.n_bit_values_container + ) -> bool | None: + if expected_output_state is None: + return None + + if expected_output_state.size() != actual_output_state.size(): + msg = f"Expected output state to have {expected_output_state.size()} qubits but actual output state contained {actual_output_state.size()} qubits!" + raise ValueError(msg) + + do_expected_and_actual_input_states_match = True + for qubit in range(actual_output_state.size()): + do_expected_and_actual_input_states_match &= actual_output_state.test(qubit) == expected_output_state.test( + qubit + ) + if not do_expected_and_actual_input_states_match: + break + return do_expected_and_actual_input_states_match + @staticmethod def _update_n_bit_values_container_qubit_value( n_bit_values_container: syrec.n_bit_values_container, qubit: int, new_qubit_value: bool @@ -132,7 +153,7 @@ def __init__( self.n_data_qubits: int = annotatable_quantum_computation.num_data_qubits self.simulation_run_models: list[SimulationRunModel] = [] self.quantum_register_layouts: list[QuantumRegisterLayout] = ( - QtSimulationRunModel.__record_quantum_register_layouts(annotatable_quantum_computation) + QtSimulationRunModel._record_quantum_register_layouts(annotatable_quantum_computation) ) self.longest_quantum_register_name: str = "" self.largest_quantum_register_size: int = 0 @@ -157,7 +178,7 @@ def _does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) - return qubit_label.startswith("__q") @staticmethod - def __record_quantum_register_layouts( + def _record_quantum_register_layouts( annotatable_quantum_computation: syrec.annotatable_quantum_computation, ) -> list[QuantumRegisterLayout]: quantum_register_layouts: list[QuantumRegisterLayout] = [] @@ -197,6 +218,9 @@ def data(self, index: QtCore.QModelIndex, role: int) -> object: if role == ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE: return self.annotatable_quantum_computation + if role == LARGEST_SIM_RUN_NUMBER_QT_ROLE: + return self.rowCount(QtCore.QModelIndex()) + return None def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: @@ -250,48 +274,49 @@ def add_all_possible_simulation_run_models(self) -> bool: self.endInsertRows() return True - # TODO: Check that no duplicate input or expected output_state is added - # TODO: Add custom error messages if validation fails - def update_simulation_run_model( - self, index: QtCore.QModelIndex, updated_simulation_run_model: SimulationRunModel + def update_edited_simulation_run_model( + self, index: QtCore.QModelIndex, updated_simulation_run_data: SimulationRunModel ) -> None: if not self.is_model_index_valid(index): msg = "Invalid model index!" raise ValueError(msg) - to_be_updated_simulation_run_model: SimulationRunModel = self.simulation_run_models[index.row()] - if updated_simulation_run_model.input_state.size() != to_be_updated_simulation_run_model.input_state.size(): - msg = "Input state sizes did not match" - raise ValueError(msg) + # TODO: Further validation - if ( - updated_simulation_run_model.expected_output_state is not None - and updated_simulation_run_model.actual_output_state is not None - and updated_simulation_run_model.expected_output_state.size() - != updated_simulation_run_model.actual_output_state.size() - ): - msg = "Actual and expected output state sizes did not match in updated model" - raise ValueError(msg) + self.simulation_run_models[index.row()] = updated_simulation_run_data + self.dataChanged.emit(index, index) - if ( - updated_simulation_run_model.expected_output_state is not None - and to_be_updated_simulation_run_model.expected_output_state is not None - and updated_simulation_run_model.expected_output_state.size() - != to_be_updated_simulation_run_model.expected_output_state.size() - ): - msg = "Expected output state sizes did not match between currently stored model and updated model" + # TODO: Check that no duplicate input or expected output_state is added + # TODO: Add custom error messages if validation fails + def update_model_using_simulation_run_result( + self, + index: QtCore.QModelIndex, + actual_output_state: syrec.n_bit_values_container, + do_expected_and_actual_outputs_match: bool | None, + execution_runtime_in_ms: float, + ) -> None: + if not self.is_model_index_valid(index): + msg = "Invalid model index!" raise ValueError(msg) + to_be_updated_simulation_run_model: SimulationRunModel = self.simulation_run_models[index.row()] + # TODO: Should we validate that the current expected output state is equal to the input state + # if updated_simulation_run_model.expected_output_state is not None and updated_simulation_run_model.expected_output_state.size() != to_be_updated_simulation_run_model.input_state.size(): + # msg = "Input state sizes did not match" + # raise ValueError(msg) + if ( - updated_simulation_run_model.actual_output_state is not None - and to_be_updated_simulation_run_model.actual_output_state is not None - and updated_simulation_run_model.actual_output_state.size() - != to_be_updated_simulation_run_model.actual_output_state.size() + actual_output_state is not None + and actual_output_state.size() != to_be_updated_simulation_run_model.input_state.size() ): - msg = "Actual output state sizes did not match between currently stored model and updated model" + msg = "Input state sizes did not match" raise ValueError(msg) - self.simulation_run_models[index.row()] = updated_simulation_run_model + self.simulation_run_models[index.row()].actual_output_state = actual_output_state + self.simulation_run_models[ + index.row() + ].do_expected_and_actual_outputs_match = do_expected_and_actual_outputs_match + self.simulation_run_models[index.row()].execution_runtime_in_ms = execution_runtime_in_ms self.dataChanged.emit(index, index) def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: diff --git a/python/mqt/syrec/simulation_view/qt_simulation_worker.py b/python/mqt/syrec/simulation_view/qt_simulation_worker.py index 305de03b..685eb91a 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_worker.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_worker.py @@ -16,6 +16,8 @@ from mqt import syrec +from .qt_simulation_run_model import SimulationRunModel + # One could simplify the signal and slot declarations by defining the import: from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot @@ -41,7 +43,7 @@ class SimulationWorker(QtCore.QObject): # type: ignore[misc] SimulationRunResult, name="simulationRunMismatchBetweenOutputStates" ) all_simulations_done = QtCore.pyqtSignal(name="allSimulationsDone") - simulation_run_failed = QtCore.pyqtSignal(ToBeExecutedSimulationRun, name="simulationRunFailed") + err_during_simulation_run = QtCore.pyqtSignal(int, Exception, name="errDuringSimulationRun") def __init__( self, @@ -64,7 +66,6 @@ def start_simulations(self) -> None: try: simulation_execution_start_time_in_seconds: float = time.time() - actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) # simulation_run_execution_statistics = syrec.statistics() actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) @@ -72,16 +73,9 @@ def start_simulations(self) -> None: syrec.simple_simulation( actual_output_state, self.annotatable_quantum_computation, dequeued_element.input_state ) - - do_expected_and_actual_input_states_match: bool | None = None - if dequeued_element.expected_output_state is not None: - for qubit in range(actual_output_state.size()): - do_expected_and_actual_input_states_match |= actual_output_state.test( - qubit - ) == dequeued_element.expected_output_state.test(qubit) - if not do_expected_and_actual_input_states_match: - break - + do_expected_and_actual_input_states_match: bool | None = SimulationRunModel.do_output_states_match( + dequeued_element.expected_output_state, actual_output_state + ) simulation_execution_end_time_in_seconds: float = time.time() simulation_execution_runtime_in_ms: float = ( simulation_execution_end_time_in_seconds - simulation_execution_start_time_in_seconds @@ -94,6 +88,7 @@ def start_simulations(self) -> None: do_expected_and_actual_input_states_match, simulation_execution_runtime_in_ms, ) + if ( self.should_stop_at_first_output_state_mismatch and do_expected_and_actual_input_states_match is not None @@ -103,10 +98,9 @@ def start_simulations(self) -> None: else: self.simulation_run_completed.emit(simulation_run_result) # time.sleep(1) - except Exception: - self.simulation_run_failed.emit(dequeued_element) + except Exception as err: + self.err_during_simulation_run.emit(dequeued_element.simulation_run_number, err) break - self.all_simulations_done.emit() @QtCore.pyqtSlot() # type: ignore[untyped-decorator] diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py index f1a69d66..b5362e73 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -21,11 +21,31 @@ DEFAULT_QREG_LAYOUT_TEXT_FORMAT: Final[str] = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT: Final[str] = "Simulation run #{simulation_run_number:d}" DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "INPUT" -DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "OUTPUT" +DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "EXPECTED OUTPUT" DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT: Final[str] = "" +CARD_TITLE_FONT_SIZE: Final[int] = 14 +CARD_CONTENT_FONT_SIZE: Final[int] = 10 +QREG_LAYOUT_INFO_FONT_SIZE: Final[int] = 8 +CARD_TITLE_BOTTOM_Y_MARGIN: Final[int] = 8 +QREG_CONTENT_Y_SPACING: Final[int] = 4 +QREG_CONTENT_X_SPACING: Final[int] = 6 +CARD_CONTENT_PADDING: Final[int] = 20 + class BaseSimulationRunStyledItemDelegate: + @staticmethod + def _get_pixel_width_for_longest_sim_run_header( + largest_sim_run_number: int, font_used_to_draw_text: QtGui.QFont, expected_font_size: int + ) -> int: + return int( + QtGui.QFontMetrics( + QtGui.QFont(font_used_to_draw_text.family(), expected_font_size, font_used_to_draw_text.weight()) + ).horizontalAdvance( + DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT.format(simulation_run_number=largest_sim_run_number) + ) + ) + @staticmethod def _get_pixel_width_of_text(text: str, font_used_to_draw_text: QtGui.QFont, expected_font_size: int) -> int: return int( diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py new file mode 100644 index 00000000..396cfd77 --- /dev/null +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py @@ -0,0 +1,549 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore, QtGui, QtWidgets + +from ..qt_simulation_run_model import ( + LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, + LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, + LARGEST_SIM_RUN_NUMBER_QT_ROLE, + LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE, + QUANTUM_REGISTER_LAYOUT_QT_ROLE, + SIMULATION_RUN_IO_STATE_QT_ROLE, +) +from .base_simulation_run_styled_item_delegate import ( + CARD_CONTENT_FONT_SIZE, + CARD_CONTENT_PADDING, + CARD_TITLE_BOTTOM_Y_MARGIN, + CARD_TITLE_FONT_SIZE, + DEFAULT_QREG_LAYOUT_TEXT_FORMAT, + DEFAULT_QREG_NAME_COLUMN_HEADER, + DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT, + DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, + QREG_CONTENT_X_SPACING, + QREG_CONTENT_Y_SPACING, + BaseSimulationRunStyledItemDelegate, +) + +if TYPE_CHECKING: + from ..qt_simulation_run_model import ( + SimulationRunModel, + ) + +AGGREGATE_RESULT_TOP_Y_MARGIN: Final[int] = 15 + +INPUT_STATE_QREG_LABEL_TEXT: Final[str] = "Input:" +EXPECTED_OUTPUT_QREG_LABEL_TEXT: Final[str] = "Expected output:" +ACTUAL_OUTPUT_QREG_LABEL_TEXT: Final[str] = "Actual output:" +QREG_OUTPUTS_MATCH_LABEL_TEXT: Final[str] = "Result:" +AGGREGATE_QREG_OUTPUTS_MATCH_LABEL_TEXT: Final[str] = "Aggregate result:" +RUNTIME_LABEL_TEXT: Final[str] = "Runtime [in ms]:" + +OUTPUTS_MATCH_TEXT: Final[str] = "OUTPUTS MATCH" +OUTPUTS_MISMATCH_TEXT: Final[str] = "OUTPUTS MISMATCH" +OUTPUTS_MATCH_UNKNOWN_TEXT: Final[str] = "UNKNOWN" + + +# Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html +class SimulationRunExecutionStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] + def __init__(self, parent=None): + super().__init__(parent) + + @staticmethod + def _get_required_width_for_labels_column(option: QtWidgets.QStyleItemOptionViewItem, font_size: int) -> int: + return ( + QREG_CONTENT_X_SPACING + + max( + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_QREG_NAME_COLUMN_HEADER, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + INPUT_STATE_QREG_LABEL_TEXT, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + EXPECTED_OUTPUT_QREG_LABEL_TEXT, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + ACTUAL_OUTPUT_QREG_LABEL_TEXT, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + QREG_OUTPUTS_MATCH_LABEL_TEXT, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + AGGREGATE_QREG_OUTPUTS_MATCH_LABEL_TEXT, option.font, font_size + ), + ) + + QREG_CONTENT_X_SPACING + ) + + @staticmethod + def _get_required_width_for_qreg_contents_and_outputs_match_result( + option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int + ) -> int: + + largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) + largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) + + required_width_for_qreg_name_and_layout_info: int = ( + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE), option.font, font_size + ) + + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_QREG_LAYOUT_TEXT_FORMAT.format( + first_qubit=largest_first_qubit_of_quantum_registers, n_qubits=largest_quantum_register_size + ), + option.font, + font_size, + ) + ) + required_width_for_largest_qreg_contents: int = ( + SimulationRunExecutionStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, largest_quantum_register_size, font_size + ) + ) + required_width_for_outputs_match_result: int = max( + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + OUTPUTS_MATCH_TEXT, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + OUTPUTS_MISMATCH_TEXT, option.font, font_size + ), + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + OUTPUTS_MATCH_UNKNOWN_TEXT, option.font, font_size + ), + ) + return ( + QREG_CONTENT_X_SPACING + + max( + required_width_for_qreg_name_and_layout_info, + required_width_for_largest_qreg_contents, + required_width_for_outputs_match_result, + ) + + QREG_CONTENT_X_SPACING + ) + + @staticmethod + def _get_required_size_for_content( + option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex + ) -> QtCore.QSize: + if not index.isValid(): + return QtCore.QSize(0, 0) + + card_title_height: int = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + option.font, CARD_TITLE_FONT_SIZE + ) + + card_title_width: int = SimulationRunExecutionStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( + index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE + ) + + n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + # Quantum register contents are displayed in the following format for every quantum register: + # R0: : + # R1: : + # R2: : + # R3: : + # + # Additionally, below the content of all quantum registers the aggregate result of the simulation run is displayed as: + # R5: : + # R6: : + required_text_line_height: int = ( + QREG_CONTENT_Y_SPACING + + SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + ) + required_qreg_contents_height: int = 4 * required_text_line_height + required_total_qreg_contents_height: int = (n_qregs * required_qreg_contents_height) + ( + (n_qregs - 1) * QREG_CONTENT_Y_SPACING if n_qregs > 1 else 0 + ) + required_aggregate_result_text_height: int = 2 * required_text_line_height + required_total_card_height: int = ( + CARD_CONTENT_PADDING + + card_title_height + + CARD_TITLE_BOTTOM_Y_MARGIN + + required_total_qreg_contents_height + + AGGREGATE_RESULT_TOP_Y_MARGIN + + required_aggregate_result_text_height + + CARD_CONTENT_PADDING + ) + required_total_card_width: int = max( + card_title_width, + SimulationRunExecutionStyledItemDelegate._get_required_width_for_qreg_contents_and_outputs_match_result( + option, index, CARD_CONTENT_FONT_SIZE + ), + ) + return QtCore.QSize(required_total_card_width, required_total_card_height) + + @staticmethod + def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 + required_content_size: QtCore.QSize = SimulationRunExecutionStyledItemDelegate._get_required_size_for_content( + option, index + ) + return QtCore.QSize( + min(option.rect.bottomRight().x(), required_content_size.width()), + min(option.rect.bottomRight().y(), required_content_size.height()), + ) + + def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: + if not index.isValid() or option.rect.width() == 0: + return + + n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + if n_qregs == 0: + return + + associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + SimulationRunExecutionStyledItemDelegate._get_required_size_for_content(option, index) + available_rect_for_content: QtCore.QRect = option.rect.adjusted( + CARD_CONTENT_PADDING, + CARD_CONTENT_PADDING, + -CARD_CONTENT_PADDING, + -CARD_CONTENT_PADDING, + ) + # SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( + # painter, available_rect_for_content, 5, QtCore.Qt.GlobalColor.green, 0 + # ) + + painter.save() + required_text_width_for_header_for_largest_sim_run_number: Final[int] = min( + QREG_CONTENT_X_SPACING + + SimulationRunExecutionStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( + index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE + ) + + QREG_CONTENT_X_SPACING, + available_rect_for_content.width(), + ) + header_text_bottom_left_point: QtCore.QPoint = self._draw_card_border_and_header( + painter, + option, + simulation_run_number=index.row(), + card_content_rect=available_rect_for_content, + available_header_width=required_text_width_for_header_for_largest_sim_run_number, + ) + + group_box_content_font_size: int = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + option.font, CARD_CONTENT_FONT_SIZE + ) + group_box_content_line_height_with_spacing: Final[int] = group_box_content_font_size + QREG_CONTENT_Y_SPACING + + available_content_width: Final[int] = available_rect_for_content.width() + required_label_column_width: Final[int] = ( + SimulationRunExecutionStyledItemDelegate._get_required_width_for_labels_column( + option, group_box_content_font_size + ) + ) + required_values_column_width: Final[int] = ( + SimulationRunExecutionStyledItemDelegate._get_required_width_for_qreg_contents_and_outputs_match_result( + option, index, group_box_content_font_size + ) + ) + + scaled_column_widths: list[int] = ( + SimulationRunExecutionStyledItemDelegate._scale_column_widths_based_on_ratio_to_total_available_width( + [required_label_column_width, required_values_column_width], + available_content_width, + ) + ) + scaled_label_column_width: Final[int] = scaled_column_widths[0] + scaled_values_column_width: Final[int] = scaled_column_widths[1] + + base_label_column_rect: QtCore.QRect = QtCore.QRect( + header_text_bottom_left_point.x(), + header_text_bottom_left_point.y() + CARD_TITLE_BOTTOM_Y_MARGIN, + scaled_label_column_width, + group_box_content_line_height_with_spacing, + ) + base_value_column_rect: QtCore.QRect = QtCore.QRect( + QREG_CONTENT_X_SPACING + base_label_column_rect.topRight().x(), + base_label_column_rect.topRight().y(), + scaled_values_column_width, + group_box_content_line_height_with_spacing, + ) + + longest_qreg_name: str = index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) + largest_qreg_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) + largest_first_qubit_of_qreg: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) + + required_width_for_longest_qreg_name: Final[int] = ( + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + longest_qreg_name, option.font, CARD_CONTENT_FONT_SIZE + ) + + QREG_CONTENT_X_SPACING + ) + required_width_for_largest_qreg_layout_info_text: Final[int] = ( + SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_QREG_LAYOUT_TEXT_FORMAT.format( + first_qubit=largest_first_qubit_of_qreg, n_qubits=largest_qreg_size + ), + option.font, + CARD_CONTENT_FONT_SIZE, + ) + ) + + scaled_qreg_name_and_layout_column_widths: list[int] = ( + SimulationRunExecutionStyledItemDelegate._scale_column_widths_based_on_ratio_to_total_available_width( + [required_width_for_longest_qreg_name, required_width_for_largest_qreg_layout_info_text], + scaled_values_column_width, + ) + ) + scaled_width_for_longest_qreg_name: Final[int] = scaled_qreg_name_and_layout_column_widths[0] + scaled_width_for_largest_qreg_layout_info_text: Final[int] = scaled_qreg_name_and_layout_column_widths[1] + + row_idx: int = 0 + qreg_contents_height_without_spacing: Final[int] = 4 * group_box_content_line_height_with_spacing + qreg_contents_height: Final[int] = qreg_contents_height_without_spacing + QREG_CONTENT_Y_SPACING + for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): + curr_row_y_offset: int = row_idx * qreg_contents_height + base_row_i_label_col_rect: QtCore.QRect = base_label_column_rect.adjusted( + 0, curr_row_y_offset, 0, curr_row_y_offset + ) + base_row_i_value_col_rect: QtCore.QRect = base_value_column_rect.adjusted( + 0, curr_row_y_offset, 0, curr_row_y_offset + ) + + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_QREG_NAME_COLUMN_HEADER, + base_row_i_label_col_rect, + CARD_CONTENT_FONT_SIZE, + draw_bold_text=True, + text_alignment=QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignRight, + ) + + qreg_name_rect: QtCore.QRect = QtCore.QRect( + base_row_i_value_col_rect.topLeft().x(), + base_row_i_value_col_rect.topLeft().y(), + scaled_width_for_longest_qreg_name, + group_box_content_font_size, + ) + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, + qreg_layout.qreg_name, + qreg_name_rect, + CARD_CONTENT_FONT_SIZE, + draw_bold_text=False, + text_alignment=QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft, + ) + + qreg_layout_info_rect: QtCore.QRect = QtCore.QRect( + qreg_name_rect.topRight().x(), + qreg_name_rect.topRight().y(), + scaled_width_for_largest_qreg_layout_info_text, + group_box_content_font_size, + ) + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_QREG_LAYOUT_TEXT_FORMAT.format( + first_qubit=qreg_layout.first_qubit_of_qreg, n_qubits=qreg_layout.qreg_size + ), + qreg_layout_info_rect, + CARD_CONTENT_FONT_SIZE, + draw_bold_text=False, + text_alignment=QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft, + text_color=QtCore.Qt.GlobalColor.gray, + ) + + base_row_i_label_col_rect.adjust( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + base_row_i_value_col_rect.adjust( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + SimulationRunExecutionStyledItemDelegate._draw_label_and_value( + painter, + base_row_i_label_col_rect, + INPUT_STATE_QREG_LABEL_TEXT, + base_row_i_value_col_rect, + SimulationRunExecutionStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.input_state, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, + ), + ) + + base_row_i_label_col_rect.adjust( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + base_row_i_value_col_rect.adjust( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + SimulationRunExecutionStyledItemDelegate._draw_label_and_value( + painter, + base_row_i_label_col_rect, + EXPECTED_OUTPUT_QREG_LABEL_TEXT, + base_row_i_value_col_rect, + SimulationRunExecutionStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.expected_output_state, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, + ) + if associated_input_output_mapping.expected_output_state is not None + else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, + value_col_text_color=QtCore.Qt.GlobalColor.gray + if associated_input_output_mapping.expected_output_state is None + else QtCore.Qt.GlobalColor.black, + ) + + base_row_i_label_col_rect.adjust( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + base_row_i_value_col_rect.adjust( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + SimulationRunExecutionStyledItemDelegate._draw_label_and_value( + painter, + base_row_i_label_col_rect, + ACTUAL_OUTPUT_QREG_LABEL_TEXT, + base_row_i_value_col_rect, + SimulationRunExecutionStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_input_output_mapping.actual_output_state, + qreg_layout.first_qubit_of_qreg, + qreg_layout.qreg_size, + ) + if associated_input_output_mapping.actual_output_state is not None + else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, + value_col_text_color=QtCore.Qt.GlobalColor.gray + if associated_input_output_mapping.actual_output_state is None + else QtCore.Qt.GlobalColor.black, + ) + row_idx += 1 + + painter.drawLine(base_row_i_label_col_rect.bottomLeft(), base_row_i_value_col_rect.bottomRight()) + + y_offset_from_card_header_to_aggregate_result_row: int = ( + qreg_contents_height_without_spacing + AGGREGATE_RESULT_TOP_Y_MARGIN + ) + if row_idx > 1: + y_offset_from_card_header_to_aggregate_result_row += (row_idx - 1) * qreg_contents_height + + aggregate_result_row_outputs_match_label_col_rect: QtCore.QRect = base_label_column_rect.adjusted( + 0, y_offset_from_card_header_to_aggregate_result_row, 0, y_offset_from_card_header_to_aggregate_result_row + ) + aggregate_result_row_outputs_match_value_col_rect: QtCore.QRect = base_value_column_rect.adjusted( + 0, y_offset_from_card_header_to_aggregate_result_row, 0, y_offset_from_card_header_to_aggregate_result_row + ) + SimulationRunExecutionStyledItemDelegate._draw_label_and_value( + painter, + aggregate_result_row_outputs_match_label_col_rect, + AGGREGATE_QREG_OUTPUTS_MATCH_LABEL_TEXT, + aggregate_result_row_outputs_match_value_col_rect, + SimulationRunExecutionStyledItemDelegate._stringify_outputs_match_result( + associated_input_output_mapping.do_expected_and_actual_outputs_match + ), + value_col_text_color=SimulationRunExecutionStyledItemDelegate._determine_color_for_outputs_match_result_text( + associated_input_output_mapping.do_expected_and_actual_outputs_match + ), + ) + + aggregate_result_row_runtime_label_col_rect: QtCore.QRect = ( + aggregate_result_row_outputs_match_label_col_rect.adjusted( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + ) + aggregate_result_row_runtime_value_col_rect: QtCore.QRect = ( + aggregate_result_row_outputs_match_value_col_rect.adjusted( + 0, group_box_content_line_height_with_spacing, 0, group_box_content_line_height_with_spacing + ) + ) + SimulationRunExecutionStyledItemDelegate._draw_label_and_value( + painter, + aggregate_result_row_runtime_label_col_rect, + RUNTIME_LABEL_TEXT, + aggregate_result_row_runtime_value_col_rect, + str(associated_input_output_mapping.execution_runtime_in_ms), + value_col_text_color=QtCore.Qt.GlobalColor.gray + if associated_input_output_mapping.execution_runtime_in_ms is None + else QtCore.Qt.GlobalColor.black, + ) + painter.restore() + + @staticmethod + def _draw_card_border_and_header( + painter: QtGui.QPainter, + option: QtWidgets.QStyleOptionViewItem, + simulation_run_number: int, + card_content_rect: QtCore.QRect, + available_header_width: int, + draw_rect_corners: bool = False, + ) -> QtCore.QPoint: + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + if draw_rect_corners: + SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( + painter, option.rect, 5, QtCore.Qt.GlobalColor.cyan, simulation_run_number + ) + SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( + painter, card_content_rect, 5, QtCore.Qt.GlobalColor.red, simulation_run_number + ) + painter.drawRoundedRect(option.rect, 3, 3) + + if QtWidgets.QStyle.StateFlag.State_Selected in option.state: + painter.fillRect(option.rect, option.palette.highlight()) + painter.setBrush(option.palette.highlightedText()) + + header_text: str = DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT.format(simulation_run_number=simulation_run_number) + header_text_height: int = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + option.font, CARD_TITLE_FONT_SIZE + ) + header_text_rect = QtCore.QRect( + card_content_rect.x(), card_content_rect.y(), available_header_width + 10, header_text_height + ) + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, header_text, header_text_rect, CARD_TITLE_FONT_SIZE, draw_bold_text=True + ) + return header_text_rect.bottomLeft() + + @staticmethod + def _draw_label_and_value( + painter: QtGui.QPainter, + label_col_rect: QtCore.QRect, + label_text: str, + value_col_rect: QtCore.QRect, + value_text: str, + value_col_text_color: QtCore.Qt.GlobalColor = QtCore.Qt.GlobalColor.black, + ) -> None: + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, + label_text, + label_col_rect, + CARD_CONTENT_FONT_SIZE, + draw_bold_text=True, + text_alignment=QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignRight, + ) + + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, + value_text, + value_col_rect, + CARD_CONTENT_FONT_SIZE, + text_alignment=QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignLeft, + text_color=value_col_text_color, + ) + + # SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( + # painter, label_col_rect, 5, QtCore.Qt.GlobalColor.red, 0 + # ) + # SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( + # painter, value_col_rect, 5, QtCore.Qt.GlobalColor.blue, 0 + # ) + + @staticmethod + def _determine_color_for_outputs_match_result_text(do_outputs_match: bool | None) -> QtCore.Qt.GlobalColor: + if do_outputs_match is None: + return QtCore.Qt.GlobalColor.gray + + return QtCore.Qt.GlobalColor.green if do_outputs_match else QtCore.Qt.GlobalColor.red + + @staticmethod + def _stringify_outputs_match_result(do_outputs_match: bool | None) -> str: + if do_outputs_match is None: + return OUTPUTS_MATCH_UNKNOWN_TEXT + + return OUTPUTS_MATCH_TEXT if do_outputs_match else OUTPUTS_MISMATCH_TEXT diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py index d16c63cf..1c84b364 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py @@ -15,17 +15,25 @@ from ..qt_simulation_run_model import ( LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, + LARGEST_SIM_RUN_NUMBER_QT_ROLE, LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE, QUANTUM_REGISTER_LAYOUT_QT_ROLE, SIMULATION_RUN_IO_STATE_QT_ROLE, ) from .base_simulation_run_styled_item_delegate import ( + CARD_CONTENT_FONT_SIZE, + CARD_CONTENT_PADDING, + CARD_TITLE_BOTTOM_Y_MARGIN, + CARD_TITLE_FONT_SIZE, DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER, DEFAULT_QREG_LAYOUT_TEXT_FORMAT, DEFAULT_QREG_NAME_COLUMN_HEADER, DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT, DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, + QREG_CONTENT_X_SPACING, + QREG_CONTENT_Y_SPACING, + QREG_LAYOUT_INFO_FONT_SIZE, BaseSimulationRunStyledItemDelegate, ) @@ -34,14 +42,6 @@ SimulationRunModel, ) -GROUP_BOX_TITLE_FONT_SIZE: Final[int] = 14 -GROUP_BOX_CONTENT_FONT_SIZE: Final[int] = 10 -QREG_LAYOUT_INFO_FONT_SIZE: Final[int] = 8 -GROUP_BOX_TITLE_BOTTOM_Y_MARGIN: Final[int] = 8 -QREG_CONTENT_Y_SPACING: Final[int] = 4 -QREG_CONTENT_X_SPACING: Final[int] = 6 -GROUP_BOX_CONTENT_PADDING: Final[int] = 20 - # Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html class SimulationRunOverviewStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] @@ -89,14 +89,17 @@ def _get_required_size_for_content( # R0: # R1: group_box_title_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( - option.font, GROUP_BOX_TITLE_FONT_SIZE + option.font, CARD_TITLE_FONT_SIZE + ) + group_box_title_width: int = ( + SimulationRunOverviewStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( + index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE + ) ) qreg_contents_text_height: int = ( QREG_CONTENT_Y_SPACING - + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( - option.font, GROUP_BOX_CONTENT_FONT_SIZE - ) + + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + QREG_CONTENT_Y_SPACING + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, QREG_LAYOUT_INFO_FONT_SIZE) ) @@ -105,43 +108,46 @@ def _get_required_size_for_content( ) total_qreg_contents_text_height: int = n_qregs * qreg_contents_text_height total_simulation_run_group_box_height = ( - GROUP_BOX_CONTENT_PADDING + CARD_CONTENT_PADDING + group_box_title_height - + GROUP_BOX_TITLE_BOTTOM_Y_MARGIN + + CARD_TITLE_BOTTOM_Y_MARGIN + column_header_height + total_qreg_contents_text_height - + GROUP_BOX_CONTENT_PADDING + + CARD_CONTENT_PADDING ) qreg_name_and_layout_info_column_width: int = ( SimulationRunOverviewStyledItemDelegate._get_required_qreg_name_and_layout_column_width( - option, index, GROUP_BOX_TITLE_FONT_SIZE + option, index, CARD_TITLE_FONT_SIZE ) ) qreg_content_header_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( - DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, option.font, GROUP_BOX_CONTENT_FONT_SIZE + DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, option.font, CARD_CONTENT_FONT_SIZE ) max_qreg_qubits_column_width: int = ( SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( option, index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, ) ) max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) - total_simulation_run_group_box_width = ( - GROUP_BOX_CONTENT_PADDING - + qreg_name_and_layout_info_column_width - + QREG_CONTENT_X_SPACING - + max_qreg_content_column_width - + QREG_CONTENT_X_SPACING - + QREG_CONTENT_X_SPACING - + max_qreg_content_column_width - + QREG_CONTENT_X_SPACING - + GROUP_BOX_CONTENT_PADDING + total_simulation_run_group_box_width = max( + group_box_title_width, + ( + CARD_CONTENT_PADDING + + qreg_name_and_layout_info_column_width + + QREG_CONTENT_X_SPACING + + max_qreg_content_column_width + + QREG_CONTENT_X_SPACING + + QREG_CONTENT_X_SPACING + + max_qreg_content_column_width + + QREG_CONTENT_X_SPACING + + CARD_CONTENT_PADDING + ), ) return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) @@ -162,15 +168,28 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) available_rect_for_content: QtCore.QRect = option.rect.adjusted( - GROUP_BOX_CONTENT_PADDING, - GROUP_BOX_CONTENT_PADDING, - -GROUP_BOX_CONTENT_PADDING, - -GROUP_BOX_CONTENT_PADDING, + CARD_CONTENT_PADDING, + CARD_CONTENT_PADDING, + -CARD_CONTENT_PADDING, + -CARD_CONTENT_PADDING, ) painter.save() + required_text_width_for_header_for_largest_sim_run_number: Final[int] = min( + QREG_CONTENT_X_SPACING + + SimulationRunOverviewStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( + index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE + ) + + QREG_CONTENT_X_SPACING, + available_rect_for_content.width(), + ) + header_text_bottom_left_point: QtCore.QPoint = self._draw_card_border_and_header( - painter, option, simulation_run_number=index.row(), card_content_rect=available_rect_for_content + painter, + option, + simulation_run_number=index.row(), + card_content_rect=available_rect_for_content, + available_header_width=required_text_width_for_header_for_largest_sim_run_number, ) header_column_rects: list[QtCore.QRect] = self._draw_and_determine_column_headers( @@ -183,9 +202,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, row_idx: int = 1 per_row_y_offset: int = ( QREG_CONTENT_Y_SPACING - + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( - option.font, GROUP_BOX_CONTENT_FONT_SIZE - ) + + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) ) for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): curr_row_y_offset: int = row_idx * per_row_y_offset @@ -193,7 +210,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, painter, qreg_layout.qreg_name, header_row_column_one_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, ) SimulationRunOverviewStyledItemDelegate._draw_elided_text( @@ -204,7 +221,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, qreg_layout.qreg_size, ), header_row_column_two_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, ) SimulationRunOverviewStyledItemDelegate._draw_elided_text( @@ -217,7 +234,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, if associated_input_output_mapping.expected_output_state is not None else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, header_row_column_three_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, ) SimulationRunOverviewStyledItemDelegate._draw_elided_text( @@ -240,6 +257,7 @@ def _draw_card_border_and_header( option: QtWidgets.QStyleOptionViewItem, simulation_run_number: int, card_content_rect: QtCore.QRect, + available_header_width: int, draw_rect_corners: bool = False, ) -> QtCore.QPoint: painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) @@ -257,17 +275,15 @@ def _draw_card_border_and_header( painter.setBrush(option.palette.highlightedText()) header_text: str = DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT.format(simulation_run_number=simulation_run_number) - header_text_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( - header_text, option.font, GROUP_BOX_TITLE_FONT_SIZE - ) + SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text(header_text, option.font, CARD_TITLE_FONT_SIZE) header_text_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( - option.font, GROUP_BOX_TITLE_FONT_SIZE + option.font, CARD_TITLE_FONT_SIZE ) header_text_rect = QtCore.QRect( - card_content_rect.x(), card_content_rect.y(), header_text_width + 10, header_text_height + card_content_rect.x(), card_content_rect.y(), available_header_width + 10, header_text_height ) SimulationRunOverviewStyledItemDelegate._draw_elided_text( - painter, header_text, header_text_rect, GROUP_BOX_TITLE_FONT_SIZE, draw_bold_text=True + painter, header_text, header_text_rect, CARD_TITLE_FONT_SIZE, draw_bold_text=True ) return header_text_rect.bottomLeft() @@ -281,7 +297,7 @@ def _draw_and_determine_column_headers( draw_rect_corners: bool = False, ) -> list[QtCore.QRect]: qreg_name_and_layout_info_column_width: int = ( - self._get_required_qreg_name_and_layout_column_width(option, index, GROUP_BOX_CONTENT_FONT_SIZE) + self._get_required_qreg_name_and_layout_column_width(option, index, CARD_CONTENT_FONT_SIZE) + QREG_CONTENT_X_SPACING ) input_state_qreg_content_column_width: int = ( @@ -289,7 +305,7 @@ def _draw_and_determine_column_headers( + SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( option, index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, ) + QREG_CONTENT_X_SPACING ) @@ -311,9 +327,9 @@ def _draw_and_determine_column_headers( header_row_column_one_rect = QtCore.QRect( header_text_bottom_left_point.x(), - header_text_bottom_left_point.y() + 2 * GROUP_BOX_TITLE_BOTTOM_Y_MARGIN, + header_text_bottom_left_point.y() + 2 * CARD_TITLE_BOTTOM_Y_MARGIN, qreg_name_and_layout_info_column_width, - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, GROUP_BOX_CONTENT_FONT_SIZE), + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), ) header_row_column_one_text_rect: QtCore.QRect = header_row_column_one_rect.adjusted( QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 @@ -322,7 +338,7 @@ def _draw_and_determine_column_headers( painter, DEFAULT_QREG_NAME_COLUMN_HEADER, header_row_column_one_text_rect, - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, draw_bold_text=True, ) @@ -330,7 +346,7 @@ def _draw_and_determine_column_headers( header_row_column_one_rect.topRight().x(), header_row_column_one_rect.topRight().y(), input_state_qreg_content_column_width, - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, GROUP_BOX_CONTENT_FONT_SIZE), + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), ) header_row_column_two_text_rect: QtCore.QRect = header_row_column_two_rect.adjusted( QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 @@ -339,7 +355,7 @@ def _draw_and_determine_column_headers( painter, DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, header_row_column_two_text_rect, - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, draw_bold_text=True, ) @@ -347,7 +363,7 @@ def _draw_and_determine_column_headers( header_row_column_two_rect.topRight().x(), header_row_column_two_rect.topRight().y(), output_state_qreg_content_column_width, - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, GROUP_BOX_CONTENT_FONT_SIZE), + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), ) header_row_column_three_text_rect: QtCore.QRect = header_row_column_three_rect.adjusted( QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 @@ -356,7 +372,7 @@ def _draw_and_determine_column_headers( painter, DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER, header_row_column_three_text_rect, - GROUP_BOX_CONTENT_FONT_SIZE, + CARD_CONTENT_FONT_SIZE, draw_bold_text=True, ) From fa15d2cb3b07fc9cab3f0b84b7bfba558c15804a Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 13 Jan 2026 23:58:26 +0100 Subject: [PATCH 23/88] Refactored all simulation run combinations generator into worker instead of executing operation on UI thread --- .../quantum_circuit_simulation_dialog.py | 40 +++- .../all_input_states_generator_worker.py | 118 ++++++++++ .../qt_all_input_states_generator_dialog.py | 203 ++++++++++++++++++ .../qt_simulation_run_dialog.py | 11 +- ...ation_run_overview_styled_item_delegate.py | 10 +- 5 files changed, 364 insertions(+), 18 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/all_input_states_generator_worker.py create mode 100644 python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index dfa06048..0e58c4be 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -15,6 +15,7 @@ from mqt import syrec +from .simulation_view.qt_all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog from .simulation_view.qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel @@ -32,6 +33,7 @@ SIMULATION_RUNS_LIST_VIEW_NAME: Final[str] = "sim_runs_list_view" +# TODO: Should a confirmation be requested when the dialog is closed and simulation runs exist? class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtWidgets.QWidget @@ -53,6 +55,7 @@ def __init__( self.setGeometry(self.left, self.top, self.width, self.height) self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None + self.all_input_states_generator_dialog: AllInputStatesGeneratorDialog | None = None self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self.simulation_run_dialog: SimulationRunDialog | None = None @@ -296,6 +299,27 @@ def handle_simulation_run_editor_dialog_close(self, result: int) -> None: finally: self.simulation_run_editor_dialog = None + def handle_open_and_start_all_input_states_generator_dialog(self, input_state_size: int) -> None: + if self.all_input_states_generator_dialog is not None: + # TODO: Error logging? + return + + self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog(self.simulation_runs_model, self) + self.all_input_states_generator_dialog.finished.connect(self.handle_input_states_generator_dialog_close) + self.all_input_states_generator_dialog.show() + self.all_input_states_generator_dialog.start_generation(input_state_size) + + def handle_input_states_generator_dialog_close(self, result: int) -> None: + self.all_input_states_generator_dialog = None + + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + return + + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted + ) + def handle_simulation_run_delete_btn_click(self) -> None: curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() if curr_active_tab_widget is None: @@ -400,12 +424,12 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i ) if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: - n_input_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits + n_input_state_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits # TODO: A large number of state combinations will lag the UI thread since the generation runs on the UI thread pressed_message_box_button_in_all_sim_run_generation_warning: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( self, "Generating all possible input state combinations!", - f"Are you sure that you want to generate {n_input_combinations} simulation runs, one for each input state combination?", + f"Are you sure that you want to generate {n_input_state_combinations} simulation runs, one for each input state combination?", buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, ) @@ -416,11 +440,15 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i ): self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - to_be_switched_to_tab_widget, True + self.handle_open_and_start_all_input_states_generator_dialog( + self.annotatable_quantum_computation.num_data_qubits ) - # TODO: Can we ignore return value? - self.simulation_runs_model.add_all_possible_simulation_run_models() + + # QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + # to_be_switched_to_tab_widget, True + # ) + # # TODO: Can we ignore return value? + # self.simulation_runs_model.add_all_possible_simulation_run_models() QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( prev_active_tab_widget, False diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py new file mode 100644 index 00000000..a3cefda1 --- /dev/null +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -0,0 +1,118 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import time +from typing import Final + +from PyQt6 import QtCore + +from mqt import syrec + +from .qt_simulation_run_model import SimulationRunModel + +# input_state_batch_type = list[syrec.n_bit_values_container] +# QtCore.qRegisterMetaType(input_state_batch_type, "input_state_batch_type") + + +class AllInputStatesGeneratorWorker(QtCore.QObject): # type: ignore[misc] + batch_generated = QtCore.pyqtSignal(list, name="batchGenerated") + generation_failed = QtCore.pyqtSignal(Exception, name="generationFailed") + generation_cancelled = QtCore.pyqtSignal(name="generationCancelled") + generation_finished = QtCore.pyqtSignal(float, name="generationFinished") + + def __init__(self, expected_input_state_size: int, batch_size: int): + super().__init__() + + if expected_input_state_size < 0: + msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" + raise ValueError(msg) + + if batch_size < 1: + msg = f"Batch size must be larger than 0 but was actually {batch_size}" + raise ValueError(msg) + + self.expected_input_state_size: Final[int] = expected_input_state_size + self.batch_size: Final[int] = batch_size + self.cancellation_requested = False + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_generation(self) -> None: + n_states_to_generate: int = 2**self.expected_input_state_size + n_batches: int = n_states_to_generate // self.batch_size + + batch_data: list[SimulationRunModel | None] = [None for i in range(self.batch_size)] + integer_defining_input_state: int = 0 + + generation_start_time: float = time.perf_counter() + curr_batch_elem_count: int = 0 + generated_batches: int = 0 + try: + for _ in range((n_batches * self.batch_size) + 1): + if self.cancellation_requested: + break + + if curr_batch_elem_count == self.batch_size: + self.batch_generated.emit(batch_data) + generated_batches += 1 + if generated_batches == n_batches: + break + + curr_batch_elem_count = 0 + for j in range(self.batch_size): + batch_data[j] = None + + batch_data[curr_batch_elem_count] = ( + AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self.expected_input_state_size, integer_defining_input_state + ) + ) + integer_defining_input_state += 1 + curr_batch_elem_count += 1 + + n_elems_in_last_batch: int = n_states_to_generate % self.batch_size + curr_batch_elem_count = 0 + if n_elems_in_last_batch != 0: + # Truncate batch result container to size of last batch + del batch_data[n_elems_in_last_batch:] + for i in range(n_elems_in_last_batch): + if self.cancellation_requested: + break + + batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self.expected_input_state_size, integer_defining_input_state + ) + integer_defining_input_state += 1 + self.batch_generated.emit(batch_data) + except Exception as err: + # TODO: Slot in dialog is missing positional argument err? + self.generation_failed.emit(err) + + generation_end_time: float = time.perf_counter() + total_generation_runtime: float = generation_end_time - generation_start_time + self.generation_finished.emit(total_generation_runtime) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def request_cancellation(self) -> None: + self.cancellation_requested = True + + @staticmethod + def _generate_sim_run_model_for_input_state( + expected_input_state_size: int, integer_defining_input_state: int + ) -> SimulationRunModel: + binary_string_of_i = format(integer_defining_input_state, "b") + input_state = syrec.n_bit_values_container(expected_input_state_size) + + n_qubits_to_process_in_binary_string: int = min(expected_input_state_size, len(binary_string_of_i)) + qubit_idx_in_binary_string: int = n_qubits_to_process_in_binary_string - 1 + for qubit in range(n_qubits_to_process_in_binary_string): + qubit_value: bool = binary_string_of_i[qubit_idx_in_binary_string] == "1" + input_state.set(qubit, qubit_value) + qubit_idx_in_binary_string -= 1 + return SimulationRunModel(input_state, expected_output_state=None) diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py new file mode 100644 index 00000000..5d4ecf88 --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -0,0 +1,203 @@ +# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +# Copyright (c) 2025 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PyQt6 import QtCore, QtWidgets + +if TYPE_CHECKING: + from PyQt6 import QtGui + + from mqt import syrec + + from .qt_simulation_run_model import QtSimulationRunModel + +from .all_input_states_generator_worker import AllInputStatesGeneratorWorker + + +class AllInputStatesGeneratorDialog(QtWidgets.QDialog): # type: ignore[misc] + def __init__(self, shared_simulation_runs_model: QtSimulationRunModel, parent: QtWidgets.QWidget): + super().__init__(parent) + + # TODO: Member variable could also be initialized in start_simulations + self.shared_simulation_runs_model: QtSimulationRunModel = shared_simulation_runs_model + self.worker_thread: QtCore.QThread | None = None + self.worker: AllInputStatesGeneratorWorker | None = None + + self.num_generated_input_states: int = 0 + self.stop_processing_recv_input_state_batches: bool = False + self.setModal(True) + self.setSizeGripEnabled(True) + self.setWindowTitle("Generating simulation runs...") + left = 0 + top = 0 + width = 400 + height = 200 + self.setGeometry(left, top, width, height) + + main_layout = QtWidgets.QVBoxLayout() + self.progress_bar = QtWidgets.QProgressBar() + # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format + self.progress_bar.setFormat("Generated %v out of %m input states") + self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.err_text_lbl = QtWidgets.QLabel("") + self.err_text_lbl.setStyleSheet("QLabel { color : red; }") + self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.progress_text_lbl = QtWidgets.QLabel("") + self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.dialog_button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + self.dialog_button_box.setCenterButtons(True) + self.dialog_button_box.rejected.connect(self._handle_input_state_generation_cancel_button_click) + self.dialog_button_box.accepted.connect(self.accept) + + self._change_dialog_ok_button_enable_state(False) + self._change_dialog_cancellation_button_enable_state(False) + + main_layout.addWidget(self.progress_bar) + main_layout.addWidget(self.progress_text_lbl) + main_layout.addWidget(self.err_text_lbl) + main_layout.addWidget(self.dialog_button_box) + self.setLayout(main_layout) + + def start_generation(self, expected_input_state_size: int, batch_size: int = 50) -> None: + self.stop_processing_recv_input_state_batches = False + self.num_generated_input_states = 0 + self.progress_text_lbl.setText("") + + try: + self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) + except ValueError as err: + self.err_text_lbl.setText(f"Error {err=}, {type(err)=} during initialization of input states generator!") + return + + # TODO: Validation that maximum value can actually be stored in progress bar maximum (should validation be performed in dialog or by caller?) + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(2**expected_input_state_size) + self.progress_bar.setValue(0) + + self.worker_thread = QtCore.QThread() + # TODO: It is recommended in the official documentation to mark slots explicitly via the QtCore.pyqtSlot() decorator: + # see https://doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html#the-slot-class + + # Do not block the UI thread by the potentially long running operations of the worker a new thread is started (which also has its own event loop) + # and the worker operation moved to the latter. We also do not want to block the UI thread by executing the slots of said worker in the UI thread but + # instead want to simply send the events to the event queue of its thread thus the QueuedConnection between the signal (here the UI thread) and the receiver (worker thread) + # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). + self.worker_thread.started.connect(self.worker.start_generation, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.batchGenerated.connect( + self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection + ) + # self.worker.generationCancelled.connect(None, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.generationFinished.connect( + self._handle_input_state_generator_finished, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker.generationFailed.connect( + self._handle_input_state_generator_failure, QtCore.Qt.ConnectionType.QueuedConnection + ) + + self.worker_thread.finished.connect(self.worker_thread.deleteLater) + self.worker_thread.finished.connect(self._reset_workers) + + self.worker.moveToThread(self.worker_thread) + self.worker_thread.start() + self._change_dialog_cancellation_button_enable_state(True) + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + # Ask for confirmation before closing + if self.worker is None or self._handle_input_state_generation_cancel_button_click(): + event.accept() + else: + event.ignore() + + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_input_state_generator_failure(self, err: Exception) -> None: + self.progress_text_lbl.setText("") + self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during generation of input states") + self.shared_simulation_runs_model.delete_all_simulation_run_models() + self._request_worker_cancellation() + + @QtCore.pyqtSlot(list) # type: ignore[untyped-decorator] + def _handle_generated_input_state_batch(self, batch_data: list[syrec.n_bit_values_container]) -> None: + if self.stop_processing_recv_input_state_batches: + return + + self.progress_text_lbl.setText("Generated input state batch!") + self.num_generated_input_states += len(batch_data) + self.progress_bar.setValue(self.num_generated_input_states) + for i in range(len(batch_data)): + self.shared_simulation_runs_model.add_simulation_run_model(batch_data[i]) + self.progress_text_lbl.setText("") + + @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] + def _handle_input_state_generator_finished(self, total_generation_runtime: float) -> None: + self.progress_text_lbl.setText(f"Input state generator finished! Total runtime: {total_generation_runtime}") + self.progress_bar.setVisible(False) + self._request_worker_cancellation() + + if self.worker_thread is not None: + self.worker_thread.quit() + self.worker_thread.wait() + self.progress_text_lbl.setText("Input state generator thread finished!") + + self._change_dialog_ok_button_enable_state(True) + self._change_dialog_cancellation_button_enable_state(False) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _request_worker_cancellation(self) -> None: + self.stop_processing_recv_input_state_batches = True + self.progress_text_lbl.setText("Requesting cancellation of input state generator!") + if self.worker is not None: + self.worker.request_cancellation() + self._change_dialog_cancellation_button_enable_state(False) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_input_state_generation_cancel_button_click(self) -> bool: + clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( + self, + "Cancellation of generation of input states requested!", + "Are you sure that you want to stop the generation of the input states? This will cause the deletion of all already generated input states.", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: + self._request_worker_cancellation() + self.shared_simulation_runs_model.delete_all_simulation_run_models() + return True + return False + + def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + + if dialog_cancel_button is None: + return + + dialog_cancel_button.setEnabled(should_button_be_enabled) + + def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_ok_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ) + + if dialog_ok_button is None: + return + + dialog_ok_button.setEnabled(should_button_be_enabled) + + def _reset_workers(self) -> None: + self.worker_thread = None + self.worker = None diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index 63576eb9..1abc9134 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -16,9 +16,8 @@ from mqt import syrec from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel - from .qt_simulation_worker import SimulationRunResult -from .qt_simulation_worker import SimulationWorker, ToBeExecutedSimulationRun +from .qt_simulation_worker import SimulationRunResult, SimulationWorker, ToBeExecutedSimulationRun from .styled_item_delegates.qt_simulation_run_execution_styled_item_delegate import ( SimulationRunExecutionStyledItemDelegate, ) @@ -162,6 +161,7 @@ def start_simulations( # TODO: Mark remaining member functions as private via underscore prefix? # TODO: Not all simulation runs are executed? (2 out of 10) but no error is printed to the console or shown in the GUI. + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_all_simulation_runs_done(self) -> None: # self.simulation_run_total_runtime_timer.stop() @@ -180,12 +180,14 @@ def _handle_all_simulation_runs_done(self) -> None: f"Finished {self.num_completed_simulation_runs} out of all {self.expected_total_num_simulation_runs} simulation runs!" ) + @QtCore.pyqtSlot(int, Exception) # type: ignore[untyped-decorator] def _handle_simulation_runs_stopped_due_to_err(self, simulation_run_num_that_failed: int, err: Exception) -> None: self.simulation_run_err_lbl.setText( f"Unexpected {err=}, {type(err)=} during execution of simulation run {simulation_run_num_that_failed}" ) self._request_worker_cancellation() + @QtCore.pyqtSlot(ToBeExecutedSimulationRun) # type: ignore[untyped-decorator] def _handle_simulation_runs_stopped_after_first_failure( self, simulation_run_causing_err: ToBeExecutedSimulationRun ) -> None: @@ -211,10 +213,7 @@ def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabl def closeEvent(self, _): # noqa: N802 self._request_worker_cancellation() - # if self.worker_thread is not None: - # self.worker_thread.quit() - # self.worker_thread.wait() - + @QtCore.pyqtSlot(SimulationRunResult) # type: ignore[untyped-decorator] def _handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: self._update_progress_controls(simulation_run_result.simulation_run_number) try: diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py index 1c84b364..e5d02640 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py @@ -48,8 +48,6 @@ class SimulationRunOverviewStyledItemDelegate(BaseSimulationRunStyledItemDelegat def __init__(self, parent=None): super().__init__(parent) - # TODO: Mark as const: https://stackoverflow.com/a/57596202 - @staticmethod def _get_required_qreg_name_and_layout_column_width( option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, font_size: int @@ -165,7 +163,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, if not index.isValid() or option.rect.width() == 0: return - associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + associated_sim_run_model: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) available_rect_for_content: QtCore.QRect = option.rect.adjusted( CARD_CONTENT_PADDING, @@ -216,7 +214,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, SimulationRunOverviewStyledItemDelegate._draw_elided_text( painter, SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.input_state, + associated_sim_run_model.input_state, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, ), @@ -227,11 +225,11 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, SimulationRunOverviewStyledItemDelegate._draw_elided_text( painter, SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_input_output_mapping.expected_output_state, + associated_sim_run_model.expected_output_state, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, ) - if associated_input_output_mapping.expected_output_state is not None + if associated_sim_run_model.expected_output_state is not None else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, header_row_column_three_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), CARD_CONTENT_FONT_SIZE, From 34ba955857df60c815bd77fbb63fc69ef57ae231 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 15 Jan 2026 01:28:10 +0100 Subject: [PATCH 24/88] Small improvements helping to reduce a complete hang of the GUI added to the all input states generator dialog, however rendering is still laggy/sluggish --- .../quantum_circuit_simulation_dialog.py | 4 +- .../all_input_states_generator_worker.py | 103 +++++++++--------- .../qt_all_input_states_generator_dialog.py | 102 +++++++++++++---- .../qt_simulation_run_model.py | 10 ++ 4 files changed, 140 insertions(+), 79 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 0e58c4be..d0cf88f0 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -304,10 +304,10 @@ def handle_open_and_start_all_input_states_generator_dialog(self, input_state_si # TODO: Error logging? return - self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog(self.simulation_runs_model, self) + self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog(self) self.all_input_states_generator_dialog.finished.connect(self.handle_input_states_generator_dialog_close) self.all_input_states_generator_dialog.show() - self.all_input_states_generator_dialog.start_generation(input_state_size) + self.all_input_states_generator_dialog.start_generation(self.simulation_runs_model, input_state_size) def handle_input_states_generator_dialog_close(self, result: int) -> None: self.all_input_states_generator_dialog = None diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index a3cefda1..57922db1 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -17,15 +17,12 @@ from .qt_simulation_run_model import SimulationRunModel -# input_state_batch_type = list[syrec.n_bit_values_container] -# QtCore.qRegisterMetaType(input_state_batch_type, "input_state_batch_type") - class AllInputStatesGeneratorWorker(QtCore.QObject): # type: ignore[misc] - batch_generated = QtCore.pyqtSignal(list, name="batchGenerated") + batch_generated = QtCore.pyqtSignal(tuple, name="batchGenerated") generation_failed = QtCore.pyqtSignal(Exception, name="generationFailed") generation_cancelled = QtCore.pyqtSignal(name="generationCancelled") - generation_finished = QtCore.pyqtSignal(float, name="generationFinished") + generation_finished = QtCore.pyqtSignal(name="generationFinished") def __init__(self, expected_input_state_size: int, batch_size: int): super().__init__() @@ -41,78 +38,78 @@ def __init__(self, expected_input_state_size: int, batch_size: int): self.expected_input_state_size: Final[int] = expected_input_state_size self.batch_size: Final[int] = batch_size self.cancellation_requested = False + self.cancellation_flag_mutex = QtCore.QMutex() + self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_generation(self) -> None: n_states_to_generate: int = 2**self.expected_input_state_size n_batches: int = n_states_to_generate // self.batch_size - batch_data: list[SimulationRunModel | None] = [None for i in range(self.batch_size)] - integer_defining_input_state: int = 0 - - generation_start_time: float = time.perf_counter() - curr_batch_elem_count: int = 0 - generated_batches: int = 0 + batch_generation_start_time: float = time.perf_counter() + batch_generation_end_time: float = 0 + batch_generation_duration_in_seconds: float = 0 try: - for _ in range((n_batches * self.batch_size) + 1): + first_integer_encoding_first_state_of_batch: int = 0 + batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] + for _ in range(n_batches): + # TODO: Do we need to use locks to read value? Maybe use QReadWriterLock instead? if self.cancellation_requested: break - if curr_batch_elem_count == self.batch_size: - self.batch_generated.emit(batch_data) - generated_batches += 1 - if generated_batches == n_batches: - break + for i in range(self.batch_size): + batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i + ) - curr_batch_elem_count = 0 - for j in range(self.batch_size): - batch_data[j] = None + batch_generation_end_time = time.perf_counter() + batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time + batch_generation_start_time = batch_generation_end_time - batch_data[curr_batch_elem_count] = ( - AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( - self.expected_input_state_size, integer_defining_input_state - ) - ) - integer_defining_input_state += 1 - curr_batch_elem_count += 1 + self.batch_generated.emit((batch_generation_duration_in_seconds, batch_data.copy())) + with QtCore.QMutexLocker(self.cancellation_flag_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.cancellation_flag_mutex) + + first_integer_encoding_first_state_of_batch += self.batch_size + for i in range(self.batch_size): + batch_data[i] = None n_elems_in_last_batch: int = n_states_to_generate % self.batch_size - curr_batch_elem_count = 0 - if n_elems_in_last_batch != 0: - # Truncate batch result container to size of last batch - del batch_data[n_elems_in_last_batch:] + if n_elems_in_last_batch != 0 and not self.cancellation_requested: + last_batch_data: list[SimulationRunModel | None] = [None for _ in range(n_elems_in_last_batch)] for i in range(n_elems_in_last_batch): - if self.cancellation_requested: - break - - batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( - self.expected_input_state_size, integer_defining_input_state + # TODO: Do we need to use locks to read value? Maybe use QReadWriterLock instead? + # if self.cancellation_requested: + # break + last_batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i ) - integer_defining_input_state += 1 - self.batch_generated.emit(batch_data) + + batch_generation_end_time = time.perf_counter() + batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time + batch_generation_start_time = batch_generation_end_time + self.batch_generated.emit((batch_generation_duration_in_seconds, last_batch_data)) except Exception as err: - # TODO: Slot in dialog is missing positional argument err? self.generation_failed.emit(err) + self.generation_finished.emit() - generation_end_time: float = time.perf_counter() - total_generation_runtime: float = generation_end_time - generation_start_time - self.generation_finished.emit(total_generation_runtime) - - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + # In the cross thread communication between the main thread (rendering the GUI) and the worker thread we had the issue that if we define the slot function with a QtCore.pyqtSlot() decorator + # the main thread will not invoke said slot in the worker thread but we do not know exactly we since other signal->slot connections between the two threads function when being defined with + # corresponding decorators. Thus for now we define the slot without a decorator. def request_cancellation(self) -> None: - self.cancellation_requested = True + with QtCore.QMutexLocker(self.cancellation_flag_mutex): + self.cancellation_requested = True + self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + + # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. + def ack_batch_processed(self) -> None: + self.wait_on_batch_processed_acknowledgement_condition.wakeAll() @staticmethod def _generate_sim_run_model_for_input_state( expected_input_state_size: int, integer_defining_input_state: int ) -> SimulationRunModel: - binary_string_of_i = format(integer_defining_input_state, "b") input_state = syrec.n_bit_values_container(expected_input_state_size) - - n_qubits_to_process_in_binary_string: int = min(expected_input_state_size, len(binary_string_of_i)) - qubit_idx_in_binary_string: int = n_qubits_to_process_in_binary_string - 1 - for qubit in range(n_qubits_to_process_in_binary_string): - qubit_value: bool = binary_string_of_i[qubit_idx_in_binary_string] == "1" - input_state.set(qubit, qubit_value) - qubit_idx_in_binary_string -= 1 + for qubit in range(expected_input_state_size): + input_state.set(qubit, bool((integer_defining_input_state >> qubit) & 1)) return SimulationRunModel(input_state, expected_output_state=None) diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 5d4ecf88..b0fedc57 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -8,31 +8,38 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from PyQt6 import QtCore, QtWidgets if TYPE_CHECKING: from PyQt6 import QtGui - from mqt import syrec - - from .qt_simulation_run_model import QtSimulationRunModel + from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel from .all_input_states_generator_worker import AllInputStatesGeneratorWorker +TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = ( + "Total runtime for input states generation [in seconds]: {total_runtime_in_sec:f}" +) + class AllInputStatesGeneratorDialog(QtWidgets.QDialog): # type: ignore[misc] - def __init__(self, shared_simulation_runs_model: QtSimulationRunModel, parent: QtWidgets.QWidget): + # input_state_batch_ack = QtCore.pyqtSignal(name="inputStateBatchAck") + input_state_batch_ack = QtCore.pyqtSignal(name="inputStateBatchAck") + request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") + + def __init__(self, parent: QtWidgets.QWidget): super().__init__(parent) - # TODO: Member variable could also be initialized in start_simulations - self.shared_simulation_runs_model: QtSimulationRunModel = shared_simulation_runs_model + self.shared_simulation_runs_model: QtSimulationRunModel | None = None self.worker_thread: QtCore.QThread | None = None self.worker: AllInputStatesGeneratorWorker | None = None self.num_generated_input_states: int = 0 self.stop_processing_recv_input_state_batches: bool = False + self.total_input_state_generation_runtime_in_seconds: float = 0 + self.setModal(True) self.setSizeGripEnabled(True) self.setWindowTitle("Generating simulation runs...") @@ -68,13 +75,23 @@ def __init__(self, shared_simulation_runs_model: QtSimulationRunModel, parent: Q main_layout.addWidget(self.progress_bar) main_layout.addWidget(self.progress_text_lbl) main_layout.addWidget(self.err_text_lbl) + + main_layout.addStretch() + self.total_runtime_text_lbl = QtWidgets.QLabel("") + self.total_runtime_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + main_layout.addWidget(self.total_runtime_text_lbl) main_layout.addWidget(self.dialog_button_box) self.setLayout(main_layout) - def start_generation(self, expected_input_state_size: int, batch_size: int = 50) -> None: + def start_generation( + self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 500 + ) -> None: + self.shared_simulation_runs_model = shared_simulation_runs_model self.stop_processing_recv_input_state_batches = False self.num_generated_input_states = 0 + self.total_input_state_generation_runtime_in_seconds = 0 self.progress_text_lbl.setText("") + self.total_runtime_text_lbl.setText("") try: self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) @@ -87,7 +104,13 @@ def start_generation(self, expected_input_state_size: int, batch_size: int = 50) self.progress_bar.setMaximum(2**expected_input_state_size) self.progress_bar.setValue(0) + self.inputStateBatchAck.connect(self.worker.ack_batch_processed, QtCore.Qt.ConnectionType.QueuedConnection) + self.requestWorkerCancellation.connect( + self.worker.request_cancellation, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker_thread = QtCore.QThread() + self.worker.moveToThread(self.worker_thread) # TODO: It is recommended in the official documentation to mark slots explicitly via the QtCore.pyqtSlot() decorator: # see https://doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html#the-slot-class @@ -99,7 +122,6 @@ def start_generation(self, expected_input_state_size: int, batch_size: int = 50) self.worker.batchGenerated.connect( self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection ) - # self.worker.generationCancelled.connect(None, QtCore.Qt.ConnectionType.QueuedConnection) self.worker.generationFinished.connect( self._handle_input_state_generator_finished, QtCore.Qt.ConnectionType.QueuedConnection ) @@ -109,9 +131,7 @@ def start_generation(self, expected_input_state_size: int, batch_size: int = 50) self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) - - self.worker.moveToThread(self.worker_thread) - self.worker_thread.start() + self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancellation_button_enable_state(True) def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 @@ -121,28 +141,62 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 else: event.ignore() + def test(self) -> None: + pass + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] def _handle_input_state_generator_failure(self, err: Exception) -> None: self.progress_text_lbl.setText("") self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during generation of input states") - self.shared_simulation_runs_model.delete_all_simulation_run_models() + if self.shared_simulation_runs_model is not None: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + else: + QtWidgets.QMessageBox.critical( + self, + "Internal state error!", + "Shared simulation runs model was not initialized during handling of input state generator failure!\nThis should not happen.", + buttons=QtWidgets.QMessageBox.StandardButton.Ok, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) self._request_worker_cancellation() - @QtCore.pyqtSlot(list) # type: ignore[untyped-decorator] - def _handle_generated_input_state_batch(self, batch_data: list[syrec.n_bit_values_container]) -> None: + @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] + def _handle_generated_input_state_batch(self, batch_data: tuple[float, list[SimulationRunModel]]) -> None: if self.stop_processing_recv_input_state_batches: return self.progress_text_lbl.setText("Generated input state batch!") - self.num_generated_input_states += len(batch_data) + batch_generation_duration_in_seconds: float = batch_data[0] + generated_simulation_run_models: list[SimulationRunModel] = batch_data[1] + + if self.shared_simulation_runs_model is None: + QtWidgets.QMessageBox.critical( + self, + "Internal state error!", + "Shared simulation runs model was not initialized during handling of generated input state batch!\nThis should not happen.", + buttons=QtWidgets.QMessageBox.StandardButton.Ok, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + self._request_worker_cancellation() + return + + # TODO: Error handling + # TODO: Use delayed processing to reduce "laggy"/almost frozen GUI + self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) + self.inputStateBatchAck.emit() + + self.total_input_state_generation_runtime_in_seconds += batch_generation_duration_in_seconds + self.total_runtime_text_lbl.setText( + TOTAL_RUNTIME_TEXT_FORMAT.format(total_runtime_in_sec=self.total_input_state_generation_runtime_in_seconds) + ) + + self.num_generated_input_states += len(generated_simulation_run_models) self.progress_bar.setValue(self.num_generated_input_states) - for i in range(len(batch_data)): - self.shared_simulation_runs_model.add_simulation_run_model(batch_data[i]) self.progress_text_lbl.setText("") - @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] - def _handle_input_state_generator_finished(self, total_generation_runtime: float) -> None: - self.progress_text_lbl.setText(f"Input state generator finished! Total runtime: {total_generation_runtime}") + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_input_state_generator_finished(self) -> None: + self.progress_text_lbl.setText("Input state generator finished!") self.progress_bar.setVisible(False) self._request_worker_cancellation() @@ -154,12 +208,11 @@ def _handle_input_state_generator_finished(self, total_generation_runtime: float self._change_dialog_ok_button_enable_state(True) self._change_dialog_cancellation_button_enable_state(False) - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _request_worker_cancellation(self) -> None: self.stop_processing_recv_input_state_batches = True self.progress_text_lbl.setText("Requesting cancellation of input state generator!") if self.worker is not None: - self.worker.request_cancellation() + self.requestWorkerCancellation.emit() self._change_dialog_cancellation_button_enable_state(False) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -174,7 +227,8 @@ def _handle_input_state_generation_cancel_button_click(self) -> bool: if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: self._request_worker_cancellation() - self.shared_simulation_runs_model.delete_all_simulation_run_models() + if self.shared_simulation_runs_model is not None: + self.shared_simulation_runs_model.delete_all_simulation_run_models() return True return False diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 9bd7cbbd..1edf1d30 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -236,6 +236,16 @@ def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> self.endInsertRows() return True + def add_simulation_run_models(self, to_be_added_simulation_run_models: list[SimulationRunModel]) -> None: + if len(to_be_added_simulation_run_models) == 0: + return + + idx_of_first_new_sim_run_model: int = len(self.simulation_run_models) + idx_of_last_new_sim_run_model: int = idx_of_first_new_sim_run_model + len(to_be_added_simulation_run_models) - 1 + self.beginInsertRows(QtCore.QModelIndex(), idx_of_first_new_sim_run_model, idx_of_last_new_sim_run_model) + self.simulation_run_models.extend(to_be_added_simulation_run_models) + self.endInsertRows() + def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: self.beginRemoveRows(QtCore.QModelIndex(), index.row(), index.row()) From a11727ced27b0d0c15b942cc1a1c2e7e35e9c55e Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 15 Jan 2026 11:08:58 +0100 Subject: [PATCH 25/88] All input states generator now used QReadWriteLock instead of QMutex. Added artifically delay to input states generator to improve UI responsiveness --- .../all_input_states_generator_worker.py | 41 ++++++++++++++++--- .../qt_all_input_states_generator_dialog.py | 2 +- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 57922db1..1b241b13 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -38,7 +38,7 @@ def __init__(self, expected_input_state_size: int, batch_size: int): self.expected_input_state_size: Final[int] = expected_input_state_size self.batch_size: Final[int] = batch_size self.cancellation_requested = False - self.cancellation_flag_mutex = QtCore.QMutex() + self.cancellation_flag_mutex = QtCore.QReadWriteLock() self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -54,7 +54,7 @@ def start_generation(self) -> None: batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] for _ in range(n_batches): # TODO: Do we need to use locks to read value? Maybe use QReadWriterLock instead? - if self.cancellation_requested: + if self._thread_safe_check_whether_cancellation_is_requested(): break for i in range(self.batch_size): @@ -67,15 +67,22 @@ def start_generation(self) -> None: batch_generation_start_time = batch_generation_end_time self.batch_generated.emit((batch_generation_duration_in_seconds, batch_data.copy())) - with QtCore.QMutexLocker(self.cancellation_flag_mutex): + try: + self.cancellation_flag_mutex.lockForRead() + # Lock needs to be already held for wait condition to not return immediately self.wait_on_batch_processed_acknowledgement_condition.wait(self.cancellation_flag_mutex) + finally: + self.cancellation_flag_mutex.unlock() + # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using + # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. + time.sleep(0.1) first_integer_encoding_first_state_of_batch += self.batch_size for i in range(self.batch_size): batch_data[i] = None n_elems_in_last_batch: int = n_states_to_generate % self.batch_size - if n_elems_in_last_batch != 0 and not self.cancellation_requested: + if n_elems_in_last_batch != 0 and not self._thread_safe_check_whether_cancellation_is_requested(): last_batch_data: list[SimulationRunModel | None] = [None for _ in range(n_elems_in_last_batch)] for i in range(n_elems_in_last_batch): # TODO: Do we need to use locks to read value? Maybe use QReadWriterLock instead? @@ -96,9 +103,19 @@ def start_generation(self) -> None: # In the cross thread communication between the main thread (rendering the GUI) and the worker thread we had the issue that if we define the slot function with a QtCore.pyqtSlot() decorator # the main thread will not invoke said slot in the worker thread but we do not know exactly we since other signal->slot connections between the two threads function when being defined with # corresponding decorators. Thus for now we define the slot without a decorator. + # + # One explanation, generated by an AI agent, was: + # The decisive moment is when the decorator runs, not when you later do moveToThread. + # At import time (define time) the worker instance already exists and lives in the GUI thread; the decorator therefore registers the slot in the GUI thread's meta-object. + # Afterwards you move the object to the worker thread, but the meta-object data that Qt uses to locate the slot stays where it was created - in the GUI thread. + # When the GUI thread later emits the signal, Qt again looks in the GUI thread's meta-object, finds the entry that was created by the decorator, and tries to invoke it. + # Because the slot entry is marked as "belonging to another thread" (the worker thread), Qt posts a queued meta-call to that thread … but the worker thread has no corresponding meta-object entry, so nothing is executed. + # If you remove the decorator the connection is handled purely in Python (Qt simply stores the callable); the queued connection then works, because Python callables are independent of the meta-object system. + # In short: + # - pyqtSlot must be executed after the object has been moved to the target thread, or + # - drop the decorator and rely on the automatic queued connection that PyQt already provides. def request_cancellation(self) -> None: - with QtCore.QMutexLocker(self.cancellation_flag_mutex): - self.cancellation_requested = True + self._thread_safe_set_cancellation_requested_flag(True) self.wait_on_batch_processed_acknowledgement_condition.wakeAll() # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. @@ -113,3 +130,15 @@ def _generate_sim_run_model_for_input_state( for qubit in range(expected_input_state_size): input_state.set(qubit, bool((integer_defining_input_state >> qubit) & 1)) return SimulationRunModel(input_state, expected_output_state=None) + + def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: + cancellation_requested: bool = False + self.cancellation_flag_mutex.lockForRead() + cancellation_requested = self.cancellation_requested + self.cancellation_flag_mutex.unlock() + return cancellation_requested + + def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None: + self.cancellation_flag_mutex.lockForWrite() + self.cancellation_requested = flag_value + self.cancellation_flag_mutex.unlock() diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index b0fedc57..e175ceea 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -84,7 +84,7 @@ def __init__(self, parent: QtWidgets.QWidget): self.setLayout(main_layout) def start_generation( - self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 500 + self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 1000 ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model self.stop_processing_recv_input_state_batches = False From e238121ef77cd0ec7afe40d42d049cb18a5263e6 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 15 Jan 2026 11:09:41 +0100 Subject: [PATCH 26/88] mend --- .../simulation_view/all_input_states_generator_worker.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 1b241b13..4e8e3b2e 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -53,7 +53,6 @@ def start_generation(self) -> None: first_integer_encoding_first_state_of_batch: int = 0 batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] for _ in range(n_batches): - # TODO: Do we need to use locks to read value? Maybe use QReadWriterLock instead? if self._thread_safe_check_whether_cancellation_is_requested(): break @@ -85,9 +84,6 @@ def start_generation(self) -> None: if n_elems_in_last_batch != 0 and not self._thread_safe_check_whether_cancellation_is_requested(): last_batch_data: list[SimulationRunModel | None] = [None for _ in range(n_elems_in_last_batch)] for i in range(n_elems_in_last_batch): - # TODO: Do we need to use locks to read value? Maybe use QReadWriterLock instead? - # if self.cancellation_requested: - # break last_batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i ) From 50fb7a8f4552c102148567add498a86a7cd91796 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 15 Jan 2026 16:34:30 +0100 Subject: [PATCH 27/88] Simulation run dialog closeEvent is now correctly handled and correctly resets dialog instance. Update closeEvent implementaiton in AllInputStatesGeneratorDialog. Fixed required height calculation in simulation run execution styled item delegate --- .../quantum_circuit_simulation_dialog.py | 4 +-- .../qt_all_input_states_generator_dialog.py | 5 +-- .../qt_simulation_run_dialog.py | 33 +++++++++++++++---- .../qt_simulation_run_editor_dialog.py | 2 ++ ...tion_run_execution_styled_item_delegate.py | 5 +-- 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index d0cf88f0..91f3e252 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -486,13 +486,13 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) + self.simulation_run_dialog.show() self.simulation_run_dialog.start_simulations( self.annotatable_quantum_computation, num_simulation_runs, stop_at_first_output_state_mismatch ) - self.simulation_run_dialog.show() # TODO: Toggle state after edits in simulation runs were performed? - def handle_simulation_runs_dialog_close(self) -> None: + def handle_simulation_runs_dialog_close(self, _: int) -> None: self.simulation_run_dialog = None @staticmethod diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index e175ceea..84e9e666 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -137,13 +137,10 @@ def start_generation( def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing if self.worker is None or self._handle_input_state_generation_cancel_button_click(): - event.accept() + self.accept() else: event.ignore() - def test(self) -> None: - pass - @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] def _handle_input_state_generator_failure(self, err: Exception) -> None: self.progress_text_lbl.setText("") diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index 1abc9134..7aef26a3 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -13,6 +13,8 @@ from PyQt6 import QtCore, QtWidgets if TYPE_CHECKING: + from PyQt6 import QtGui + from mqt import syrec from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel @@ -106,7 +108,7 @@ def __init__(self, shared_simulation_run_model: QtSimulationRunModel, parent: Qt # TODO: One could also offer a close button in the dialog (that warns the user when closing the dialog during a simulation run execution)? self.dialog_button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Cancel) self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.rejected.connect(self._request_worker_cancellation) + self.dialog_button_box.rejected.connect(self._handle_simulation_runs_cancel_button_click) main_layout.addWidget(self.dialog_button_box) def start_simulations( @@ -127,8 +129,9 @@ def start_simulations( # self.simulation_run_total_runtime_timer.start(TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS) - self.worker_thread = QtCore.QThread() self.worker = SimulationWorker(annotatable_quantum_computation, stop_at_first_output_state_mismatch) + self.worker_thread = QtCore.QThread() + self.worker.moveToThread(self.worker_thread) # TODO: It is recommended in the official documentation to mark slots explicitly via the QtCore.pyqtSlot() decorator: # see https://doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html#the-slot-class @@ -154,8 +157,7 @@ def start_simulations( self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) - self.worker.moveToThread(self.worker_thread) - self.worker_thread.start() + self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._enqueue_next_simulation_run(0) self._change_dialog_cancellation_button_enable_state(True) @@ -210,8 +212,27 @@ def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabl dialog_cancel_button.setEnabled(should_button_be_enabled) - def closeEvent(self, _): # noqa: N802 - self._request_worker_cancellation() + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + # Ask for confirmation before closing + if self.worker is None or self.handle_simulation_runs_cancel_button_click(): + self.accept() + else: + event.ignore() + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_simulation_runs_cancel_button_click(self) -> bool: + clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( + self, + "Cancellation of simulation runs requested!", + "Are you sure that you want to stop the execution of the simulation runs?", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: + self._request_worker_cancellation() + return True + return False @QtCore.pyqtSlot(SimulationRunResult) # type: ignore[untyped-decorator] def _handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index 808e931c..64137b9d 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -457,6 +457,8 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid ) self.dialog_button_box.setCenterButtons(True) self.dialog_button_box.accepted.connect(self.accept) + # TODO: Only update copy of simulation run model to be able to discard changes + # TODO: Require confirmation to discard changes self.dialog_button_box.rejected.connect(self.reject) main_layout.addWidget(self.dialog_button_box) diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py index 396cfd77..456a498b 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py @@ -189,7 +189,7 @@ def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) ) return QtCore.QSize( min(option.rect.bottomRight().x(), required_content_size.width()), - min(option.rect.bottomRight().y(), required_content_size.height()), + max(option.rect.bottomRight().y(), required_content_size.height()), ) def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: @@ -208,9 +208,6 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, -CARD_CONTENT_PADDING, -CARD_CONTENT_PADDING, ) - # SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( - # painter, available_rect_for_content, 5, QtCore.Qt.GlobalColor.green, 0 - # ) painter.save() required_text_width_for_header_for_largest_sim_run_number: Final[int] = min( From 6497271fef20eac19109643d4fa6ecbeb25e0ee6 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 16 Jan 2026 00:48:08 +0100 Subject: [PATCH 28/88] Edit dialog of simulation run now uses copy of reference simulation run instead of working directly on the latter. --- .../quantum_circuit_simulation_dialog.py | 33 +++++++++++++++++-- .../qt_simulation_run_editor_dialog.py | 24 +++++++------- .../qt_simulation_run_model.py | 14 ++++++-- 3 files changed, 56 insertions(+), 15 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 91f3e252..813cc2dc 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -18,7 +18,11 @@ from .simulation_view.qt_all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog -from .simulation_view.qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel +from .simulation_view.qt_simulation_run_model import ( + SIMULATION_RUN_IO_STATE_QT_ROLE, + QtSimulationRunModel, + SimulationRunModel, +) from .simulation_view.styled_item_delegates.qt_simulation_run_overview_styled_item_delegate import ( SimulationRunOverviewStyledItemDelegate, ) @@ -272,7 +276,32 @@ def handle_simulation_run_edit_btn_click(self) -> None: if simulation_runs_list_view is None: return - self.simulation_run_editor_dialog = SimulationRunEditorDialog(simulation_runs_list_view.currentIndex(), self) + reference_sim_run_model: SimulationRunModel = simulation_runs_list_view.currentIndex().data( + SIMULATION_RUN_IO_STATE_QT_ROLE + ) + if ( + reference_sim_run_model.expected_output_state is not None + and reference_sim_run_model.input_state.size() != reference_sim_run_model.expected_output_state.size() + ): + QtWidgets.QMessageBox.critical( + self, + "Initial simulation run model validation error", + f"Expected reference simulation runs input state size (n={reference_sim_run_model.input_state.size()}) to match expected output states size (n={reference_sim_run_model.expected_output_state.size()})", + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + return + + # Since we want to be able to discard the changes made in the dialog by either closing the dialog or by pressing the cancel button + # a copy of the original simulation run object is needed + copy_of_reference_sim_run_model: SimulationRunModel = SimulationRunModel( + reference_sim_run_model.input_state, + reference_sim_run_model.expected_output_state, + create_new_n_bit_values_container_instances=True, + ) + + self.simulation_run_editor_dialog = SimulationRunEditorDialog( + simulation_runs_list_view.currentIndex(), copy_of_reference_sim_run_model, self + ) self.simulation_run_editor_dialog.finished.connect(self.handle_simulation_run_editor_dialog_close) self.simulation_run_editor_dialog.show() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index 64137b9d..31916449 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -14,15 +14,17 @@ from mqt import syrec -if TYPE_CHECKING: - from .qt_simulation_run_model import QuantumRegisterLayout, SimulationRunModel - from .qt_simulation_run_model import ( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE, QUANTUM_REGISTER_LAYOUT_QT_ROLE, - SIMULATION_RUN_IO_STATE_QT_ROLE, ) +if TYPE_CHECKING: + from .qt_simulation_run_model import ( + QuantumRegisterLayout, + SimulationRunModel, + ) + def stringify_some_qubits_of_n_bit_values_container( n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int @@ -59,12 +61,15 @@ def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 # TODO: Replace 'simple' returns with QDialog.reject("") to indicate fatal errors and stop editing simulation run but also reject changes made in parent widget that opened dialog class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] - def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWidgets.QWidget): + def __init__( + self, + simulation_run_model_index: QtCore.QModelIndex, + copy_of_reference_edit_sim_run_model: SimulationRunModel, + parent: QtWidgets.QWidget, + ): super().__init__(parent) self.simulation_run_model_index: QtCore.QModelIndex = simulation_run_model_index - self.edited_simulation_run_model: SimulationRunModel = simulation_run_model_index.data( - SIMULATION_RUN_IO_STATE_QT_ROLE - ) + self.edited_simulation_run_model: SimulationRunModel = copy_of_reference_edit_sim_run_model self.qreg_layouts: list[QuantumRegisterLayout] = simulation_run_model_index.data( QUANTUM_REGISTER_LAYOUT_QT_ROLE @@ -89,9 +94,6 @@ def __init__(self, simulation_run_model_index: QtCore.QModelIndex, parent: QtWid ) main_layout.addWidget(self.simulation_run_wrapper_box) - # TODO: Validation that input and output state have same size (validate all input parameters) - # TODO: Define validator for input and output state inputs - # TODO: Update input/output state value when qubit value is changed # TODO: How to render n-dimensional variables self.qubit_label_name_format = "q_{qubit:d}_lbl" diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 1edf1d30..ea30696c 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -56,13 +56,23 @@ def __init__( self, input_state: syrec.n_bit_values_container, expected_output_state: syrec.n_bit_values_container | None = None, + create_new_n_bit_values_container_instances: bool = False, ): if expected_output_state is not None and input_state.size() != expected_output_state.size(): msg = f"Expected output state size (n_qubits = {expected_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" raise ValueError(msg) - self.input_state = input_state - self.expected_output_state = expected_output_state + if not create_new_n_bit_values_container_instances: + self.input_state = input_state + self.expected_output_state = expected_output_state + else: + self.input_state = syrec.n_bit_values_container(input_state.size()) + for qubit in range(input_state.size()): + self.input_state.set(qubit, input_state.test(qubit)) + if expected_output_state is not None: + self.expected_output_state = syrec.n_bit_values_container(expected_output_state.size()) + for qubit in range(expected_output_state.size()): + self.expected_output_state.set(qubit, expected_output_state.test(qubit)) def initialize_expected_output_state_as_copy_of_input_state(self) -> None: if self.expected_output_state is not None: From e3bc0b649d6fd96da669b9858b6876c29fd62c3b Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 18 Jan 2026 14:01:40 +0100 Subject: [PATCH 29/88] Basic setup of ijson library to allow for import of simulation runs from .json file --- pyproject.toml | 1 + .../quantum_circuit_simulation_dialog.py | 122 ++++++++- .../all_input_states_generator_worker.py | 5 +- .../qt_all_input_states_generator_dialog.py | 19 +- .../qt_simulation_run_dialog.py | 25 +- .../qt_simulation_run_json_import_dialog.py | 250 ++++++++++++++++++ .../simulation_run_json_import_worker.py | 203 ++++++++++++++ uv.lock | 93 +++++++ 8 files changed, 686 insertions(+), 32 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py create mode 100644 python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py diff --git a/pyproject.toml b/pyproject.toml index 438a70f7..de44bfef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ requires-python = ">=3.10" dependencies = [ "mqt.core~=3.4.0", "PyQt6>=6.8", + "ijson~=3.4.0" ] dynamic = ["version"] diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 813cc2dc..8b8abfe2 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -9,6 +9,7 @@ from __future__ import annotations import sys +from pathlib import Path from typing import Final from PyQt6 import QtCore, QtGui, QtWidgets @@ -18,6 +19,7 @@ from .simulation_view.qt_all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog +from .simulation_view.qt_simulation_run_json_import_dialog import SimulationRunJsonImportDialog from .simulation_view.qt_simulation_run_model import ( SIMULATION_RUN_IO_STATE_QT_ROLE, QtSimulationRunModel, @@ -28,6 +30,7 @@ ) LOADED_FROM_FILE_INPUT_FIELD_NAME: Final[str] = "load_from_file_input_field" +IMPORT_FROM_FILE_BUTTON_NAME: Final[str] = "import_from_file_btn" ADD_SIM_RUN_BTN_NAME: Final[str] = "add_sim_run_btn" EDIT_SIM_RUN_BTN_NAME: Final[str] = "edit_sim_run_btn" DELETE_SIM_RUN_BTN_NAME: Final[str] = "delete_sim_run_btn" @@ -36,6 +39,8 @@ RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME: Final[str] = "run_sims_stop_first_failure_btn" SIMULATION_RUNS_LIST_VIEW_NAME: Final[str] = "sim_runs_list_view" +IMPORT_FROM_FILE_NO_FILE_SELECTED_PLACEHOLDER_TEXT: Final[str] = "" + # TODO: Should a confirmation be requested when the dialog is closed and simulation runs exist? class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] @@ -60,6 +65,7 @@ def __init__( self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None self.all_input_states_generator_dialog: AllInputStatesGeneratorDialog | None = None + self.simulation_run_import_from_file_dialog: SimulationRunJsonImportDialog | None = None self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self.simulation_run_dialog: SimulationRunDialog | None = None @@ -111,7 +117,7 @@ def initialize_simulation_runs_tab_widget( manual_y_space_size: Final[int] = 35 if create_load_from_file_controls: tab_wrapper_widget_layout.addLayout( - QuantumCircuitSimulationDialog.initialize_load_simulation_runs_from_file_controls() + QuantumCircuitSimulationDialog.initialize_load_simulation_runs_from_file_controls(self) ) tab_wrapper_widget_layout.addSpacing(manual_y_space_size) @@ -141,7 +147,7 @@ def initialize_simulation_runs_tab_widget( add_simulation_run_button = QtWidgets.QPushButton( QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.ListAdd), "Add simulation run", objectName=ADD_SIM_RUN_BTN_NAME ) - add_simulation_run_button.setEnabled(True) + add_simulation_run_button.setEnabled(not create_load_from_file_controls) add_simulation_run_button.clicked.connect(self.handle_simulation_run_add_btn_click) simulation_runs_list_modification_buttons_layout.addWidget(add_simulation_run_button) @@ -371,24 +377,30 @@ def handle_simulation_run_delete_btn_click(self) -> None: curr_active_tab_widget, False ) - @staticmethod - def initialize_load_simulation_runs_from_file_controls() -> QtWidgets.QLayout: + def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayout: controls_layout = QtWidgets.QHBoxLayout() controls_layout.addStretch() info_label = QtWidgets.QLabel("File to load simulation runs from:") controls_layout.addWidget(info_label) - selected_file_name_input_field = QtWidgets.QLineEdit(objectName=LOADED_FROM_FILE_INPUT_FIELD_NAME) - selected_file_name_input_field.setEnabled(False) - controls_layout.addWidget(selected_file_name_input_field) + selected_file_name_label = QtWidgets.QLabel( + IMPORT_FROM_FILE_NO_FILE_SELECTED_PLACEHOLDER_TEXT, objectName=LOADED_FROM_FILE_INPUT_FIELD_NAME + ) + selected_file_name_label.setEnabled(False) + controls_layout.addWidget(selected_file_name_label) open_file_dialog_button = QtWidgets.QPushButton("Select file...") + open_file_dialog_button.clicked.connect(self.open_import_file_selector) controls_layout.addWidget(open_file_dialog_button) trigger_load_from_file_button = QtWidgets.QPushButton( - QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.DocumentOpen), "Load from file" + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.DocumentOpen), + "Load from file", + objectName=IMPORT_FROM_FILE_BUTTON_NAME, ) + trigger_load_from_file_button.clicked.connect(self.open_import_from_file_dialog) + trigger_load_from_file_button.setEnabled(False) controls_layout.addWidget(trigger_load_from_file_button) controls_layout.addStretch() @@ -524,6 +536,96 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma def handle_simulation_runs_dialog_close(self, _: int) -> None: self.simulation_run_dialog = None + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def open_import_file_selector(self) -> None: + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Select a file to import simulation runs from", str(Path.home()), "Json files (*.json)" + ) + + active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + selected_filename_lbl: QtWidgets.QWidget | None = ( + active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) + if active_tab_widget is not None + else None + ) + load_from_file_btn: QtWidgets.QWidget | None = ( + active_tab_widget.findChild(QtWidgets.QPushButton, IMPORT_FROM_FILE_BUTTON_NAME) + if active_tab_widget is not None + else None + ) + if active_tab_widget is None or selected_filename_lbl is None or load_from_file_btn is None: + return + + if not filename: + return + + selected_filename_lbl.setText(filename) + load_from_file_btn.setEnabled(True) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def open_import_from_file_dialog(self) -> None: + if self.simulation_run_import_from_file_dialog is not None: + return + + active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + + selected_filename_lbl: QtWidgets.QWidget | None = ( + active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) + if active_tab_widget is not None + else None + ) + if active_tab_widget is None or selected_filename_lbl is None: + return + + if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: + pressed_btn_in_confirm_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( + self, + "Existing simulation runs detected", + "Importing from a file will delete any existing simulation runs. Do you want to continue?", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + if pressed_btn_in_confirm_dialog == QtWidgets.QMessageBox.StandardButton.Cancel: + return + self.simulation_runs_model.delete_all_simulation_run_models() + + self.simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog(self) + self.simulation_run_import_from_file_dialog.finished.connect(self.handle_import_from_file_dialog_close) + self.simulation_run_import_from_file_dialog.show() + self.simulation_run_import_from_file_dialog.start_generation( + Path(selected_filename_lbl.text()), + self.simulation_runs_model, + expected_input_state_size=self.annotatable_quantum_computation.num_data_qubits, + ) + + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] + def handle_import_from_file_dialog_close(self, result: int) -> None: + self.simulation_run_import_from_file_dialog = None + curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if curr_active_tab_widget is None: + # TODO: Error logging + return + + QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted + ) + + load_from_file_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, IMPORT_FROM_FILE_BUTTON_NAME + ) + add_sim_run_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME + ) + if load_from_file_btn is None or add_sim_run_btn is None: + # TODO: Error logging + return + + add_sim_run_btn.setEnabled(result == QtWidgets.QDialog.DialogCode.Accepted) + @staticmethod def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( tab_widget: QtWidgets.QWidget, should_controls_be_enabled: bool diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 4e8e3b2e..04fd5297 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -21,7 +21,6 @@ class AllInputStatesGeneratorWorker(QtCore.QObject): # type: ignore[misc] batch_generated = QtCore.pyqtSignal(tuple, name="batchGenerated") generation_failed = QtCore.pyqtSignal(Exception, name="generationFailed") - generation_cancelled = QtCore.pyqtSignal(name="generationCancelled") generation_finished = QtCore.pyqtSignal(name="generationFinished") def __init__(self, expected_input_state_size: int, batch_size: int): diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 84e9e666..4a4383b8 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -25,7 +25,6 @@ class AllInputStatesGeneratorDialog(QtWidgets.QDialog): # type: ignore[misc] - # input_state_batch_ack = QtCore.pyqtSignal(name="inputStateBatchAck") input_state_batch_ack = QtCore.pyqtSignal(name="inputStateBatchAck") request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") @@ -60,6 +59,7 @@ def __init__(self, parent: QtWidgets.QWidget): self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.progress_text_lbl = QtWidgets.QLabel("") + self.progress_text_lbl.setStyleSheet("QLabel { color : gray; }") self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.dialog_button_box = QtWidgets.QDialogButtonBox( @@ -156,6 +156,7 @@ def _handle_input_state_generator_failure(self, err: Exception) -> None: defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, ) self._request_worker_cancellation() + self._await_worker_thread_completion() @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] def _handle_generated_input_state_batch(self, batch_data: tuple[float, list[SimulationRunModel]]) -> None: @@ -196,11 +197,7 @@ def _handle_input_state_generator_finished(self) -> None: self.progress_text_lbl.setText("Input state generator finished!") self.progress_bar.setVisible(False) self._request_worker_cancellation() - - if self.worker_thread is not None: - self.worker_thread.quit() - self.worker_thread.wait() - self.progress_text_lbl.setText("Input state generator thread finished!") + self._await_worker_thread_completion() self._change_dialog_ok_button_enable_state(True) self._change_dialog_cancellation_button_enable_state(False) @@ -229,6 +226,12 @@ def _handle_input_state_generation_cancel_button_click(self) -> bool: return True return False + def _await_worker_thread_completion(self) -> None: + if self.worker_thread is not None: + self.worker_thread.quit() + self.worker_thread.wait() + self.progress_text_lbl.setText("Input state generator thread finished!") + def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( QtWidgets.QDialogButtonBox.StandardButton.Cancel diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index 7aef26a3..d64638db 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -93,6 +93,8 @@ def __init__(self, shared_simulation_run_model: QtSimulationRunModel, parent: Qt self.simulation_run_progress_bar.setFormat("Executing simulation run %v of %m") self.simulation_run_progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.simulation_run_progress_lbl = QtWidgets.QLabel("") + self.simulation_run_progress_lbl.setStyleSheet("QLabel { color : gray; }") + self.simulation_run_err_lbl = QtWidgets.QLabel("") self.simulation_run_err_lbl.setStyleSheet("QLabel { color : red; }") @@ -119,8 +121,6 @@ def start_simulations( ) -> None: self.num_completed_simulation_runs = 0 self.expected_total_num_simulation_runs = expected_total_num_simulation_runs - self.simulation_run_progress_lbl.setText("") - self.simulation_run_progress_lbl.setText("") self.simulation_run_progress_bar.setMinimum(0) self.simulation_run_progress_bar.setMaximum(expected_total_num_simulation_runs - 1) @@ -165,13 +165,8 @@ def start_simulations( # TODO: Not all simulation runs are executed? (2 out of 10) but no error is printed to the console or shown in the GUI. @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_all_simulation_runs_done(self) -> None: - # self.simulation_run_total_runtime_timer.stop() - - if self.worker_thread is not None: - self.worker_thread.quit() - self.worker_thread.wait() - self._change_dialog_cancellation_button_enable_state(False) - self.simulation_run_progress_bar.setVisible(False) + self._request_worker_cancellation() + self._await_worker_thread_completion() if self.num_completed_simulation_runs == self.expected_total_num_simulation_runs: self.simulation_run_progress_lbl.setText( @@ -188,6 +183,7 @@ def _handle_simulation_runs_stopped_due_to_err(self, simulation_run_num_that_fai f"Unexpected {err=}, {type(err)=} during execution of simulation run {simulation_run_num_that_failed}" ) self._request_worker_cancellation() + self._await_worker_thread_completion() @QtCore.pyqtSlot(ToBeExecutedSimulationRun) # type: ignore[untyped-decorator] def _handle_simulation_runs_stopped_after_first_failure( @@ -195,6 +191,7 @@ def _handle_simulation_runs_stopped_after_first_failure( ) -> None: self._update_progress_controls(simulation_run_causing_err.simulation_run_number) self._request_worker_cancellation() + self._await_worker_thread_completion() def _request_worker_cancellation(self) -> None: if self.worker is not None: @@ -268,6 +265,12 @@ def _enqueue_next_simulation_run(self, simulation_run_number: int) -> None: ) ) + def _await_worker_thread_completion(self) -> None: + if self.worker_thread is not None: + self.worker_thread.quit() + self.worker_thread.wait() + self.simulation_run_progress_lbl.setText("Simulation run thread finished!") + def _reset_workers(self) -> None: self.worker_thread = None self.worker = None diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py new file mode 100644 index 00000000..2ae9bc21 --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -0,0 +1,250 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore, QtWidgets + +if TYPE_CHECKING: + from pathlib import Path + + from PyQt6 import QtGui + + from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel + +from .simulation_run_json_import_worker import SimulationRunJsonImportWorker + +AGGREGATE_IMPORT_DATA_TEXT_FORMAT: Final[str] = ( + "Imported simulation runs: {num_imported_simulation_runs:d} | Total runtime for simulation run import [in seconds]: {total_runtime_in_sec:f}" +) +IMPORT_ORIGIN_INFO_TEXT_FORMAT: Final[str] = "Importing simulation runs from file {path_to_json_file:s}" + + +class SimulationRunJsonImportDialog(QtWidgets.QDialog): # type: ignore[misc] + imported_sim_run_batch_ack = QtCore.pyqtSignal(name="importedSimRunBatchAck") + request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") + + def __init__(self, parent: QtWidgets.QWidget): + super().__init__(parent) + + self.shared_simulation_runs_model: QtSimulationRunModel | None = None + self.worker_thread: QtCore.QThread | None = None + self.worker: SimulationRunJsonImportWorker | None = None + + self.num_imported_simulation_runs: int = 0 + self.stop_processing_imported_sim_run_batches: bool = False + self.total_simulation_run_import_runtime_in_seconds: float = 0 + + self.setModal(True) + self.setSizeGripEnabled(True) + self.setWindowTitle("Importing simulation runs...") + left = 0 + top = 0 + width = 400 + height = 200 + self.setGeometry(left, top, width, height) + + main_layout = QtWidgets.QVBoxLayout() + self.import_origin_info_lbl = QtWidgets.QLabel("") + self.import_origin_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.progress_text_lbl = QtWidgets.QLabel("") + self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.progress_text_lbl.setStyleSheet("QLabel { color : gray; }") + + self.err_text_lbl = QtWidgets.QLabel("") + self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.err_text_lbl.setStyleSheet("QLabel { color : red; }") + + main_layout.addWidget(self.import_origin_info_lbl) + main_layout.addWidget(self.progress_text_lbl) + main_layout.addWidget(self.err_text_lbl) + main_layout.addStretch() + + self.total_runtime_text_lbl = QtWidgets.QLabel("") + self.total_runtime_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + main_layout.addWidget(self.total_runtime_text_lbl) + + self.dialog_button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + self.dialog_button_box.setCenterButtons(True) + self.dialog_button_box.rejected.connect(self._handle_import_from_file_cancel_button_click) + self.dialog_button_box.accepted.connect(self.accept) + + self._change_dialog_ok_button_enable_state(False) + self._change_dialog_cancellation_button_enable_state(False) + + main_layout.addWidget(self.dialog_button_box) + self.setLayout(main_layout) + + def start_generation( + self, + path_to_json_file: Path, + shared_simulation_runs_model: QtSimulationRunModel, + expected_input_state_size: int, + batch_size: int = 1000, + ) -> None: + self.shared_simulation_runs_model = shared_simulation_runs_model + self.import_origin_info_lbl.setText( + IMPORT_ORIGIN_INFO_TEXT_FORMAT.format(path_to_json_file=str(path_to_json_file)) + ) + try: + self.worker = SimulationRunJsonImportWorker(path_to_json_file, expected_input_state_size, batch_size) + except ValueError as err: + self.err_text_lbl.setText( + f"Error {err=}, {type(err)=} during initialization of simulation run json importer!" + ) + return + + self.importedSimRunBatchAck.connect(self.worker.ack_batch_processed, QtCore.Qt.ConnectionType.QueuedConnection) + self.requestWorkerCancellation.connect( + self.worker.request_cancellation, QtCore.Qt.ConnectionType.QueuedConnection + ) + + self.worker_thread = QtCore.QThread() + self.worker.moveToThread(self.worker_thread) + self.worker_thread.started.connect(self.worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) + + self.worker.batchImported.connect( + self._handle_imported_sim_run_batch, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker.importFinished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.importFailed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) + + self.worker_thread.finished.connect(self.worker_thread.deleteLater) + self.worker_thread.finished.connect(self._reset_workers) + self.worker_thread.start(QtCore.QThread.Priority.LowPriority) + self._change_dialog_cancellation_button_enable_state(True) + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + # Ask for confirmation before closing + if self.worker is None or self._handle_import_from_file_cancel_button_click(): + if not self.err_text_lbl.text(): + self.accept() + else: + self.reject() + else: + event.ignore() + + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_importer_failure(self, err: Exception) -> None: + self.progress_text_lbl.setText("") + self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during import of simulation runs") + if self.shared_simulation_runs_model is not None: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + else: + QtWidgets.QMessageBox.critical( + self, + "Internal state error!", + "Shared simulation runs model was not initialized during handling of importer failure!\nThis should not happen.", + buttons=QtWidgets.QMessageBox.StandardButton.Ok, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + self._request_worker_cancellation() + self._await_worker_thread_completion() + self._change_dialog_cancellation_button_enable_state(False) + + @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] + def _handle_imported_sim_run_batch(self, batch_data: tuple[float, list[SimulationRunModel]]) -> None: + if self.stop_processing_imported_sim_run_batches: + return + + batch_generation_duration_in_seconds: float = batch_data[0] + self.progress_text_lbl.setText( + f"Finished generation of simulation runs batch from data of file (runtime [in sec]: {batch_generation_duration_in_seconds}!" + ) + generated_simulation_run_models: list[SimulationRunModel] = batch_data[1] + + if self.shared_simulation_runs_model is None: + QtWidgets.QMessageBox.critical( + self, + "Internal state error!", + "Shared simulation runs model was not initialized during handling of generated input state batch!\nThis should not happen.", + buttons=QtWidgets.QMessageBox.StandardButton.Ok, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + self._request_worker_cancellation() + self._await_worker_thread_completion() + return + + # TODO: Error handling + # TODO: Use delayed processing to reduce "laggy"/almost frozen GUI + self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) + self.imported_sim_run_batch_ack.emit() + + self.total_simulation_run_import_runtime_in_seconds += batch_generation_duration_in_seconds + self.num_imported_simulation_runs += len(generated_simulation_run_models) + self.total_runtime_text_lbl.setText( + AGGREGATE_IMPORT_DATA_TEXT_FORMAT.format( + num_imported_simulation_runs=self.num_imported_simulation_runs, + total_runtime_in_sec=self.total_simulation_run_import_runtime_in_seconds, + ) + ) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_import_completion(self) -> None: + self._request_worker_cancellation() + self._await_worker_thread_completion() + self._change_dialog_cancellation_button_enable_state(False) + self._change_dialog_ok_button_enable_state(True) + + def _request_worker_cancellation(self) -> None: + self.stop_processing_imported_sim_run_batches = True + self.progress_text_lbl.setText("Requesting cancellation of simulation run importer!") + if self.worker is not None: + self.requestWorkerCancellation.emit() + + def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + + if dialog_cancel_button is None: + return + + dialog_cancel_button.setEnabled(should_button_be_enabled) + + def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_ok_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ) + + if dialog_ok_button is None: + return + + dialog_ok_button.setEnabled(should_button_be_enabled) + + def _reset_workers(self) -> None: + self.worker_thread = None + self.worker = None + + def _await_worker_thread_completion(self) -> None: + if self.worker_thread is not None: + self.worker_thread.quit() + self.worker_thread.wait() + self.progress_text_lbl.setText("Simulation run importer thread finished!") + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_import_from_file_cancel_button_click(self) -> bool: + clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( + self, + "Cancellation of import from json file!", + "Are you sure that you want to stop the import of simulation runs from the file? This will cause the deletion of all already generated simulation runs.", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: + self._request_worker_cancellation() + if self.shared_simulation_runs_model is not None: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + return True + return False diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py new file mode 100644 index 00000000..015e05be --- /dev/null +++ b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py @@ -0,0 +1,203 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING, Any, Final + +# TODO: Correctly configure third-party package for mypy +# The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) +import ijson.backends.yajl2_c as ijson # type: ignore[import-not-found] +from PyQt6 import QtCore + +from mqt import syrec + +from .qt_simulation_run_model import SimulationRunModel + +if TYPE_CHECKING: + from pathlib import Path + +INPUT_STATE_JSON_KEY: Final[str] = "in" +EXPECTED_OUTPUT_STATE_JSON_KEY: Final[str] = "out" + + +class SimulationRunJsonImportWorker(QtCore.QObject): # type: ignore[misc] + batch_imported = QtCore.pyqtSignal(tuple, name="batchImported") + import_finished = QtCore.pyqtSignal(name="importFinished") + import_cancelled = QtCore.pyqtSignal(name="importCancelled") + import_failed = QtCore.pyqtSignal(Exception, name="importFailed") + + def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batch_size: int): + super().__init__() + + if expected_input_state_size < 0: + msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" + raise ValueError(msg) + + if batch_size < 1: + msg = f"Batch size must be larger than 0 but was actually {batch_size}" + raise ValueError(msg) + + self.path_to_json_file: Path = path_to_json_file + self.expected_input_state_size: Final[int] = expected_input_state_size + self.batch_size: Final[int] = batch_size + self.cancellation_requested = False + self.cancellation_flag_mutex = QtCore.QReadWriteLock() + self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_import(self) -> None: + batch_generation_end_time: float = 0 + batch_generation_duration_in_seconds: float = 0 + + batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] + batch_idx: int = 0 + try: + # Reading bytes instead of strings leads to better parser performance + with self.path_to_json_file.open("rb") as file: + batch_generation_start_time: float = time.perf_counter() + # The json parser starts at the first element matching the prefix which in our case starts at an element with key 'simulationRuns' that is expected + # to be a property of the top level element (i.e. the path to the 'simulationRuns' element is relative to the top level element). + # Additionally, with the postfix '.item', only the entries of a JSON array are processed. If the 'simulationRuns' entry value is no + # array then no objects will be parsed. + parser = ijson.items(file, prefix="simulationRuns.item") + for arr_elem in parser: + if self._thread_safe_check_whether_cancellation_is_requested(): + break + + # the ison.items(...) function converts JSON objects to python dictionaries (https://pypi.org/project/ijson/#options). However, we + # need to discard any other type of JSON elements (integers, strings, array, etc.) by checking whether we are actually processing a + # python dictionary. + if not isinstance(arr_elem, dict): + continue + + batch_data[batch_idx] = SimulationRunJsonImportWorker._try_deserialize_simulation_run( + self.expected_input_state_size, arr_elem + ) + batch_idx += 1 + if batch_idx == self.batch_size: + batch_generation_end_time = time.perf_counter() + batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time + batch_generation_start_time = batch_generation_end_time + + self.batch_imported.emit((batch_generation_duration_in_seconds, batch_data)) + try: + self.cancellation_flag_mutex.lockForRead() + # Lock needs to be already held for wait condition to not return immediately + self.wait_on_batch_processed_acknowledgement_condition.wait(self.cancellation_flag_mutex) + finally: + self.cancellation_flag_mutex.unlock() + # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using + # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. + time.sleep(0.1) + + for i in range(self.batch_size): + batch_data[i] = None + batch_idx = 0 + + if batch_idx != 0: + del batch_data[batch_idx:] + batch_generation_end_time = time.perf_counter() + batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time + batch_generation_start_time = batch_generation_end_time + self.batch_imported.emit((batch_generation_duration_in_seconds, batch_data)) + self.import_finished.emit() + except Exception as err: + self.import_failed.emit(err) + + # def write_streaming(self, source: Iterable[Any], chunk_size: int = 1_000): + # """ + # Write large JSON array without blocking. + # source can be a generator (lazy). + # """ + # try: + # total = None + # if hasattr(source, "__len__"): + # total = len(source) + # with self.file_path.open("w", encoding="utf-8") as f: + # f.write("[\n") + # for idx, item in enumerate(source): + # if self.should_stop(): + # self.finished.emit(False) + # return + # if idx: + # f.write(",\n") + # json.dump(item, f, ensure_ascii=False) + # if total: + # self.progress.emit(int(100 * (idx + 1) / total)) + # else: + # self.progress.emit(-1) # indeterminate + # f.write("\n]") + # self.progress.emit(100) + # self.finished.emit(True) + # except Exception as exc: + # self.error.emit(str(exc)) + + # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. + + def request_cancellation(self) -> None: + self._thread_safe_set_cancellation_requested_flag(True) + self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + + def ack_batch_processed(self) -> None: + self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + + def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: + cancellation_requested: bool = False + self.cancellation_flag_mutex.lockForRead() + cancellation_requested = self.cancellation_requested + self.cancellation_flag_mutex.unlock() + return cancellation_requested + + def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None: + self.cancellation_flag_mutex.lockForWrite() + self.cancellation_requested = flag_value + self.cancellation_flag_mutex.unlock() + + @staticmethod + def _try_deserialize_simulation_run( + expected_state_size: int, parsed_json_elem_values_dict: dict[str, Any] + ) -> SimulationRunModel: + if INPUT_STATE_JSON_KEY not in parsed_json_elem_values_dict: + msg = f"Values of input state (expected json key '{INPUT_STATE_JSON_KEY}') was not defined in json object!" + raise ValueError(msg) + + stringified_input_state: Final[str] = parsed_json_elem_values_dict[INPUT_STATE_JSON_KEY] + if len(stringified_input_state) != expected_state_size: + msg = f"Parsed input state size (n={len(stringified_input_state)}) did not match expected input state size (n={expected_state_size})!" + raise ValueError(msg) + + if any(qubit_value not in {"0", "1"} for qubit_value in stringified_input_state): + msg = f"Qubit values of input state must be defined as an enumeration of '0' and '1' literals combined without any delimiter (i.e. a 4 qubit state must be defined as '0101') but was actually {stringified_input_state}" + raise ValueError(msg) + + stringified_expected_output_state: Final[str | None] = parsed_json_elem_values_dict.get( + EXPECTED_OUTPUT_STATE_JSON_KEY + ) + + if stringified_expected_output_state is not None: + if len(stringified_expected_output_state) != expected_state_size: + msg = f"Parsed expected output state size (n={len(stringified_expected_output_state)}) did not match expected input state size (n={expected_state_size})!" + raise ValueError(msg) + if any(qubit_value not in {"0", "1"} for qubit_value in stringified_expected_output_state): + msg = f"Qubit values of expected output state must be defined as an enumeration of '0' and '1' literals combined without any delimiter (i.e. a 4 qubit state must be defined as '0101') but was actually {stringified_expected_output_state}" + raise ValueError(msg) + + input_state: syrec.n_bit_values_container = syrec.n_bit_values_container(expected_state_size) + expected_output_state: syrec.n_bit_values_container | None = ( + syrec.n_bit_values_container(expected_state_size) if stringified_expected_output_state is not None else None + ) + for i in range(expected_state_size): + input_state.set(i, stringified_input_state[i] != "0") + + if expected_output_state is not None: + for i in range(expected_state_size): + expected_output_state.set(i, stringified_expected_output_state[i] != "0") # type: ignore[index] + + return SimulationRunModel(input_state, expected_output_state) diff --git a/uv.lock b/uv.lock index b01ca64e..72bb5dc9 100644 --- a/uv.lock +++ b/uv.lock @@ -663,6 +663,97 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "ijson" +version = "3.4.0.post0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/30/7ab4b9e88e7946f6beef419f74edcc541df3ea562c7882257b4eaa82417d/ijson-3.4.0.post0.tar.gz", hash = "sha256:9aa02dc70bb245670a6ca7fba737b992aeeb4895360980622f7e568dbf23e41e", size = 67216, upload-time = "2025-10-10T05:29:25.62Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/15/4f4921ed9ab94032fd0b03ecb211ff9dbd5cc9953463f5b5c4ddeab406fc/ijson-3.4.0.post0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f904a405b58a04b6ef0425f1babbc5c65feb66b0a4cc7f214d4ad7de106f77d", size = 88244, upload-time = "2025-10-10T05:27:42.001Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/b85d4da1752362a789bc3e0fc4b55e812a374a50d2fe1c06cab2e2bcb170/ijson-3.4.0.post0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a07dcc1a8a1ddd76131a7c7528cbd12951c2e34eb3c3d63697b905069a2d65b1", size = 59880, upload-time = "2025-10-10T05:27:44.791Z" }, + { url = "https://files.pythonhosted.org/packages/c3/96/e1027e6d0efb5b9192bdc9f0af5633c20a56999cce4cf7ad35427f823138/ijson-3.4.0.post0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab3be841b8c430c1883b8c0775eb551f21b5500c102c7ee828afa35ddd701bdd", size = 59939, upload-time = "2025-10-10T05:27:45.66Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/b9ca0a19afb2f36be35c6afa2c4d1c19950dc45f6a50b483b56082b3e165/ijson-3.4.0.post0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:43059ae0d657b11c5ddb11d149bc400c44f9e514fb8663057e9b2ea4d8d44c1f", size = 125894, upload-time = "2025-10-10T05:27:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/02/1b/f7356de078d85564829c5e2a2a31473ee0ad1876258ceecf550b582e57b7/ijson-3.4.0.post0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d3e82963096579d1385c06b2559570d7191e225664b7fa049617da838e1a4a4", size = 132385, upload-time = "2025-10-10T05:27:48Z" }, + { url = "https://files.pythonhosted.org/packages/57/7b/08f86eed5df0849b673260dd2943b6a7367a55b5a4b6e73ddbfbdf4206f1/ijson-3.4.0.post0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:461ce4e87a21a261b60c0a68a2ad17c7dd214f0b90a0bec7e559a66b6ae3bd7e", size = 129567, upload-time = "2025-10-10T05:27:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/69672d95b1a16e7c6bf89cef6c892b228cc84b484945a731786a425700d2/ijson-3.4.0.post0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:890cf6610c9554efcb9765a93e368efeb5bb6135f59ce0828d92eaefff07fde5", size = 132821, upload-time = "2025-10-10T05:27:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/9ed4868e2e92db2454508f7ea1282bec0b039bd344ac0cbac4a2de16786d/ijson-3.4.0.post0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6793c29a5728e7751a7df01be58ba7da9b9690c12bf79d32094c70a908fa02b9", size = 127757, upload-time = "2025-10-10T05:27:51.203Z" }, + { url = "https://files.pythonhosted.org/packages/5b/aa/08a308d3aaa6e98511f3100f8a1e4e8ff8c853fa4ec3f18b71094ac36bbe/ijson-3.4.0.post0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a56b6674d7feec0401c91f86c376f4e3d8ff8129128a8ad21ca43ec0b1242f79", size = 130439, upload-time = "2025-10-10T05:27:52.123Z" }, + { url = "https://files.pythonhosted.org/packages/56/46/3da05a044f335b97635d59eede016ea158fbf1b59e584149177b6524e1e5/ijson-3.4.0.post0-cp310-cp310-win32.whl", hash = "sha256:01767fcbd75a5fa5a626069787b41f04681216b798510d5f63bcf66884386368", size = 52004, upload-time = "2025-10-10T05:27:53.441Z" }, + { url = "https://files.pythonhosted.org/packages/60/d7/a126d58f379df16fa9a0c2532ac00ae3debf1d28c090020775bc735032b8/ijson-3.4.0.post0-cp310-cp310-win_amd64.whl", hash = "sha256:09127c06e5dec753feb9e4b8c5f6a23603d1cd672d098159a17e53a73b898eec", size = 54407, upload-time = "2025-10-10T05:27:54.259Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ac/3d57249d4acba66a33eaef794edb5b2a2222ca449ae08800f8abe9286645/ijson-3.4.0.post0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b473112e72c0c506da425da3278367b6680f340ecc093084693a1e819d28435", size = 88278, upload-time = "2025-10-10T05:27:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/2d068d23d1a665f500282ceb6f2473952a95fc7107d739fd629b4ab41959/ijson-3.4.0.post0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:043f9b7cf9cc744263a78175e769947733710d2412d25180df44b1086b23ebd5", size = 59898, upload-time = "2025-10-10T05:27:56.361Z" }, + { url = "https://files.pythonhosted.org/packages/26/3d/8b14589dfb0e5dbb7bcf9063e53d3617c041cf315ff3dfa60945382237ce/ijson-3.4.0.post0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b55e49045f4c8031f3673f56662fd828dc9e8d65bd3b03a9420dda0d370e64ba", size = 59945, upload-time = "2025-10-10T05:27:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/086a75094397d4b7584698a540a279689e12905271af78cdfc903bf9eaf8/ijson-3.4.0.post0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11f13b73194ea2a5a8b4a2863f25b0b4624311f10db3a75747b510c4958179b0", size = 131318, upload-time = "2025-10-10T05:27:58.453Z" }, + { url = "https://files.pythonhosted.org/packages/df/35/7f61e9ce4a9ff1306ec581eb851f8a660439126d92ee595c6dc8084aac97/ijson-3.4.0.post0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:659acb2843433e080c271ecedf7d19c71adde1ee5274fc7faa2fec0a793f9f1c", size = 137990, upload-time = "2025-10-10T05:27:59.328Z" }, + { url = "https://files.pythonhosted.org/packages/59/bf/590bbc3c3566adce5e2f43ba5894520cbaf19a3e7f38c1250926ba67eee4/ijson-3.4.0.post0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deda4cfcaafa72ca3fa845350045b1d0fef9364ec9f413241bb46988afbe6ee6", size = 134416, upload-time = "2025-10-10T05:28:00.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/fb719049851979df71f3e039d6f1a565d349c9cb1b29c0f8775d9db141b4/ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47352563e8c594360bacee2e0753e97025f0861234722d02faace62b1b6d2b2a", size = 138034, upload-time = "2025-10-10T05:28:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/ccda891f572876aaf2c43f0b2079e31d5b476c3ae53196187eab1a788eff/ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5a48b9486242d1295abe7fd0fbb6308867da5ca3f69b55c77922a93c2b6847aa", size = 132510, upload-time = "2025-10-10T05:28:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/11/b5/ca8e64ab7cf5252f358e467be767630f085b5bbcd3c04333a3a5f36c3dd3/ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9c0886234d1fae15cf4581a430bdba03d79251c1ab3b07e30aa31b13ef28d01c", size = 134907, upload-time = "2025-10-10T05:28:04.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/14/63a4d5dc548690f29f0c2fc9cabd5ecbb37532547439c05f5b3b9ce73021/ijson-3.4.0.post0-cp311-cp311-win32.whl", hash = "sha256:fecae19b5187d92900c73debb3a979b0b3290a53f85df1f8f3c5ba7d1e9fb9cb", size = 52006, upload-time = "2025-10-10T05:28:05.424Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/932740899e572a97f9be0c6cd64ebda557eae7701ac216fc284aba21786d/ijson-3.4.0.post0-cp311-cp311-win_amd64.whl", hash = "sha256:b39dbf87071f23a23c8077eea2ae7cfeeca9ff9ffec722dfc8b5f352e4dd729c", size = 54410, upload-time = "2025-10-10T05:28:06.264Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/3b6af0025288e769dbfa30485dae1b3bd3f33f00390f3ee532cbb1c33e9b/ijson-3.4.0.post0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b607a500fca26101be47d2baf7cddb457b819ab60a75ce51ed1092a40da8b2f9", size = 87847, upload-time = "2025-10-10T05:28:07.229Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/95ee2ca82f3b1a57892452f6e5087607d56c620beb8ce625475194568698/ijson-3.4.0.post0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4827d9874a6a81625412c59f7ca979a84d01f7f6bfb3c6d4dc4c46d0382b14e0", size = 59815, upload-time = "2025-10-10T05:28:08.448Z" }, + { url = "https://files.pythonhosted.org/packages/51/8d/5a704ab3c17c55c21c86423458db8610626ca99cc9086a74dfeb7ee9054c/ijson-3.4.0.post0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4d4afec780881edb2a0d2dd40b1cdbe246e630022d5192f266172a0307986a7", size = 59648, upload-time = "2025-10-10T05:28:09.307Z" }, + { url = "https://files.pythonhosted.org/packages/25/56/ca5d6ca145d007f30b44e747f3c163bc08710ce004af0deaad4a2301339b/ijson-3.4.0.post0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432fb60ffb952926f9438e0539011e2dfcd108f8426ee826ccc6173308c3ff2c", size = 138279, upload-time = "2025-10-10T05:28:10.489Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d3/22e3cc806fcdda7ad4c8482ed74db7a017d4a1d49b4300c7bc07052fb561/ijson-3.4.0.post0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54a0e3e05d9a0c95ecba73d9579f146cf6d5c5874116c849dba2d39a5f30380e", size = 149110, upload-time = "2025-10-10T05:28:12.263Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/efb30f413648b9267f5a33920ac124d7ebef3bc4063af8f6ffc8ca11ddcb/ijson-3.4.0.post0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05807edc0bcbd222dc6ea32a2b897f0c81dc7f12c8580148bc82f6d7f5e7ec7b", size = 149026, upload-time = "2025-10-10T05:28:13.557Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/481165f7046ade32488719300a3994a437020bc41cfbb54334356348f513/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5269af16f715855d9864937f9dd5c348ca1ac49cee6a2c7a1b7091c159e874f", size = 150012, upload-time = "2025-10-10T05:28:14.859Z" }, + { url = "https://files.pythonhosted.org/packages/0f/24/642e3289917ecf860386e26dfde775f9962d26ab7f6c2e364ed3ca3c25d8/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b200df83c901f5bfa416d069ac71077aa1608f854a4c50df1b84ced560e9c9ec", size = 142193, upload-time = "2025-10-10T05:28:16.131Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f5/fd2f038abe95e553e1c3ee207cda19db9196eb416e63c7c89699a8cf0db7/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6458bd8e679cdff459a0a5e555b107c3bbacb1f382da3fe0f40e392871eb518d", size = 150904, upload-time = "2025-10-10T05:28:17.401Z" }, + { url = "https://files.pythonhosted.org/packages/49/35/24259d22519987928164e6cb8fe3486e1df0899b2999ada4b0498639b463/ijson-3.4.0.post0-cp312-cp312-win32.whl", hash = "sha256:55f7f656b5986326c978cbb3a9eea9e33f3ef6ecc4535b38f1d452c731da39ab", size = 52358, upload-time = "2025-10-10T05:28:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2b/6f7ade27a8ff5758fc41006dadd2de01730def84fe3e60553b329c59e0d4/ijson-3.4.0.post0-cp312-cp312-win_amd64.whl", hash = "sha256:e15833dcf6f6d188fdc624a31cd0520c3ba21b6855dc304bc7c1a8aeca02d4ac", size = 54789, upload-time = "2025-10-10T05:28:19.552Z" }, + { url = "https://files.pythonhosted.org/packages/1b/20/aaec6977f9d538bbadd760c7fa0f6a0937742abdcc920ec6478a8576e55f/ijson-3.4.0.post0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:114ed248166ac06377e87a245a158d6b98019d2bdd3bb93995718e0bd996154f", size = 87863, upload-time = "2025-10-10T05:28:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/5b/29/06bf56a866e2fe21453a1ad8f3a5d7bca3c723f73d96329656dfee969783/ijson-3.4.0.post0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffb21203736b08fe27cb30df6a4f802fafb9ef7646c5ff7ef79569b63ea76c57", size = 59806, upload-time = "2025-10-10T05:28:21.596Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/e1d0fda91ba7a444b75f0d60cb845fdb1f55d3111351529dcbf4b1c276fe/ijson-3.4.0.post0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:07f20ecd748602ac7f18c617637e53bd73ded7f3b22260bba3abe401a7fc284e", size = 59643, upload-time = "2025-10-10T05:28:22.45Z" }, + { url = "https://files.pythonhosted.org/packages/4d/24/5a24533be2726396cc1724dc237bada09b19715b5bfb0e7b9400db0901ad/ijson-3.4.0.post0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27aa193d47ffc6bc4e45453896ad98fb089a367e8283b973f1fe5c0198b60b4e", size = 138082, upload-time = "2025-10-10T05:28:23.319Z" }, + { url = "https://files.pythonhosted.org/packages/05/60/026c3efcec23c329657e878cbc0a9a25b42e7eb3971e8c2377cb3284e2b7/ijson-3.4.0.post0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccddb2894eb7af162ba43b9475ac5825d15d568832f82eb8783036e5d2aebd42", size = 149145, upload-time = "2025-10-10T05:28:24.279Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c2/036499909b7a1bc0bcd85305e4348ad171aeb9df57581287533bdb3497e9/ijson-3.4.0.post0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61ab0b8c5bf707201dc67e02c116f4b6545c4afd7feb2264b989d242d9c4348a", size = 149046, upload-time = "2025-10-10T05:28:25.186Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/e7736073ad96867c129f9e799e3e65086badd89dbf3911f76d9b3bf8a115/ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:254cfb8c124af68327a0e7a49b50bbdacafd87c4690a3d62c96eb01020a685ef", size = 150356, upload-time = "2025-10-10T05:28:26.135Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1b/1c1575d2cda136985561fcf774fe6c54412cd0fa08005342015af0403193/ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04ac9ca54db20f82aeda6379b5f4f6112fdb150d09ebce04affeab98a17b4ed3", size = 142322, upload-time = "2025-10-10T05:28:27.125Z" }, + { url = "https://files.pythonhosted.org/packages/28/4d/aba9871feb624df8494435d1a9ddc7b6a4f782c6044bfc0d770a4b59f145/ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a603d7474bf35e7b3a8e49c8dabfc4751841931301adff3f3318171c4e407f32", size = 151386, upload-time = "2025-10-10T05:28:28.274Z" }, + { url = "https://files.pythonhosted.org/packages/3f/9a/791baa83895fb6e492bce2c7a0ea6427b6a41fe854349e62a37d0c9deaf0/ijson-3.4.0.post0-cp313-cp313-win32.whl", hash = "sha256:ec5bb1520cb212ebead7dba048bb9b70552c3440584f83b01b0abc96862e2a09", size = 52352, upload-time = "2025-10-10T05:28:29.191Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0c/061f51493e1da21116d74ee8f6a6b9ae06ca5fa2eb53c3b38b64f9a9a5ae/ijson-3.4.0.post0-cp313-cp313-win_amd64.whl", hash = "sha256:3505dff18bdeb8b171eb28af6df34857e2be80dc01e2e3b624e77215ad58897f", size = 54783, upload-time = "2025-10-10T05:28:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/c7/89/4344e176f2c5f5ef3251c9bfa4ddd5b4cf3f9601fd6ec3f677a3ba0b9c71/ijson-3.4.0.post0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:45a0b1c833ed2620eaf8da958f06ac8351c59e5e470e078400d23814670ed708", size = 92342, upload-time = "2025-10-10T05:28:31.389Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b1/85012c586a6645f9fb8bfa3ef62ed2f303c8d73fc7c2f705111582925980/ijson-3.4.0.post0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7809ec8c8f40228edaaa089f33e811dff4c5b8509702652870d3f286c9682e27", size = 62028, upload-time = "2025-10-10T05:28:32.849Z" }, + { url = "https://files.pythonhosted.org/packages/65/ea/7b7e2815c101d78b33e74d64ddb70cccc377afccd5dda76e566ed3fcb56f/ijson-3.4.0.post0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cf4a34c2cfe852aee75c89c05b0a4531c49dc0be27eeed221afd6fbf9c3e149c", size = 61773, upload-time = "2025-10-10T05:28:34.016Z" }, + { url = "https://files.pythonhosted.org/packages/59/7d/2175e599cb77a64f528629bad3ce95dfdf2aa6171d313c1fc00bbfaf0d22/ijson-3.4.0.post0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a39d5d36067604b26b78de70b8951c90e9272450642661fe531a8f7a6936a7fa", size = 198562, upload-time = "2025-10-10T05:28:34.878Z" }, + { url = "https://files.pythonhosted.org/packages/13/97/82247c501c92405bb2fc44ab5efb497335bcb9cf0f5d3a0b04a800737bd8/ijson-3.4.0.post0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83fc738d81c9ea686b452996110b8a6678296c481e0546857db24785bff8da92", size = 216212, upload-time = "2025-10-10T05:28:36.208Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/b956f507bb02e05ce109fd11ab6a2c054f8b686cc5affe41afe50630984d/ijson-3.4.0.post0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2a81aee91633868f5b40280e2523f7c5392e920a5082f47c5e991e516b483f6", size = 206618, upload-time = "2025-10-10T05:28:37.243Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/e827840ab81d86a9882e499097934df53294f05155f1acfcb9a211ac1142/ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56169e298c5a2e7196aaa55da78ddc2415876a74fe6304f81b1eb0d3273346f7", size = 210689, upload-time = "2025-10-10T05:28:38.252Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3b/59238d9422c31a4aefa22ebeb8e599e706158a0ab03669ef623be77a499a/ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eeb9540f0b1a575cbb5968166706946458f98c16e7accc6f2fe71efa29864241", size = 199927, upload-time = "2025-10-10T05:28:39.233Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0f/ec01c36c128c37edb8a5ae8f3de3256009f886338d459210dfe121ee4ba9/ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ba3478ff0bb49d7ba88783f491a99b6e3fa929c930ab062d2bb7837e6a38fe88", size = 204455, upload-time = "2025-10-10T05:28:40.644Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cf/5560e1db96c6d10a5313be76bf5a1754266cbfb5cc13ff64d107829e07b1/ijson-3.4.0.post0-cp313-cp313t-win32.whl", hash = "sha256:b005ce84e82f28b00bf777a464833465dfe3efa43a0a26c77b5ac40723e1a728", size = 54566, upload-time = "2025-10-10T05:28:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/22/5a/cbb69144c3b25dd56f5421ff7dc0cf3051355579062024772518e4f4b3c5/ijson-3.4.0.post0-cp313-cp313t-win_amd64.whl", hash = "sha256:fe9c84c9b1c8798afa407be1cea1603401d99bfc7c34497e19f4f5e5ddc9b441", size = 57298, upload-time = "2025-10-10T05:28:42.881Z" }, + { url = "https://files.pythonhosted.org/packages/af/0b/a4ce8524fd850302bbf5d9f38d07c0fa981fdbe44951d2fcd036935b67dd/ijson-3.4.0.post0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da6a21b88cbf5ecbc53371283988d22c9643aa71ae2873bbeaefd2dea3b6160b", size = 88361, upload-time = "2025-10-10T05:28:43.73Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/a5e5f33e46f28174a9c8142d12dcb3d26ce358d9a2230b9b15f5c987b3a5/ijson-3.4.0.post0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cf24a48a1c3ca9d44a04feb59ccefeb9aa52bb49b9cb70ad30518c25cce74bb7", size = 59960, upload-time = "2025-10-10T05:28:44.585Z" }, + { url = "https://files.pythonhosted.org/packages/83/e2/551dd7037dda759aa0ce53f0d3d7be03b03c6b05c0b0a5d5ab7a47e6b4b1/ijson-3.4.0.post0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d14427d366f95f21adcb97d0ed1f6d30f6fdc04d0aa1e4de839152c50c2b8d65", size = 59957, upload-time = "2025-10-10T05:28:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b9/3006384f85cc26cf83dbbd542d362cc336f1e1ddd491e32147cfa46ea8ae/ijson-3.4.0.post0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339d49f6c5d24051c85d9226be96d2d56e633cb8b7d09dd8099de8d8b51a97e2", size = 139967, upload-time = "2025-10-10T05:28:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/77/3b/b5234add8115cbfe8635b6c152fb527327f45e4c0f0bf2e93844b36b5217/ijson-3.4.0.post0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7206afcb396aaef66c2b066997b4e9d9042c4b7d777f4d994e9cec6d322c2fe6", size = 149196, upload-time = "2025-10-10T05:28:48.226Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d2/c4ae543e37d7a9fba09740c221976a63705dbad23a9cda9022fc9fa0f3de/ijson-3.4.0.post0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8dd327da225887194fe8b93f2b3c9c256353e14a6b9eefc940ed17fde38f5b8", size = 148516, upload-time = "2025-10-10T05:28:49.237Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a1/914b5fb1c26af2474cd04841626e0e95576499a4ca940661fb105ee12dd2/ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4810546e66128af51fd4a0c9a640e84e8508e9c15c4f247d8a3e3253b20e1465", size = 149770, upload-time = "2025-10-10T05:28:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c1/51c3584102d0d85d4aa10cc88dbbe431ecb9fe98160a9e2fad62a4456aed/ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:103a0838061297d063bca81d724b0958b616f372bd893bbc278320152252c652", size = 143688, upload-time = "2025-10-10T05:28:51.823Z" }, + { url = "https://files.pythonhosted.org/packages/47/3d/a54f13d766332620bded8ee76bcdd274509ecc53cf99573450f95b3ad910/ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:40007c977e230e04118b27322f25a72ae342a3d61464b2057fcd9b21eeb7427a", size = 150688, upload-time = "2025-10-10T05:28:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/72/49/43d97cccf3266da7c044bd42e5083340ad1fd97fbb16d1bcd6791fd8918f/ijson-3.4.0.post0-cp314-cp314-win32.whl", hash = "sha256:f932969fc1fd4449ca141cf5f47ff357656a154a361f28d9ebca0badc5b02297", size = 52882, upload-time = "2025-10-10T05:28:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f0/008f1ed4e0fc6f6dc7a5a82ecf08a59bb212514e158954374d440d700e6c/ijson-3.4.0.post0-cp314-cp314-win_amd64.whl", hash = "sha256:3ed19b1e4349240773a8ce4a4bfa450892d4a57949c02c515cd6be5a46b7696a", size = 55568, upload-time = "2025-10-10T05:28:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/69/1c/8a199fded709e762aced89bb7086973c837e432dd714bbad78a6ac789c23/ijson-3.4.0.post0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:226447e40ca9340a39ed07d68ea02ee14b52cb4fe649425b256c1f0073531c83", size = 92345, upload-time = "2025-10-10T05:28:55.657Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/04e97f6a403203bd2eb8849570bdce5719d696b5fb96aa2a62566fe7a1d9/ijson-3.4.0.post0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c88f0669d45d4b1aa017c9b68d378e7cd15d188dfb6f0209adc78b7f45590a7", size = 62029, upload-time = "2025-10-10T05:28:56.561Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/e88295f9456ba939d90d4603af28fcabda3b443ef55e709e9381df3daa58/ijson-3.4.0.post0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:56b3089dc28c12492d92cc4896d2be585a89ecae34e25d08c1df88f21815cb50", size = 61776, upload-time = "2025-10-10T05:28:57.401Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9f/0e9c236e720c2de887ab0d7cad8a15d2aa55fb449f792437fc99899957a9/ijson-3.4.0.post0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c117321cfa7b749cc1213f9b4c80dc958f0a206df98ec038ae4bcbbdb8463a15", size = 199808, upload-time = "2025-10-10T05:28:58.62Z" }, + { url = "https://files.pythonhosted.org/packages/0e/70/c21de30e7013e074924cd82057acfc5760e7b2cc41180f80770621b0ad36/ijson-3.4.0.post0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8311f48db6a33116db5c81682f08b6e2405501a4b4e460193ae69fec3cd1f87a", size = 217152, upload-time = "2025-10-10T05:28:59.656Z" }, + { url = "https://files.pythonhosted.org/packages/64/78/63a0bcc0707037df4e22bb836451279d850592258c859685a402c27f5d6d/ijson-3.4.0.post0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91c61a3e63e04da648737e6b4abd537df1b46fb8cdf3219b072e790bb3c1a46b", size = 207663, upload-time = "2025-10-10T05:29:00.73Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/834e9838d69893cb7567e1210be044444213c78f7414aaf1cd241df16078/ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1709171023ce82651b2f132575c2e6282e47f64ad67bd3260da476418d0e7895", size = 211157, upload-time = "2025-10-10T05:29:01.87Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9b/9fda503799ebc30397710552e5dedc1d98d9ea6a694e5717415892623a94/ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5f0a72b1e3c0f78551670c12b2fdc1bf05f2796254d9c2055ba319bec2216020", size = 200231, upload-time = "2025-10-10T05:29:02.883Z" }, + { url = "https://files.pythonhosted.org/packages/15/f3/6419d1d5795a16591233d3aa3747b084e82c0c1d7184bdad9be638174560/ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b982a3597b0439ce9c8f4cfc929d86c6ed43907908be1e8463a34dc35fe5b258", size = 204825, upload-time = "2025-10-10T05:29:04.242Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8d/a520e6902129c55fa94428ea0a22e8547540d5e7ca30f18b39594a5feea2/ijson-3.4.0.post0-cp314-cp314t-win32.whl", hash = "sha256:4e39bfdc36b0b460ef15a06550a6a385c64c81f7ac205ccff39bd45147918912", size = 55559, upload-time = "2025-10-10T05:29:05.681Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/0ac6dd0045957ba1270b7b1860864f7d8cea4062e70b1083134c587e5768/ijson-3.4.0.post0-cp314-cp314t-win_amd64.whl", hash = "sha256:17e45262a5ddef39894013fb1548ee7094e444c8389eb1a97f86708b19bea03e", size = 58238, upload-time = "2025-10-10T05:29:06.656Z" }, + { url = "https://files.pythonhosted.org/packages/43/66/27cfcea16e85b95e33814eae2052dab187206b8820cdd90aa39d32ffb441/ijson-3.4.0.post0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:add9242f886eae844a7410b84aee2bbb8bdc83c624f227cb1fdb2d0476a96cb1", size = 57029, upload-time = "2025-10-10T05:29:19.733Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/df3f1561c6629241fb2f8bd7ea1da14e3c2dd16fe9d7cbc97120870ed09c/ijson-3.4.0.post0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:69718ed41710dfcaa7564b0af42abc05875d4f7aaa24627c808867ef32634bc7", size = 56523, upload-time = "2025-10-10T05:29:20.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/0a/6c6a3221ddecf62b696fde0e864415237e05b9a36ab6685a606b8fb3b5a2/ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:636b6eca96c6c43c04629c6b37fad0181662eaacf9877c71c698485637f752f9", size = 70546, upload-time = "2025-10-10T05:29:21.526Z" }, + { url = "https://files.pythonhosted.org/packages/42/cb/edf69755e86a3a9f8b418efd60239cb308af46c7c8e12f869423f51c9851/ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5e73028f6e63d27b3d286069fe350ed80a4ccc493b022b590fea4bb086710d", size = 70532, upload-time = "2025-10-10T05:29:22.718Z" }, + { url = "https://files.pythonhosted.org/packages/96/7e/c8730ea39b8712622cd5a1bdff676098208400e37bb92052ba52f93e2aa1/ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:461acf4320219459dabe5ed90a45cb86c9ba8cc6d6db9dad0d9427d42f57794c", size = 67927, upload-time = "2025-10-10T05:29:23.596Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f2/53b6e9bdd2a91202066764eaa74b572ba4dede0fe47a5a26f4de34b7541a/ijson-3.4.0.post0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a0fedf09c0f6ffa2a99e7e7fd9c5f3caf74e655c1ee015a0797383e99382ebc3", size = 54657, upload-time = "2025-10-10T05:29:24.482Z" }, +] + [[package]] name = "imagesize" version = "1.4.1" @@ -1075,6 +1166,7 @@ wheels = [ name = "mqt-syrec" source = { editable = "." } dependencies = [ + { name = "ijson" }, { name = "mqt-core" }, { name = "pyqt6" }, ] @@ -1125,6 +1217,7 @@ test = [ [package.metadata] requires-dist = [ + { name = "ijson", specifier = "~=3.4.0" }, { name = "mqt-core", specifier = "~=3.4.0" }, { name = "pyqt6", specifier = ">=6.8" }, ] From f83c6f104e1bbdd2b4eb72549cbf23150a4c5e71 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 18 Jan 2026 15:58:30 +0100 Subject: [PATCH 30/88] Added tool tip containing information about expected .json file format to load simulation runs from file button --- .../quantum_circuit_simulation_dialog.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 8b8abfe2..15f77c1e 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -401,6 +401,47 @@ def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayou ) trigger_load_from_file_button.clicked.connect(self.open_import_from_file_dialog) trigger_load_from_file_button.setEnabled(False) + trigger_load_from_file_button.setToolTip(""" +

Expected format of .json file

+
+ Simulation runs imported from a .json file need to be defined in the following json structure: + + { + "simulationRuns": [ + { "in": "1011", "out": "1011" }, + ... + { "in": "1001", "out": "1001" } + ] + } + +
+
+ Further details about the contents of the .json file are listed below: +
    +
  • + The 'simulationRuns' JSON array needs to be defined as a property of the singular top level JSON object, + with every simulation run being defined as a JSON object consisting of an input state qubit values definition + and an optional expected output state definition. All other elements of the top-level object are ignored. +
  • +
  • + All expected json element keys are case sensitive. +
  • +
  • + Qubit values must be defined as strings containing '0' or '1' characters with the total number of qubits in a + state definition matching the number of data qubits of the synthesized quantum computation (i.e. equal to the number of non-ancillary qubits). +
  • +
  • + Any additional objects defined in a simulation run JSON object is skipped. +
  • +
  • + No error will be reported if the 'simulationRuns' object was not defined in the .json file or contained no entries. +
  • +
  • + Any error during the parsing of the .json file will cause the deletion of all parsed simulation runs. +
  • +
+
+ """) controls_layout.addWidget(trigger_load_from_file_button) controls_layout.addStretch() From fe63c77ad52a5aa4b4b4b23cf7ad066fcc779673 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 18 Jan 2026 21:24:46 +0100 Subject: [PATCH 31/88] Reworked acknowledgement of batch data to worker by introducing separate mutex and invoking worker functions directly instead of using signals --- .../quantum_circuit_simulation_dialog.py | 29 +++ .../all_input_states_generator_worker.py | 14 +- .../qt_all_input_states_generator_dialog.py | 28 +-- .../qt_simulation_run_json_export_dialog.py | 223 ++++++++++++++++++ .../qt_simulation_run_json_import_dialog.py | 35 ++- .../qt_simulation_run_model.py | 12 +- .../simulation_run_json_export_worker.py | 110 +++++++++ .../simulation_run_json_import_worker.py | 86 +++---- 8 files changed, 435 insertions(+), 102 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py create mode 100644 python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 15f77c1e..ed12d72e 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -19,6 +19,7 @@ from .simulation_view.qt_all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog +from .simulation_view.qt_simulation_run_json_export_dialog import SimulationRunJsonExportDialog from .simulation_view.qt_simulation_run_json_import_dialog import SimulationRunJsonImportDialog from .simulation_view.qt_simulation_run_model import ( SIMULATION_RUN_IO_STATE_QT_ROLE, @@ -66,6 +67,7 @@ def __init__( self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None self.all_input_states_generator_dialog: AllInputStatesGeneratorDialog | None = None self.simulation_run_import_from_file_dialog: SimulationRunJsonImportDialog | None = None + self.simulation_run_export_to_file_dialog: SimulationRunJsonExportDialog | None = None self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self.simulation_run_dialog: SimulationRunDialog | None = None @@ -182,6 +184,7 @@ def initialize_simulation_runs_tab_widget( "Save simulation runs to file", objectName=SAVE_SIM_RUNS_TO_FILE_BTN_NAME, ) + save_simulation_runs_to_file_button.clicked.connect(self.handle_sim_run_save_to_file_btn_click) save_simulation_runs_to_file_button.setEnabled(False) simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) @@ -334,6 +337,32 @@ def handle_simulation_run_editor_dialog_close(self, result: int) -> None: finally: self.simulation_run_editor_dialog = None + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def handle_sim_run_save_to_file_btn_click(self) -> None: + if self.simulation_run_export_to_file_dialog is not None: + # TODO: Error logging + return + + filename, _ = QtWidgets.QFileDialog.getOpenFileName( + self, "Select a file to export simulation runs to", str(Path.home()), "Json files (*.json)" + ) + + if not filename: + return + + self.simulation_run_export_to_file_dialog = SimulationRunJsonExportDialog(self) + self.simulation_run_export_to_file_dialog.finished.connect(self.handle_sim_run_export_to_file_dialog_close) + self.simulation_run_export_to_file_dialog.start_export( + Path(filename), + self.simulation_runs_model.get_all_simulation_run_models(), + self.simulation_runs_model.rowCount(QtCore.QModelIndex()), + ) + self.simulation_run_export_to_file_dialog.show() + + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] + def handle_sim_run_export_to_file_dialog_close(self, _: int) -> None: + self.simulation_run_export_to_file_dialog = None + def handle_open_and_start_all_input_states_generator_dialog(self, input_state_size: int) -> None: if self.all_input_states_generator_dialog is not None: # TODO: Error logging? diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 04fd5297..dc0d92f6 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -37,6 +37,7 @@ def __init__(self, expected_input_state_size: int, batch_size: int): self.expected_input_state_size: Final[int] = expected_input_state_size self.batch_size: Final[int] = batch_size self.cancellation_requested = False + self.ack_flag_mutex = QtCore.QMutex() self.cancellation_flag_mutex = QtCore.QReadWriteLock() self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() @@ -65,12 +66,8 @@ def start_generation(self) -> None: batch_generation_start_time = batch_generation_end_time self.batch_generated.emit((batch_generation_duration_in_seconds, batch_data.copy())) - try: - self.cancellation_flag_mutex.lockForRead() - # Lock needs to be already held for wait condition to not return immediately - self.wait_on_batch_processed_acknowledgement_condition.wait(self.cancellation_flag_mutex) - finally: - self.cancellation_flag_mutex.unlock() + with QtCore.QMutexLocker(self.ack_flag_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_flag_mutex) # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. time.sleep(0.1) @@ -111,11 +108,12 @@ def start_generation(self) -> None: # - drop the decorator and rely on the automatic queued connection that PyQt already provides. def request_cancellation(self) -> None: self._thread_safe_set_cancellation_requested_flag(True) - self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + self.ack_batch_processed() # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. def ack_batch_processed(self) -> None: - self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + with QtCore.QMutexLocker(self.ack_flag_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wakeOne() @staticmethod def _generate_sim_run_model_for_input_state( diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 4a4383b8..19ba6573 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -22,12 +22,12 @@ TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = ( "Total runtime for input states generation [in seconds]: {total_runtime_in_sec:f}" ) +GENERATED_INPUT_STATE_BATCH_PROGRESS_INFO_TEXT_FORMAT: Final[str] = ( + "Generated batch of input states (runtime [in sec]: {batch_generation_duration_in_seconds:f}!" +) class AllInputStatesGeneratorDialog(QtWidgets.QDialog): # type: ignore[misc] - input_state_batch_ack = QtCore.pyqtSignal(name="inputStateBatchAck") - request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") - def __init__(self, parent: QtWidgets.QWidget): super().__init__(parent) @@ -104,21 +104,12 @@ def start_generation( self.progress_bar.setMaximum(2**expected_input_state_size) self.progress_bar.setValue(0) - self.inputStateBatchAck.connect(self.worker.ack_batch_processed, QtCore.Qt.ConnectionType.QueuedConnection) - self.requestWorkerCancellation.connect( - self.worker.request_cancellation, QtCore.Qt.ConnectionType.QueuedConnection - ) - self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) - # TODO: It is recommended in the official documentation to mark slots explicitly via the QtCore.pyqtSlot() decorator: - # see https://doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html#the-slot-class - # Do not block the UI thread by the potentially long running operations of the worker a new thread is started (which also has its own event loop) # and the worker operation moved to the latter. We also do not want to block the UI thread by executing the slots of said worker in the UI thread but # instead want to simply send the events to the event queue of its thread thus the QueuedConnection between the signal (here the UI thread) and the receiver (worker thread) # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). - self.worker_thread.started.connect(self.worker.start_generation, QtCore.Qt.ConnectionType.QueuedConnection) self.worker.batchGenerated.connect( self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection ) @@ -129,6 +120,7 @@ def start_generation( self._handle_input_state_generator_failure, QtCore.Qt.ConnectionType.QueuedConnection ) + self.worker_thread.started.connect(self.worker.start_generation, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) @@ -163,8 +155,12 @@ def _handle_generated_input_state_batch(self, batch_data: tuple[float, list[Simu if self.stop_processing_recv_input_state_batches: return - self.progress_text_lbl.setText("Generated input state batch!") batch_generation_duration_in_seconds: float = batch_data[0] + self.progress_text_lbl.setText( + GENERATED_INPUT_STATE_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( + batch_generation_duration_in_seconds=batch_generation_duration_in_seconds + ) + ) generated_simulation_run_models: list[SimulationRunModel] = batch_data[1] if self.shared_simulation_runs_model is None: @@ -181,8 +177,8 @@ def _handle_generated_input_state_batch(self, batch_data: tuple[float, list[Simu # TODO: Error handling # TODO: Use delayed processing to reduce "laggy"/almost frozen GUI self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) - self.inputStateBatchAck.emit() - + if self.worker is not None: + self.worker.ack_batch_processed() self.total_input_state_generation_runtime_in_seconds += batch_generation_duration_in_seconds self.total_runtime_text_lbl.setText( TOTAL_RUNTIME_TEXT_FORMAT.format(total_runtime_in_sec=self.total_input_state_generation_runtime_in_seconds) @@ -206,7 +202,7 @@ def _request_worker_cancellation(self) -> None: self.stop_processing_recv_input_state_batches = True self.progress_text_lbl.setText("Requesting cancellation of input state generator!") if self.worker is not None: - self.requestWorkerCancellation.emit() + self.worker.request_cancellation() self._change_dialog_cancellation_button_enable_state(False) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py new file mode 100644 index 00000000..6fdd8333 --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -0,0 +1,223 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore, QtWidgets + +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + + from PyQt6 import QtGui + + from .qt_simulation_run_model import SimulationRunModel + from .simulation_run_json_export_worker import SimulationRunJsonExportWorker +from .simulation_run_json_export_worker import SimulationRunJsonExportWorker + +AGGREGATE_EXPORT_DATA_TEXT_FORMAT: Final[str] = ( + "Exported simulation runs: {num_exported_simulation_runs:d} | Total runtime for simulation run export [in seconds]: {total_runtime_in_sec:f}" +) +EXPORT_LOCATION_INFO_TEXT_FORMAT: Final[str] = "Exporting simulation runs to file {path_to_json_file:s}" +EXPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT: Final[str] = ( + "Finished export of simulation runs batch to file (runtime [in sec]: {batch_export_duration_in_seconds:f}!" +) + + +class SimulationRunJsonExportDialog(QtWidgets.QDialog): # type: ignore[misc] + request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") + + def __init__(self, parent: QtWidgets.QWidget): + super().__init__(parent) + + self.worker_thread: QtCore.QThread | None = None + self.worker: SimulationRunJsonExportWorker | None = None + + self.num_exported_simulation_runs: int = 0 + self.total_sim_run_export_duration_in_secs: float = 0 + + self.setModal(True) + self.setSizeGripEnabled(True) + self.setWindowTitle("Exporting simulation runs...") + left = 0 + top = 0 + width = 400 + height = 200 + self.setGeometry(left, top, width, height) + + main_layout = QtWidgets.QVBoxLayout() + self.export_location_info_lbl = QtWidgets.QLabel("") + self.export_location_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.progress_text_lbl = QtWidgets.QLabel("") + self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.progress_text_lbl.setStyleSheet("QLabel { color : gray; }") + + self.err_text_lbl = QtWidgets.QLabel("") + self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.err_text_lbl.setStyleSheet("QLabel { color : red; }") + + main_layout.addWidget(self.export_location_info_lbl) + main_layout.addWidget(self.progress_text_lbl) + main_layout.addWidget(self.err_text_lbl) + main_layout.addStretch() + + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setFormat("Exported simulation run %v of %m") + self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.total_runtime_text_lbl = QtWidgets.QLabel("") + self.total_runtime_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + main_layout.addWidget(self.total_runtime_text_lbl) + + self.dialog_button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + self.dialog_button_box.setCenterButtons(True) + self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) + self.dialog_button_box.accepted.connect(self.accept) + + self._change_dialog_ok_button_enable_state(False) + self._change_dialog_cancellation_button_enable_state(False) + + main_layout.addWidget(self.dialog_button_box) + self.setLayout(main_layout) + + def start_export( + self, + export_location: Path, + sim_runs_to_export: Iterable[SimulationRunModel], + num_sim_runs_to_export: int, + batch_size: int = 500, + ) -> None: + self.export_location_info_lbl.setText( + EXPORT_LOCATION_INFO_TEXT_FORMAT.format(path_to_json_file=str(export_location)) + ) + self.progress_bar.setMaximum(num_sim_runs_to_export) + + self.worker = SimulationRunJsonExportWorker() # type: ignore[no-untyped-call] + self.requestWorkerCancellation.connect( + self.worker.request_cancellation, QtCore.Qt.ConnectionType.QueuedConnection + ) + + self.worker_thread = QtCore.QThread() + self.worker.moveToThread(self.worker_thread) + self.worker.batchExported.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.exportFinished.connect(self._handle_export_completion, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.exportFailed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) + + self.worker_thread.started.connect( + lambda: self.worker.start_export(export_location, sim_runs_to_export, batch_size), + QtCore.Qt.ConnectionType.QueuedConnection, + ) + self.worker_thread.finished.connect(self.worker_thread.deleteLater) + self.worker_thread.finished.connect(self._reset_workers) + self.worker_thread.start(QtCore.QThread.Priority.LowPriority) + self._change_dialog_cancellation_button_enable_state(True) + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + # Ask for confirmation before closing + if self.worker is None or self._handle_export_to_file_cancel_button_click(): + if not self.err_text_lbl.text(): + self.accept() + else: + self.reject() + else: + event.ignore() + + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_export_failure(self, err: Exception) -> None: + self.progress_text_lbl.setText("") + self.progress_bar.setVisible(False) + + self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during export of simulation runs") + self._request_worker_cancellation() + self._await_worker_thread_completion() + self._change_dialog_cancellation_button_enable_state(False) + + @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] + def _handle_batch_exported(self, batch_data: tuple[float, int]) -> None: + batch_export_duration_in_seconds: Final[float] = batch_data[0] + self.progress_text_lbl.setText( + EXPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( + batch_export_duration_in_seconds=batch_export_duration_in_seconds + ) + ) + num_sim_runs_exported_in_batch: Final[int] = batch_data[1] + + self.num_exported_simulation_runs += num_sim_runs_exported_in_batch + self.progress_bar.setValue(self.num_exported_simulation_runs) + + self.total_sim_run_export_duration_in_secs += batch_export_duration_in_seconds + self.progress_text_lbl.setText( + AGGREGATE_EXPORT_DATA_TEXT_FORMAT.format( + num_exported_simulation_runs=self.num_exported_simulation_runs, + total_runtime_in_sec=self.total_sim_run_export_duration_in_secs, + ) + ) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_export_completion(self) -> None: + self.progress_bar.setVisible(False) + + self._request_worker_cancellation() + self._await_worker_thread_completion() + self._change_dialog_cancellation_button_enable_state(False) + self._change_dialog_ok_button_enable_state(True) + + def _request_worker_cancellation(self) -> None: + self.progress_text_lbl.setText("Requesting cancellation of simulation run importer!") + if self.worker is not None: + self.worker.request_cancellation() + + def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + + if dialog_cancel_button is None: + return + + dialog_cancel_button.setEnabled(should_button_be_enabled) + + def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: + dialog_ok_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + QtWidgets.QDialogButtonBox.StandardButton.Ok + ) + + if dialog_ok_button is None: + return + + dialog_ok_button.setEnabled(should_button_be_enabled) + + def _reset_workers(self) -> None: + self.worker_thread = None + self.worker = None + + def _await_worker_thread_completion(self) -> None: + if self.worker_thread is not None: + self.worker_thread.quit() + self.worker_thread.wait() + self.progress_text_lbl.setText("Simulation run exporter thread finished!") + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_export_to_file_cancel_button_click(self) -> bool: + clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( + self, + "Cancellation of export to json file!", + "Are you sure that you want to stop the export of simulation runs to the .json file?", + buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, + defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + ) + + if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: + self._request_worker_cancellation() + return True + return False diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py index 2ae9bc21..6956ca55 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -25,12 +25,12 @@ "Imported simulation runs: {num_imported_simulation_runs:d} | Total runtime for simulation run import [in seconds]: {total_runtime_in_sec:f}" ) IMPORT_ORIGIN_INFO_TEXT_FORMAT: Final[str] = "Importing simulation runs from file {path_to_json_file:s}" +IMPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT: Final[str] = ( + "Finished import of simulation runs batch from file (runtime [in sec]: {batch_generation_duration_in_seconds:f}!" +) class SimulationRunJsonImportDialog(QtWidgets.QDialog): # type: ignore[misc] - imported_sim_run_batch_ack = QtCore.pyqtSignal(name="importedSimRunBatchAck") - request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") - def __init__(self, parent: QtWidgets.QWidget): super().__init__(parent) @@ -96,29 +96,21 @@ def start_generation( self.import_origin_info_lbl.setText( IMPORT_ORIGIN_INFO_TEXT_FORMAT.format(path_to_json_file=str(path_to_json_file)) ) - try: - self.worker = SimulationRunJsonImportWorker(path_to_json_file, expected_input_state_size, batch_size) - except ValueError as err: - self.err_text_lbl.setText( - f"Error {err=}, {type(err)=} during initialization of simulation run json importer!" - ) - return - - self.importedSimRunBatchAck.connect(self.worker.ack_batch_processed, QtCore.Qt.ConnectionType.QueuedConnection) - self.requestWorkerCancellation.connect( - self.worker.request_cancellation, QtCore.Qt.ConnectionType.QueuedConnection - ) + # TODO: Why can we not use a lambda to pass the arguments to the start_import call of the worker instance + # instead of passing them as constructor arguments that are otherwise not needed in the instance. + # Compare with the SimulationRunJsonExportWorker (that could contain a deadlock) since when we were using a + # lambda, the _handle_imported_sim_run_batch was not called. + self.worker = SimulationRunJsonImportWorker(path_to_json_file, expected_input_state_size, batch_size) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) - self.worker_thread.started.connect(self.worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.batchImported.connect( self._handle_imported_sim_run_batch, QtCore.Qt.ConnectionType.QueuedConnection ) self.worker.importFinished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) self.worker.importFailed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker_thread.started.connect(self.worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) @@ -159,7 +151,9 @@ def _handle_imported_sim_run_batch(self, batch_data: tuple[float, list[Simulatio batch_generation_duration_in_seconds: float = batch_data[0] self.progress_text_lbl.setText( - f"Finished generation of simulation runs batch from data of file (runtime [in sec]: {batch_generation_duration_in_seconds}!" + IMPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( + batch_generation_duration_in_seconds=batch_generation_duration_in_seconds + ) ) generated_simulation_run_models: list[SimulationRunModel] = batch_data[1] @@ -178,7 +172,8 @@ def _handle_imported_sim_run_batch(self, batch_data: tuple[float, list[Simulatio # TODO: Error handling # TODO: Use delayed processing to reduce "laggy"/almost frozen GUI self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) - self.imported_sim_run_batch_ack.emit() + if self.worker is not None: + self.worker.ack_batch_processed() self.total_simulation_run_import_runtime_in_seconds += batch_generation_duration_in_seconds self.num_imported_simulation_runs += len(generated_simulation_run_models) @@ -200,7 +195,7 @@ def _request_worker_cancellation(self) -> None: self.stop_processing_imported_sim_run_batches = True self.progress_text_lbl.setText("Requesting cancellation of simulation run importer!") if self.worker is not None: - self.requestWorkerCancellation.emit() + self.worker.request_cancellation() def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index ea30696c..54b7cd12 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -9,12 +9,15 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Final +from typing import TYPE_CHECKING, Final from PyQt6 import QtCore from mqt import syrec +if TYPE_CHECKING: + from collections.abc import Iterable + # TODO: Mark as const: https://stackoverflow.com/a/57596202 # Some debugging tips: https://www.eso.org/~eltmgr/ECS/documents-latest/CUT/sphinx_doc/latest/docs/500_gui_development.html#gdb # First custom item data role usable according to: https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum @@ -238,6 +241,9 @@ def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: return self.simulation_run_models[index] return None + def get_all_simulation_run_models(self) -> Iterable[SimulationRunModel]: + yield from self.simulation_run_models + # TODO: Check for duplicates? def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> bool: n_simulation_runs: int = len(self.simulation_run_models) diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py new file mode 100644 index 00000000..58f65218 --- /dev/null +++ b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import json +import time +from typing import TYPE_CHECKING, Any + +from PyQt6 import QtCore + +from .qt_simulation_run_model import SimulationRunModel + +if TYPE_CHECKING: + from collections.abc import Iterable + from pathlib import Path + + +class SimulationRunJsonExportWorker(QtCore.QObject): # type: ignore[misc] + batch_exported = QtCore.pyqtSignal(tuple, name="batchExported") + export_failed = QtCore.pyqtSignal(Exception, name="exportFailed") + export_finished = QtCore.pyqtSignal(name="exportFinished") + + def __init__(self): + super().__init__() + + self.cancellation_requested = False + self.cancellation_flag_mutex = QtCore.QReadWriteLock() + + # TODO: Pretty printing + # TODO: This untyped decorator should not be invocable via a signal? + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_export( + self, + path_to_json_file: Path, + simulation_runs_to_export: Iterable[SimulationRunModel], + export_batch_size: int, + ) -> None: + if export_batch_size < 1: + return + + n_generated_batches: int = 0 + try: + batch_idx: int = 0 + with path_to_json_file.open("w", encoding="utf-8") as file: + # file.write("{\n\t\"simulationRuns\": [\n") + file.write('{"simulationRuns":[') + batch_export_start_time: float = time.perf_counter() + batch_export_end_time: float = 0 + batch_export_duration: float = 0 + for sim_run in simulation_runs_to_export: + if self._thread_safe_check_whether_cancellation_is_requested(): + break + + # if batch_idx > 0: + # file.write(",\n") + # file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json, indent=2)) + + if batch_idx > 0 or (batch_idx == 0 and n_generated_batches > 0): + file.write(",") + file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json)) + + batch_idx += 1 + if batch_idx == export_batch_size: + batch_export_end_time = time.perf_counter() + batch_export_duration = batch_export_end_time - batch_export_start_time + batch_export_start_time = batch_export_end_time + self.batch_exported.emit((batch_export_duration, export_batch_size)) + batch_idx = 0 + n_generated_batches += 1 + # file.write("\n\t]\n}") + file.write("]}") + + if batch_idx > 0 and not self._thread_safe_check_whether_cancellation_is_requested(): + batch_export_end_time = time.perf_counter() + batch_export_duration = batch_export_end_time - batch_export_start_time + self.batch_exported.emit((batch_export_duration, batch_idx)) + self.export_finished.emit() + except Exception as err: + self.export_failed.emit(err) + return + + def request_cancellation(self) -> None: + self._thread_safe_set_cancellation_requested_flag(True) + + def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: + cancellation_requested: bool = False + self.cancellation_flag_mutex.lockForRead() + cancellation_requested = self.cancellation_requested + self.cancellation_flag_mutex.unlock() + return cancellation_requested + + def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None: + self.cancellation_flag_mutex.lockForWrite() + self.cancellation_requested = flag_value + self.cancellation_flag_mutex.unlock() + + @staticmethod + def serialize_to_json(obj: Any) -> object: + if isinstance(obj, SimulationRunModel): + if obj.expected_output_state is None: + return {"in": str(obj.input_state)} + return {"in": str(obj.input_state), "out": str(obj.expected_output_state)} + msg = f"Cannot serialize object of {type(obj)}" + raise TypeError(msg) diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py index 015e05be..7247e90d 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py @@ -9,6 +9,8 @@ from __future__ import annotations import time + +# import pthread from typing import TYPE_CHECKING, Any, Final # TODO: Correctly configure third-party package for mypy @@ -23,6 +25,7 @@ if TYPE_CHECKING: from pathlib import Path +SIMULATION_RUNS_JSON_KEY: Final[str] = "simulationRuns" INPUT_STATE_JSON_KEY: Final[str] = "in" EXPECTED_OUTPUT_STATE_JSON_KEY: Final[str] = "out" @@ -36,19 +39,13 @@ class SimulationRunJsonImportWorker(QtCore.QObject): # type: ignore[misc] def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batch_size: int): super().__init__() - if expected_input_state_size < 0: - msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" - raise ValueError(msg) - - if batch_size < 1: - msg = f"Batch size must be larger than 0 but was actually {batch_size}" - raise ValueError(msg) + self.path_to_json_file = path_to_json_file + self.expected_input_state_size = expected_input_state_size + self.batch_size = batch_size - self.path_to_json_file: Path = path_to_json_file - self.expected_input_state_size: Final[int] = expected_input_state_size - self.batch_size: Final[int] = batch_size self.cancellation_requested = False self.cancellation_flag_mutex = QtCore.QReadWriteLock() + self.ack_mutex = QtCore.QMutex() self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -56,9 +53,11 @@ def start_import(self) -> None: batch_generation_end_time: float = 0 batch_generation_duration_in_seconds: float = 0 - batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] - batch_idx: int = 0 try: + SimulationRunJsonImportWorker._validate_parameters(self.expected_input_state_size, self.batch_size) + batch_idx: int = 0 + batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] + # Reading bytes instead of strings leads to better parser performance with self.path_to_json_file.open("rb") as file: batch_generation_start_time: float = time.perf_counter() @@ -66,7 +65,7 @@ def start_import(self) -> None: # to be a property of the top level element (i.e. the path to the 'simulationRuns' element is relative to the top level element). # Additionally, with the postfix '.item', only the entries of a JSON array are processed. If the 'simulationRuns' entry value is no # array then no objects will be parsed. - parser = ijson.items(file, prefix="simulationRuns.item") + parser = ijson.items(file, prefix=f"{SIMULATION_RUNS_JSON_KEY}.item") for arr_elem in parser: if self._thread_safe_check_whether_cancellation_is_requested(): break @@ -86,16 +85,12 @@ def start_import(self) -> None: batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time batch_generation_start_time = batch_generation_end_time - self.batch_imported.emit((batch_generation_duration_in_seconds, batch_data)) - try: - self.cancellation_flag_mutex.lockForRead() - # Lock needs to be already held for wait condition to not return immediately - self.wait_on_batch_processed_acknowledgement_condition.wait(self.cancellation_flag_mutex) - finally: - self.cancellation_flag_mutex.unlock() - # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using - # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. - time.sleep(0.1) + self.batch_imported.emit((batch_generation_duration_in_seconds, batch_data.copy())) + with QtCore.QMutexLocker(self.ack_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_mutex) + # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using + # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. + time.sleep(0.1) for i in range(self.batch_size): batch_data[i] = None @@ -111,42 +106,13 @@ def start_import(self) -> None: except Exception as err: self.import_failed.emit(err) - # def write_streaming(self, source: Iterable[Any], chunk_size: int = 1_000): - # """ - # Write large JSON array without blocking. - # source can be a generator (lazy). - # """ - # try: - # total = None - # if hasattr(source, "__len__"): - # total = len(source) - # with self.file_path.open("w", encoding="utf-8") as f: - # f.write("[\n") - # for idx, item in enumerate(source): - # if self.should_stop(): - # self.finished.emit(False) - # return - # if idx: - # f.write(",\n") - # json.dump(item, f, ensure_ascii=False) - # if total: - # self.progress.emit(int(100 * (idx + 1) / total)) - # else: - # self.progress.emit(-1) # indeterminate - # f.write("\n]") - # self.progress.emit(100) - # self.finished.emit(True) - # except Exception as exc: - # self.error.emit(str(exc)) - - # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. - def request_cancellation(self) -> None: self._thread_safe_set_cancellation_requested_flag(True) - self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + self.ack_batch_processed() def ack_batch_processed(self) -> None: - self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + with QtCore.QMutexLocker(self.ack_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wakeOne() def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: cancellation_requested: bool = False @@ -160,6 +126,16 @@ def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None self.cancellation_requested = flag_value self.cancellation_flag_mutex.unlock() + @staticmethod + def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: + if expected_input_state_size < 0: + msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" + raise ValueError(msg) + + if batch_size < 1: + msg = f"Batch size must be larger than 0 but was actually {batch_size}" + raise ValueError(msg) + @staticmethod def _try_deserialize_simulation_run( expected_state_size: int, parsed_json_elem_values_dict: dict[str, Any] From 4d267308169bd78288da2ca41ad093b6ca13fffa Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 18 Jan 2026 23:19:56 +0100 Subject: [PATCH 32/88] Refactored some shared worker functionality into shared base class --- .../all_input_states_generator_worker.py | 64 ++++------------ .../cancellable_base_worker.py | 74 +++++++++++++++++++ .../qt_all_input_states_generator_dialog.py | 27 ++++--- .../qt_simulation_run_json_export_dialog.py | 30 ++++---- .../qt_simulation_run_json_import_dialog.py | 22 ++++-- .../simulation_view/qt_simulation_worker.py | 5 +- .../simulation_run_json_export_worker.py | 39 +++------- .../simulation_run_json_import_worker.py | 56 ++++---------- 8 files changed, 160 insertions(+), 157 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/cancellable_base_worker.py diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index dc0d92f6..d16ebbf5 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -15,16 +15,13 @@ from mqt import syrec +from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel -class AllInputStatesGeneratorWorker(QtCore.QObject): # type: ignore[misc] - batch_generated = QtCore.pyqtSignal(tuple, name="batchGenerated") - generation_failed = QtCore.pyqtSignal(Exception, name="generationFailed") - generation_finished = QtCore.pyqtSignal(name="generationFinished") - +class AllInputStatesGeneratorWorker(CancellableBaseWorker): def __init__(self, expected_input_state_size: int, batch_size: int): - super().__init__() + super().__init__(do_batches_require_ack=True) if expected_input_state_size < 0: msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" @@ -36,10 +33,6 @@ def __init__(self, expected_input_state_size: int, batch_size: int): self.expected_input_state_size: Final[int] = expected_input_state_size self.batch_size: Final[int] = batch_size - self.cancellation_requested = False - self.ack_flag_mutex = QtCore.QMutex() - self.cancellation_flag_mutex = QtCore.QReadWriteLock() - self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_generation(self) -> None: @@ -50,10 +43,14 @@ def start_generation(self) -> None: batch_generation_end_time: float = 0 batch_generation_duration_in_seconds: float = 0 try: + if self.wait_on_batch_processed_acknowledgement_condition is None: + self.failed.emit(ValueError("Internal batch processed acknowledgement condition was not initialized")) + return + first_integer_encoding_first_state_of_batch: int = 0 batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] for _ in range(n_batches): - if self._thread_safe_check_whether_cancellation_is_requested(): + if self.is_cancellation_requested(): break for i in range(self.batch_size): @@ -65,7 +62,7 @@ def start_generation(self) -> None: batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time batch_generation_start_time = batch_generation_end_time - self.batch_generated.emit((batch_generation_duration_in_seconds, batch_data.copy())) + self.batchCompleted.emit(batch_generation_duration_in_seconds, batch_data.copy()) with QtCore.QMutexLocker(self.ack_flag_mutex): self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_flag_mutex) # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using @@ -77,7 +74,7 @@ def start_generation(self) -> None: batch_data[i] = None n_elems_in_last_batch: int = n_states_to_generate % self.batch_size - if n_elems_in_last_batch != 0 and not self._thread_safe_check_whether_cancellation_is_requested(): + if n_elems_in_last_batch != 0 and not self.is_cancellation_requested(): last_batch_data: list[SimulationRunModel | None] = [None for _ in range(n_elems_in_last_batch)] for i in range(n_elems_in_last_batch): last_batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( @@ -87,33 +84,10 @@ def start_generation(self) -> None: batch_generation_end_time = time.perf_counter() batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time batch_generation_start_time = batch_generation_end_time - self.batch_generated.emit((batch_generation_duration_in_seconds, last_batch_data)) + self.batchCompleted.emit(batch_generation_duration_in_seconds, last_batch_data) except Exception as err: - self.generation_failed.emit(err) - self.generation_finished.emit() - - # In the cross thread communication between the main thread (rendering the GUI) and the worker thread we had the issue that if we define the slot function with a QtCore.pyqtSlot() decorator - # the main thread will not invoke said slot in the worker thread but we do not know exactly we since other signal->slot connections between the two threads function when being defined with - # corresponding decorators. Thus for now we define the slot without a decorator. - # - # One explanation, generated by an AI agent, was: - # The decisive moment is when the decorator runs, not when you later do moveToThread. - # At import time (define time) the worker instance already exists and lives in the GUI thread; the decorator therefore registers the slot in the GUI thread's meta-object. - # Afterwards you move the object to the worker thread, but the meta-object data that Qt uses to locate the slot stays where it was created - in the GUI thread. - # When the GUI thread later emits the signal, Qt again looks in the GUI thread's meta-object, finds the entry that was created by the decorator, and tries to invoke it. - # Because the slot entry is marked as "belonging to another thread" (the worker thread), Qt posts a queued meta-call to that thread … but the worker thread has no corresponding meta-object entry, so nothing is executed. - # If you remove the decorator the connection is handled purely in Python (Qt simply stores the callable); the queued connection then works, because Python callables are independent of the meta-object system. - # In short: - # - pyqtSlot must be executed after the object has been moved to the target thread, or - # - drop the decorator and rely on the automatic queued connection that PyQt already provides. - def request_cancellation(self) -> None: - self._thread_safe_set_cancellation_requested_flag(True) - self.ack_batch_processed() - - # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. - def ack_batch_processed(self) -> None: - with QtCore.QMutexLocker(self.ack_flag_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wakeOne() + self.failed.emit(err) + self.finished.emit() @staticmethod def _generate_sim_run_model_for_input_state( @@ -123,15 +97,3 @@ def _generate_sim_run_model_for_input_state( for qubit in range(expected_input_state_size): input_state.set(qubit, bool((integer_defining_input_state >> qubit) & 1)) return SimulationRunModel(input_state, expected_output_state=None) - - def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: - cancellation_requested: bool = False - self.cancellation_flag_mutex.lockForRead() - cancellation_requested = self.cancellation_requested - self.cancellation_flag_mutex.unlock() - return cancellation_requested - - def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None: - self.cancellation_flag_mutex.lockForWrite() - self.cancellation_requested = flag_value - self.cancellation_flag_mutex.unlock() diff --git a/python/mqt/syrec/simulation_view/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/cancellable_base_worker.py new file mode 100644 index 00000000..29f9e236 --- /dev/null +++ b/python/mqt/syrec/simulation_view/cancellable_base_worker.py @@ -0,0 +1,74 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import Any, TypeVar + +from PyQt6 import QtCore + +T = TypeVar("T") + + +class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] + batch_completed = QtCore.pyqtSignal(float, object, name="batchCompleted") + finished = QtCore.pyqtSignal(name="finished") + failed = QtCore.pyqtSignal(Exception, name="failed") + + def __init__(self, do_batches_require_ack: bool): + super().__init__() + + self.cancellation_requested = False + self.cancellation_flag_mutex = QtCore.QReadWriteLock() + self.batch_ack_mutex: QtCore.QMutex | None = QtCore.QMutex() if do_batches_require_ack else None + self.wait_on_batch_processed_acknowledgement_condition: QtCore.QWaitCondition | None = ( + QtCore.QWaitCondition() if do_batches_require_ack else None + ) + + # TODO: Is this comment still required if we manually invoke the function instead of using signals? Check AI bots? + # In the cross thread communication between the main thread (rendering the GUI) and the worker thread we had the issue that if we define the slot function with a QtCore.pyqtSlot() decorator + # the main thread will not invoke said slot in the worker thread but we do not know exactly we since other signal->slot connections between the two threads function when being defined with + # corresponding decorators. Thus for now we define the slot without a decorator. + # + # One explanation, generated by an AI agent, was: + # The decisive moment is when the decorator runs, not when you later do moveToThread. + # At import time (define time) the worker instance already exists and lives in the GUI thread; the decorator therefore registers the slot in the GUI thread's meta-object. + # Afterwards you move the object to the worker thread, but the meta-object data that Qt uses to locate the slot stays where it was created - in the GUI thread. + # When the GUI thread later emits the signal, Qt again looks in the GUI thread's meta-object, finds the entry that was created by the decorator, and tries to invoke it. + # Because the slot entry is marked as "belonging to another thread" (the worker thread), Qt posts a queued meta-call to that thread … but the worker thread has no corresponding meta-object entry, so nothing is executed. + # If you remove the decorator the connection is handled purely in Python (Qt simply stores the callable); the queued connection then works, because Python callables are independent of the meta-object system. + # In short: + # - pyqtSlot must be executed after the object has been moved to the target thread, or + # - drop the decorator and rely on the automatic queued connection that PyQt already provides. + def request_cancellation(self) -> None: + self.set_cancellation_requested_flag(True) + self.ack_batch_processed() + + # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. + def ack_batch_processed(self) -> None: + if self.batch_ack_mutex is not None and self.wait_on_batch_processed_acknowledgement_condition is not None: + with QtCore.QMutexLocker(self.batch_ack_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wakeOne() + + def is_cancellation_requested(self) -> bool: + cancellation_requested: bool = False + self.cancellation_flag_mutex.lockForRead() + cancellation_requested = self.cancellation_requested + self.cancellation_flag_mutex.unlock() + return cancellation_requested + + def set_cancellation_requested_flag(self, flag_value: bool) -> None: + self.cancellation_flag_mutex.lockForWrite() + self.cancellation_requested = flag_value + self.cancellation_flag_mutex.unlock() + + @staticmethod + def are_list_of_batch_items_of_type(batch_data: Any, expected_batch_element_type: type[T]) -> bool: + return isinstance(batch_data, list) and all( + isinstance(batch_item, expected_batch_element_type) for batch_item in batch_data + ) diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 19ba6573..42ac3889 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -15,9 +15,10 @@ if TYPE_CHECKING: from PyQt6 import QtGui - from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel + from .qt_simulation_run_model import QtSimulationRunModel from .all_input_states_generator_worker import AllInputStatesGeneratorWorker +from .qt_simulation_run_model import SimulationRunModel TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = ( "Total runtime for input states generation [in seconds]: {total_runtime_in_sec:f}" @@ -93,6 +94,7 @@ def start_generation( self.progress_text_lbl.setText("") self.total_runtime_text_lbl.setText("") + # TODO: Again try to refactor code such that constructor arguments are pass in start_generation call instead try: self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) except ValueError as err: @@ -110,13 +112,13 @@ def start_generation( # and the worker operation moved to the latter. We also do not want to block the UI thread by executing the slots of said worker in the UI thread but # instead want to simply send the events to the event queue of its thread thus the QueuedConnection between the signal (here the UI thread) and the receiver (worker thread) # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). - self.worker.batchGenerated.connect( + self.worker.batchCompleted.connect( self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.generationFinished.connect( + self.worker.finished.connect( self._handle_input_state_generator_finished, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.generationFailed.connect( + self.worker.failed.connect( self._handle_input_state_generator_failure, QtCore.Qt.ConnectionType.QueuedConnection ) @@ -150,19 +152,26 @@ def _handle_input_state_generator_failure(self, err: Exception) -> None: self._request_worker_cancellation() self._await_worker_thread_completion() - @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] - def _handle_generated_input_state_batch(self, batch_data: tuple[float, list[SimulationRunModel]]) -> None: + @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] + def _handle_generated_input_state_batch( + self, batch_generation_duration_in_seconds: float, batch_data: object + ) -> None: if self.stop_processing_recv_input_state_batches: return - batch_generation_duration_in_seconds: float = batch_data[0] + if not AllInputStatesGeneratorWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunModel): + # TODO: Error logging? + # TODO: Cancel worker? + if self.worker is not None: + self.worker.ack_batch_processed() + return + self.progress_text_lbl.setText( GENERATED_INPUT_STATE_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( batch_generation_duration_in_seconds=batch_generation_duration_in_seconds ) ) - generated_simulation_run_models: list[SimulationRunModel] = batch_data[1] - + generated_simulation_run_models: Final[list[SimulationRunModel]] = batch_data # type: ignore[assignment] if self.shared_simulation_runs_model is None: QtWidgets.QMessageBox.critical( self, diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py index 6fdd8333..e075dfba 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -32,8 +32,6 @@ class SimulationRunJsonExportDialog(QtWidgets.QDialog): # type: ignore[misc] - request_worker_cancellation = QtCore.pyqtSignal(name="requestWorkerCancellation") - def __init__(self, parent: QtWidgets.QWidget): super().__init__(parent) @@ -103,15 +101,11 @@ def start_export( self.progress_bar.setMaximum(num_sim_runs_to_export) self.worker = SimulationRunJsonExportWorker() # type: ignore[no-untyped-call] - self.requestWorkerCancellation.connect( - self.worker.request_cancellation, QtCore.Qt.ConnectionType.QueuedConnection - ) - self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) - self.worker.batchExported.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.exportFinished.connect(self._handle_export_completion, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.exportFailed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.batchCompleted.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.finished.connect(self._handle_export_completion, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.failed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.started.connect( lambda: self.worker.start_export(export_location, sim_runs_to_export, batch_size), @@ -142,20 +136,22 @@ def _handle_export_failure(self, err: Exception) -> None: self._await_worker_thread_completion() self._change_dialog_cancellation_button_enable_state(False) - @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] - def _handle_batch_exported(self, batch_data: tuple[float, int]) -> None: - batch_export_duration_in_seconds: Final[float] = batch_data[0] + @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] + def _handle_batch_exported(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: + if not isinstance(batch_data, int): + # TODO: Error logging? + # TODO: Cancel worker? + return + self.progress_text_lbl.setText( EXPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( - batch_export_duration_in_seconds=batch_export_duration_in_seconds + batch_export_duration_in_seconds=batch_generation_duration_in_seconds ) ) - num_sim_runs_exported_in_batch: Final[int] = batch_data[1] - - self.num_exported_simulation_runs += num_sim_runs_exported_in_batch + self.num_exported_simulation_runs += batch_data self.progress_bar.setValue(self.num_exported_simulation_runs) - self.total_sim_run_export_duration_in_secs += batch_export_duration_in_seconds + self.total_sim_run_export_duration_in_secs += batch_generation_duration_in_seconds self.progress_text_lbl.setText( AGGREGATE_EXPORT_DATA_TEXT_FORMAT.format( num_exported_simulation_runs=self.num_exported_simulation_runs, diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py index 6956ca55..690e0d08 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -19,6 +19,7 @@ from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel +from .qt_simulation_run_model import SimulationRunModel from .simulation_run_json_import_worker import SimulationRunJsonImportWorker AGGREGATE_IMPORT_DATA_TEXT_FORMAT: Final[str] = ( @@ -104,11 +105,11 @@ def start_generation( self.worker = SimulationRunJsonImportWorker(path_to_json_file, expected_input_state_size, batch_size) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) - self.worker.batchImported.connect( + self.worker.batchCompleted.connect( self._handle_imported_sim_run_batch, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.importFinished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.importFailed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.finished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.failed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.started.connect(self.worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.finished.connect(self.worker_thread.deleteLater) @@ -144,19 +145,24 @@ def _handle_importer_failure(self, err: Exception) -> None: self._await_worker_thread_completion() self._change_dialog_cancellation_button_enable_state(False) - @QtCore.pyqtSlot(tuple) # type: ignore[untyped-decorator] - def _handle_imported_sim_run_batch(self, batch_data: tuple[float, list[SimulationRunModel]]) -> None: + @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] + def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: if self.stop_processing_imported_sim_run_batches: return - batch_generation_duration_in_seconds: float = batch_data[0] + if not SimulationRunJsonImportWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunModel): + # TODO: Error logging? + # TODO: Cancel worker? + if self.worker is not None: + self.worker.ack_batch_processed() + return + self.progress_text_lbl.setText( IMPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( batch_generation_duration_in_seconds=batch_generation_duration_in_seconds ) ) - generated_simulation_run_models: list[SimulationRunModel] = batch_data[1] - + generated_simulation_run_models: Final[list[SimulationRunModel]] = batch_data # type: ignore[assignment] if self.shared_simulation_runs_model is None: QtWidgets.QMessageBox.critical( self, diff --git a/python/mqt/syrec/simulation_view/qt_simulation_worker.py b/python/mqt/syrec/simulation_view/qt_simulation_worker.py index 685eb91a..7523e993 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_worker.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_worker.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -37,6 +37,7 @@ class SimulationRunResult: execution_runtime_in_ms: float +# TODO: Rework to use same cancel functionality and signals as cancellable_base_worker if possible class SimulationWorker(QtCore.QObject): # type: ignore[misc] simulation_run_completed = QtCore.pyqtSignal(SimulationRunResult, name="simulationRunCompleted") simulation_run_mismatch_between_output_states = QtCore.pyqtSignal( diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py index 58f65218..acb39a3d 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py @@ -14,6 +14,7 @@ from PyQt6 import QtCore +from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: @@ -21,16 +22,9 @@ from pathlib import Path -class SimulationRunJsonExportWorker(QtCore.QObject): # type: ignore[misc] - batch_exported = QtCore.pyqtSignal(tuple, name="batchExported") - export_failed = QtCore.pyqtSignal(Exception, name="exportFailed") - export_finished = QtCore.pyqtSignal(name="exportFinished") - +class SimulationRunJsonExportWorker(CancellableBaseWorker): def __init__(self): - super().__init__() - - self.cancellation_requested = False - self.cancellation_flag_mutex = QtCore.QReadWriteLock() + super().__init__(do_batches_require_ack=False) # TODO: Pretty printing # TODO: This untyped decorator should not be invocable via a signal? @@ -54,7 +48,7 @@ def start_export( batch_export_end_time: float = 0 batch_export_duration: float = 0 for sim_run in simulation_runs_to_export: - if self._thread_safe_check_whether_cancellation_is_requested(): + if self.is_cancellation_requested(): break # if batch_idx > 0: @@ -70,36 +64,21 @@ def start_export( batch_export_end_time = time.perf_counter() batch_export_duration = batch_export_end_time - batch_export_start_time batch_export_start_time = batch_export_end_time - self.batch_exported.emit((batch_export_duration, export_batch_size)) + self.batchCompleted.emit(batch_export_duration, export_batch_size) batch_idx = 0 n_generated_batches += 1 # file.write("\n\t]\n}") file.write("]}") - if batch_idx > 0 and not self._thread_safe_check_whether_cancellation_is_requested(): + if batch_idx > 0 and not self.is_cancellation_requested(): batch_export_end_time = time.perf_counter() batch_export_duration = batch_export_end_time - batch_export_start_time - self.batch_exported.emit((batch_export_duration, batch_idx)) - self.export_finished.emit() + self.batchCompleted.emit(batch_export_duration, batch_idx) + self.finished.emit() except Exception as err: - self.export_failed.emit(err) + self.failed.emit(err) return - def request_cancellation(self) -> None: - self._thread_safe_set_cancellation_requested_flag(True) - - def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: - cancellation_requested: bool = False - self.cancellation_flag_mutex.lockForRead() - cancellation_requested = self.cancellation_requested - self.cancellation_flag_mutex.unlock() - return cancellation_requested - - def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None: - self.cancellation_flag_mutex.lockForWrite() - self.cancellation_requested = flag_value - self.cancellation_flag_mutex.unlock() - @staticmethod def serialize_to_json(obj: Any) -> object: if isinstance(obj, SimulationRunModel): diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py index 7247e90d..89c308d6 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py @@ -20,6 +20,7 @@ from mqt import syrec +from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: @@ -30,24 +31,14 @@ EXPECTED_OUTPUT_STATE_JSON_KEY: Final[str] = "out" -class SimulationRunJsonImportWorker(QtCore.QObject): # type: ignore[misc] - batch_imported = QtCore.pyqtSignal(tuple, name="batchImported") - import_finished = QtCore.pyqtSignal(name="importFinished") - import_cancelled = QtCore.pyqtSignal(name="importCancelled") - import_failed = QtCore.pyqtSignal(Exception, name="importFailed") - +class SimulationRunJsonImportWorker(CancellableBaseWorker): def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batch_size: int): - super().__init__() + super().__init__(do_batches_require_ack=True) self.path_to_json_file = path_to_json_file self.expected_input_state_size = expected_input_state_size self.batch_size = batch_size - self.cancellation_requested = False - self.cancellation_flag_mutex = QtCore.QReadWriteLock() - self.ack_mutex = QtCore.QMutex() - self.wait_on_batch_processed_acknowledgement_condition = QtCore.QWaitCondition() - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_import(self) -> None: batch_generation_end_time: float = 0 @@ -55,6 +46,10 @@ def start_import(self) -> None: try: SimulationRunJsonImportWorker._validate_parameters(self.expected_input_state_size, self.batch_size) + if self.wait_on_batch_processed_acknowledgement_condition is None: + self.failed.emit(ValueError("Internal batch processed acknowledgement condition was not initialized")) + return + batch_idx: int = 0 batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] @@ -67,7 +62,7 @@ def start_import(self) -> None: # array then no objects will be parsed. parser = ijson.items(file, prefix=f"{SIMULATION_RUNS_JSON_KEY}.item") for arr_elem in parser: - if self._thread_safe_check_whether_cancellation_is_requested(): + if self.is_cancellation_requested(): break # the ison.items(...) function converts JSON objects to python dictionaries (https://pypi.org/project/ijson/#options). However, we @@ -85,9 +80,9 @@ def start_import(self) -> None: batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time batch_generation_start_time = batch_generation_end_time - self.batch_imported.emit((batch_generation_duration_in_seconds, batch_data.copy())) - with QtCore.QMutexLocker(self.ack_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_mutex) + self.batchCompleted.emit(batch_generation_duration_in_seconds, batch_data.copy()) + with QtCore.QMutexLocker(self.ack_flag_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_flag_mutex) # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. time.sleep(0.1) @@ -96,35 +91,16 @@ def start_import(self) -> None: batch_data[i] = None batch_idx = 0 - if batch_idx != 0: + if batch_idx != 0 and not self.is_cancellation_requested(): del batch_data[batch_idx:] batch_generation_end_time = time.perf_counter() batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time batch_generation_start_time = batch_generation_end_time - self.batch_imported.emit((batch_generation_duration_in_seconds, batch_data)) - self.import_finished.emit() + + self.batchCompleted.emit(batch_generation_duration_in_seconds, batch_data) + self.finished.emit() except Exception as err: - self.import_failed.emit(err) - - def request_cancellation(self) -> None: - self._thread_safe_set_cancellation_requested_flag(True) - self.ack_batch_processed() - - def ack_batch_processed(self) -> None: - with QtCore.QMutexLocker(self.ack_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wakeOne() - - def _thread_safe_check_whether_cancellation_is_requested(self) -> bool: - cancellation_requested: bool = False - self.cancellation_flag_mutex.lockForRead() - cancellation_requested = self.cancellation_requested - self.cancellation_flag_mutex.unlock() - return cancellation_requested - - def _thread_safe_set_cancellation_requested_flag(self, flag_value: bool) -> None: - self.cancellation_flag_mutex.lockForWrite() - self.cancellation_requested = flag_value - self.cancellation_flag_mutex.unlock() + self.failed.emit(err) @staticmethod def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: From 2a687f41350f922ab86ca6bed3af14a82ac6861d Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Mon, 19 Jan 2026 21:44:05 +0100 Subject: [PATCH 33/88] Added descriptive comments about worker-object and QThread relationship and refactored batch timestamp generation into base class of workers --- .../all_input_states_generator_worker.py | 57 ++++++++++--------- .../cancellable_base_worker.py | 30 +++++----- .../qt_all_input_states_generator_dialog.py | 18 +----- .../qt_simulation_run_dialog.py | 10 +--- .../qt_simulation_run_json_export_dialog.py | 5 +- .../qt_simulation_run_json_import_dialog.py | 32 +++++++++-- .../simulation_run_json_export_worker.py | 51 +++++++++-------- .../simulation_run_json_import_worker.py | 34 ++++++----- 8 files changed, 120 insertions(+), 117 deletions(-) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index d16ebbf5..03c1e95a 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -22,27 +22,19 @@ class AllInputStatesGeneratorWorker(CancellableBaseWorker): def __init__(self, expected_input_state_size: int, batch_size: int): super().__init__(do_batches_require_ack=True) - - if expected_input_state_size < 0: - msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" - raise ValueError(msg) - - if batch_size < 1: - msg = f"Batch size must be larger than 0 but was actually {batch_size}" - raise ValueError(msg) - self.expected_input_state_size: Final[int] = expected_input_state_size self.batch_size: Final[int] = batch_size @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_generation(self) -> None: - n_states_to_generate: int = 2**self.expected_input_state_size - n_batches: int = n_states_to_generate // self.batch_size - batch_generation_start_time: float = time.perf_counter() - batch_generation_end_time: float = 0 - batch_generation_duration_in_seconds: float = 0 try: + AllInputStatesGeneratorWorker._validate_parameters(self.expected_input_state_size, self.batch_size) + n_states_to_generate: int = 2**self.expected_input_state_size + n_batches: int = n_states_to_generate // self.batch_size + + batch_start_timestamp: float = AllInputStatesGeneratorWorker._get_timestamp() + batch_generation_duration: float = 0 if self.wait_on_batch_processed_acknowledgement_condition is None: self.failed.emit(ValueError("Internal batch processed acknowledgement condition was not initialized")) return @@ -57,14 +49,14 @@ def start_generation(self) -> None: batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i ) - - batch_generation_end_time = time.perf_counter() - batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time - batch_generation_start_time = batch_generation_end_time - - self.batchCompleted.emit(batch_generation_duration_in_seconds, batch_data.copy()) - with QtCore.QMutexLocker(self.ack_flag_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_flag_mutex) + batch_generation_duration = ( + AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + self.batchCompleted.emit(batch_generation_duration, batch_data.copy()) + with QtCore.QMutexLocker(self.batch_ack_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. time.sleep(0.1) @@ -80,15 +72,26 @@ def start_generation(self) -> None: last_batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i ) - - batch_generation_end_time = time.perf_counter() - batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time - batch_generation_start_time = batch_generation_end_time - self.batchCompleted.emit(batch_generation_duration_in_seconds, last_batch_data) + batch_generation_duration = ( + AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + self.batchCompleted.emit(batch_generation_duration, last_batch_data) except Exception as err: self.failed.emit(err) self.finished.emit() + @staticmethod + def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: + if expected_input_state_size < 0: + msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" + raise ValueError(msg) + + if batch_size < 1: + msg = f"Batch size must be larger than 0 but was actually {batch_size}" + raise ValueError(msg) + @staticmethod def _generate_sim_run_model_for_input_state( expected_input_state_size: int, integer_defining_input_state: int diff --git a/python/mqt/syrec/simulation_view/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/cancellable_base_worker.py index 29f9e236..ecc7e6fc 100644 --- a/python/mqt/syrec/simulation_view/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/cancellable_base_worker.py @@ -8,7 +8,8 @@ from __future__ import annotations -from typing import Any, TypeVar +import time +from typing import Any, Final, TypeVar from PyQt6 import QtCore @@ -30,26 +31,10 @@ def __init__(self, do_batches_require_ack: bool): QtCore.QWaitCondition() if do_batches_require_ack else None ) - # TODO: Is this comment still required if we manually invoke the function instead of using signals? Check AI bots? - # In the cross thread communication between the main thread (rendering the GUI) and the worker thread we had the issue that if we define the slot function with a QtCore.pyqtSlot() decorator - # the main thread will not invoke said slot in the worker thread but we do not know exactly we since other signal->slot connections between the two threads function when being defined with - # corresponding decorators. Thus for now we define the slot without a decorator. - # - # One explanation, generated by an AI agent, was: - # The decisive moment is when the decorator runs, not when you later do moveToThread. - # At import time (define time) the worker instance already exists and lives in the GUI thread; the decorator therefore registers the slot in the GUI thread's meta-object. - # Afterwards you move the object to the worker thread, but the meta-object data that Qt uses to locate the slot stays where it was created - in the GUI thread. - # When the GUI thread later emits the signal, Qt again looks in the GUI thread's meta-object, finds the entry that was created by the decorator, and tries to invoke it. - # Because the slot entry is marked as "belonging to another thread" (the worker thread), Qt posts a queued meta-call to that thread … but the worker thread has no corresponding meta-object entry, so nothing is executed. - # If you remove the decorator the connection is handled purely in Python (Qt simply stores the callable); the queued connection then works, because Python callables are independent of the meta-object system. - # In short: - # - pyqtSlot must be executed after the object has been moved to the target thread, or - # - drop the decorator and rely on the automatic queued connection that PyQt already provides. def request_cancellation(self) -> None: self.set_cancellation_requested_flag(True) self.ack_batch_processed() - # Again we define the slot without the corresponding decorator, for further information we refer to the request_cancellation function. def ack_batch_processed(self) -> None: if self.batch_ack_mutex is not None and self.wait_on_batch_processed_acknowledgement_condition is not None: with QtCore.QMutexLocker(self.batch_ack_mutex): @@ -72,3 +57,14 @@ def are_list_of_batch_items_of_type(batch_data: Any, expected_batch_element_type return isinstance(batch_data, list) and all( isinstance(batch_item, expected_batch_element_type) for batch_item in batch_data ) + + @staticmethod + def _get_timestamp() -> float: + return time.perf_counter() + + @staticmethod + def _calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestamp: float) -> float: + batch_end_timestamp: Final[float] = CancellableBaseWorker._get_timestamp() + batch_duration = batch_end_timestamp - batch_start_timestamp + batch_start_timestamp = batch_end_timestamp + return batch_duration diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 42ac3889..585d0550 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -88,30 +88,16 @@ def start_generation( self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 1000 ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model - self.stop_processing_recv_input_state_batches = False - self.num_generated_input_states = 0 - self.total_input_state_generation_runtime_in_seconds = 0 - self.progress_text_lbl.setText("") - self.total_runtime_text_lbl.setText("") - - # TODO: Again try to refactor code such that constructor arguments are pass in start_generation call instead - try: - self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) - except ValueError as err: - self.err_text_lbl.setText(f"Error {err=}, {type(err)=} during initialization of input states generator!") - return # TODO: Validation that maximum value can actually be stored in progress bar maximum (should validation be performed in dialog or by caller?) self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(2**expected_input_state_size) self.progress_bar.setValue(0) + # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation + self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) - # Do not block the UI thread by the potentially long running operations of the worker a new thread is started (which also has its own event loop) - # and the worker operation moved to the latter. We also do not want to block the UI thread by executing the slots of said worker in the UI thread but - # instead want to simply send the events to the event queue of its thread thus the QueuedConnection between the signal (here the UI thread) and the receiver (worker thread) - # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). self.worker.batchCompleted.connect( self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection ) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index d64638db..c18e00f8 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -128,18 +128,10 @@ def start_simulations( self.simulation_run_progress_bar.setVisible(True) # self.simulation_run_total_runtime_timer.start(TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS) - + # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker = SimulationWorker(annotatable_quantum_computation, stop_at_first_output_state_mismatch) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) - - # TODO: It is recommended in the official documentation to mark slots explicitly via the QtCore.pyqtSlot() decorator: - # see https://doc.qt.io/qtforpython-6/tutorials/basictutorial/signals_and_slots.html#the-slot-class - - # Do not block the UI thread by the potentially long running operations of the worker a new thread is started (which also has its own event loop) - # and the worker operation moved to the latter. We also do not want to block the UI thread by executing the slots of said worker in the UI thread but - # instead want to simply send the events to the event queue of its thread thus the QueuedConnection between the signal (here the UI thread) and the receiver (worker thread) - # needs to be defined as a QueuedConnection (QtCore.Qt.ConnectionType.QueuedConnection). self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) self.worker.allSimulationsDone.connect( self._handle_all_simulation_runs_done, QtCore.Qt.ConnectionType.QueuedConnection diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py index e075dfba..ef9fe39d 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -100,7 +100,8 @@ def start_export( ) self.progress_bar.setMaximum(num_sim_runs_to_export) - self.worker = SimulationRunJsonExportWorker() # type: ignore[no-untyped-call] + # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation + self.worker = SimulationRunJsonExportWorker(export_location, sim_runs_to_export, batch_size) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) self.worker.batchCompleted.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) @@ -108,7 +109,7 @@ def start_export( self.worker.failed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.started.connect( - lambda: self.worker.start_export(export_location, sim_runs_to_export, batch_size), + self.worker.start_export, QtCore.Qt.ConnectionType.QueuedConnection, ) self.worker_thread.finished.connect(self.worker_thread.deleteLater) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py index 690e0d08..7a5ec1d6 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -98,22 +98,46 @@ def start_generation( IMPORT_ORIGIN_INFO_TEXT_FORMAT.format(path_to_json_file=str(path_to_json_file)) ) - # TODO: Why can we not use a lambda to pass the arguments to the start_import call of the worker instance - # instead of passing them as constructor arguments that are otherwise not needed in the instance. - # Compare with the SimulationRunJsonExportWorker (that could contain a deadlock) since when we were using a - # lambda, the _handle_imported_sim_run_batch was not called. + # Some helpful links are the official QThread documentation but some helpful explanaitions were also found in: + # - https://www.haccks.com/posts/how-to-use-qthread-correctly-p1/ + # We are creating a worker object that will perform a long running operation in the future with the worker + # instance being a member of the dialog class/object. Since the worker will define slots for the cancellation of + # the long running operation that it executes but also emits signals, said worker needs to be implemented as a QObject that will run its own event loop + # instead of subclassing QThread which executes its slots in the thread in which the QThread was created and might not execute its own event loop. self.worker = SimulationRunJsonImportWorker(path_to_json_file, expected_input_state_size, batch_size) + # Create a new QThread that manages one system thread WITHOUT BEING AN ACTUAL THREAD (see: https://doc.qt.io/qtforpython-6/PySide6/QtCore/QThread.html#detailed-description) self.worker_thread = QtCore.QThread() + # We are now modifying the thread affinity (https://doc.qt.io/qt-6/qobject.html#thread-affinity) of the worker object to the worker thread. + # This will control in which thread the received events of the worker are processed. The worker instance is still available in the dialog + # so the latter can still access member variables, etc. of the former. self.worker.moveToThread(self.worker_thread) + # If the worker has completed a batch in its long running operation then it will emit a corresponding signal that should be processed by the dialog instance. + # Since we have changed the thread affinity of the worker, the worker signal -> dialog slot connection needs to be marked as a queued connection so that the + # signal of the worker will enqueue an entry into the event queue of the dialog and then continue its long running operation in the worker thread. Since the + # worker and main thread do not share the same event queue, the slot called in the dialog is then executed in the main thread. + # + # At runtime Qt could decide at runtime whether a direct or queued connection is required based on the thread affinity between the connected signal and slot but + # we try to mark this behaviour explicitly by defining the signal-slot connection as a queued connection. self.worker.batchCompleted.connect( self._handle_imported_sim_run_batch, QtCore.Qt.ConnectionType.QueuedConnection ) + # The worker thread executing the long running worker operation will still continue running after the 'finished' signal of the worker was received thus + # we need to manually handle the correct cancellation of the worker thread self.worker.finished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) + # Assuming that we are correctly catching all errors of the long running worker operation in the function execution said operation (executed in the worker thread) + # the worker will emit a signal containing the caught error with the worker thread still running thus again we need to manually handle its cancellation self.worker.failed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) + # We initially tried to move the constructor parameters of the SimulationRunJsonImportWorker to the function executing the long running operation by using a lambda + # that will trigger the latter but since python lambdas seemingly do not have thread affinity (https://stackoverflow.com/a/28626472) the lambda is executed in the main + # thread and thus the long running operation would be executed in the main thread blocking the GUI and potentially causing thread starvation if generated batches need to + # be acknowledged. self.worker_thread.started.connect(self.worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) + # Since we are manually triggering the worker thread shutdown, after the worker thread has finished the associated QThread should be deleted self.worker_thread.finished.connect(self.worker_thread.deleteLater) + # Additionally we 'clean' up the worker and worker thread instances (by setting them to None) after the worker thread has finished self.worker_thread.finished.connect(self._reset_workers) + # Only this call will actually start a new thread self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancellation_button_enable_state(True) diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py index acb39a3d..13a5fc99 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py @@ -9,8 +9,7 @@ from __future__ import annotations import json -import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final from PyQt6 import QtCore @@ -23,31 +22,30 @@ class SimulationRunJsonExportWorker(CancellableBaseWorker): - def __init__(self): + def __init__( + self, path_to_json_file: Path, simulation_runs_to_export: Iterable[SimulationRunModel], export_batch_size: int + ): super().__init__(do_batches_require_ack=False) + self.path_to_json_file: Final[Path] = path_to_json_file + self.simulation_runs_to_export: Iterable[SimulationRunModel] = simulation_runs_to_export + self.export_batch_size: Final[int] = export_batch_size + # TODO: Pretty printing - # TODO: This untyped decorator should not be invocable via a signal? @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def start_export( - self, - path_to_json_file: Path, - simulation_runs_to_export: Iterable[SimulationRunModel], - export_batch_size: int, - ) -> None: - if export_batch_size < 1: + def start_export(self) -> None: + if self.export_batch_size < 1: return n_generated_batches: int = 0 try: batch_idx: int = 0 - with path_to_json_file.open("w", encoding="utf-8") as file: + with self.path_to_json_file.open("w", encoding="ascii") as file: # file.write("{\n\t\"simulationRuns\": [\n") file.write('{"simulationRuns":[') - batch_export_start_time: float = time.perf_counter() - batch_export_end_time: float = 0 - batch_export_duration: float = 0 - for sim_run in simulation_runs_to_export: + batch_start_timestamp: float = SimulationRunJsonExportWorker._get_timestamp() + batch_generation_duration: float = 0 + for sim_run in self.simulation_runs_to_export: if self.is_cancellation_requested(): break @@ -60,20 +58,25 @@ def start_export( file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json)) batch_idx += 1 - if batch_idx == export_batch_size: - batch_export_end_time = time.perf_counter() - batch_export_duration = batch_export_end_time - batch_export_start_time - batch_export_start_time = batch_export_end_time - self.batchCompleted.emit(batch_export_duration, export_batch_size) + if batch_idx == self.export_batch_size: + batch_generation_duration = ( + SimulationRunJsonExportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + self.batchCompleted.emit(batch_generation_duration, self.export_batch_size) batch_idx = 0 n_generated_batches += 1 # file.write("\n\t]\n}") file.write("]}") if batch_idx > 0 and not self.is_cancellation_requested(): - batch_export_end_time = time.perf_counter() - batch_export_duration = batch_export_end_time - batch_export_start_time - self.batchCompleted.emit(batch_export_duration, batch_idx) + batch_generation_duration = ( + SimulationRunJsonExportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + self.batchCompleted.emit(batch_generation_duration, batch_idx) self.finished.emit() except Exception as err: self.failed.emit(err) diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py index 89c308d6..455e7563 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py @@ -9,8 +9,6 @@ from __future__ import annotations import time - -# import pthread from typing import TYPE_CHECKING, Any, Final # TODO: Correctly configure third-party package for mypy @@ -41,9 +39,6 @@ def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batc @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_import(self) -> None: - batch_generation_end_time: float = 0 - batch_generation_duration_in_seconds: float = 0 - try: SimulationRunJsonImportWorker._validate_parameters(self.expected_input_state_size, self.batch_size) if self.wait_on_batch_processed_acknowledgement_condition is None: @@ -55,7 +50,8 @@ def start_import(self) -> None: # Reading bytes instead of strings leads to better parser performance with self.path_to_json_file.open("rb") as file: - batch_generation_start_time: float = time.perf_counter() + batch_start_timestamp: float = SimulationRunJsonImportWorker._get_timestamp() + batch_generation_duration: float = 0 # The json parser starts at the first element matching the prefix which in our case starts at an element with key 'simulationRuns' that is expected # to be a property of the top level element (i.e. the path to the 'simulationRuns' element is relative to the top level element). # Additionally, with the postfix '.item', only the entries of a JSON array are processed. If the 'simulationRuns' entry value is no @@ -76,13 +72,14 @@ def start_import(self) -> None: ) batch_idx += 1 if batch_idx == self.batch_size: - batch_generation_end_time = time.perf_counter() - batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time - batch_generation_start_time = batch_generation_end_time - - self.batchCompleted.emit(batch_generation_duration_in_seconds, batch_data.copy()) - with QtCore.QMutexLocker(self.ack_flag_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.ack_flag_mutex) + batch_generation_duration = ( + SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + self.batchCompleted.emit(batch_generation_duration, batch_data.copy()) + with QtCore.QMutexLocker(self.batch_ack_mutex): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. time.sleep(0.1) @@ -93,11 +90,12 @@ def start_import(self) -> None: if batch_idx != 0 and not self.is_cancellation_requested(): del batch_data[batch_idx:] - batch_generation_end_time = time.perf_counter() - batch_generation_duration_in_seconds = batch_generation_end_time - batch_generation_start_time - batch_generation_start_time = batch_generation_end_time - - self.batchCompleted.emit(batch_generation_duration_in_seconds, batch_data) + batch_generation_duration = ( + SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + self.batchCompleted.emit(batch_generation_duration, batch_data) self.finished.emit() except Exception as err: self.failed.emit(err) From 7cd18b8fa66b8191f6ec372654556b19db42cbb8 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 20 Jan 2026 17:49:39 +0100 Subject: [PATCH 34/88] Refactored shared logging and message box functionality to helper classes. Fixed QWaitCondition race condition in cancellable base worker class and AllInputStatesGeneratorWorker --- python/mqt/syrec/logger_utils.py | 57 ++++ python/mqt/syrec/message_box_utils.py | 141 +++++++++ .../all_input_states_generator_worker.py | 15 +- .../cancellable_base_worker.py | 26 +- .../dialogs/base_progress_dialog.py | 198 +++++++++++++ .../qt_all_input_states_generator_dialog.py | 271 ++++++++---------- .../qt_simulation_run_json_export_dialog.py | 10 +- .../qt_simulation_run_json_import_dialog.py | 10 +- .../simulation_run_json_export_worker.py | 3 +- .../simulation_run_json_import_worker.py | 2 +- python/mqt/syrec/syrec_editor.py | 3 + 11 files changed, 557 insertions(+), 179 deletions(-) create mode 100644 python/mqt/syrec/logger_utils.py create mode 100644 python/mqt/syrec/message_box_utils.py create mode 100644 python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py diff --git a/python/mqt/syrec/logger_utils.py b/python/mqt/syrec/logger_utils.py new file mode 100644 index 00000000..2a54c00b --- /dev/null +++ b/python/mqt/syrec/logger_utils.py @@ -0,0 +1,57 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +import logging +from typing import Final + +DEFAULT_LOGGER_NAME: Final[str] = "syrec-console-logger" + + +def configure_default_console_logger() -> None: + # For supported log message formats (see https://docs.python.org/3/library/logging.html#formatter-objects) + logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s-%(levelname)s-[%(filename)s:%(lineno)s - %(funcName)20s()]-%(message)s", + datefmt="%H:%M:%S", + ) + + +def log_debug_to_console( + info_msg: str, num_additionally_skipped_stack_frames_starting_from_caller_function: int = 0 +) -> None: + logger = logging.getLogger(DEFAULT_LOGGER_NAME) + # We do not want to log the origin of the helper function but of the caller of the function itself. + # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + logger.debug(msg=info_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) + + +def log_info_to_console( + info_msg: str, num_additionally_skipped_stack_frames_starting_from_caller_function: int = 0 +) -> None: + logger = logging.getLogger(DEFAULT_LOGGER_NAME) + # We do not want to log the origin of the helper function but of the caller of the function itself. + # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + logger.info(msg=info_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) + + +def log_warning_to_console( + warn_msg: str, num_additionally_skipped_stack_frames_starting_from_caller_function: int = 0 +) -> None: + logger = logging.getLogger(DEFAULT_LOGGER_NAME) + # We do not want to log the origin of the helper function but of the caller of the function itself. + # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + logger.warning(msg=warn_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) + + +def log_error_to_console( + err_msg: str, num_additionally_skipped_stack_frames_starting_from_caller_function: int = 0 +) -> None: + logger = logging.getLogger(DEFAULT_LOGGER_NAME) + # We do not want to log the origin of the helper function but of the caller of the function itself. + # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + logger.error(msg=err_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) diff --git a/python/mqt/syrec/message_box_utils.py b/python/mqt/syrec/message_box_utils.py new file mode 100644 index 00000000..b040173c --- /dev/null +++ b/python/mqt/syrec/message_box_utils.py @@ -0,0 +1,141 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from enum import Enum + +from PyQt6 import QtWidgets +from typing_extensions import assert_never + +from .logger_utils import log_debug_to_console, log_error_to_console, log_info_to_console, log_warning_to_console + + +class MessageBoxType(Enum): + QUESTION = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + + +def show_optionally_cancellable_notification( + message_box_type: MessageBoxType, + message_box_parent: QtWidgets.QWidget, + message_box_title: str, + message_box_content: str, + is_cancellable: bool, + log_contents: bool = True, +) -> bool: + clicked_message_box_button: QtWidgets.QMessageBox.StandardButton | None = None + match message_box_type: + case MessageBoxType.QUESTION: + if log_contents: + log_debug_to_console( + f"{message_box_title} - {message_box_content}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + + clicked_message_box_button = QtWidgets.QMessageBox.question( + message_box_parent, + message_box_title, + message_box_content, + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + ) + return _check_whether_message_box_cancellation_was_clicked( + message_box_type, is_cancellable, clicked_message_box_button + ) + case MessageBoxType.INFO: + if log_contents: + log_info_to_console( + f"{message_box_title} - {message_box_content}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + + clicked_message_box_button = QtWidgets.QMessageBox.information( + message_box_parent, + message_box_title, + message_box_content, + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + ) + return _check_whether_message_box_cancellation_was_clicked( + message_box_type, is_cancellable, clicked_message_box_button + ) + case MessageBoxType.WARNING: + if log_contents: + log_warning_to_console( + f"{message_box_title} - {message_box_content}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + + clicked_message_box_button = QtWidgets.QMessageBox.warning( + message_box_parent, + message_box_title, + message_box_content, + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + ) + return _check_whether_message_box_cancellation_was_clicked( + message_box_type, is_cancellable, clicked_message_box_button + ) + case MessageBoxType.ERROR: + if log_contents: + log_error_to_console( + f"{message_box_title} - {message_box_content}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + + clicked_message_box_button = QtWidgets.QMessageBox.critical( + message_box_parent, + message_box_title, + message_box_content, + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + ) + return _check_whether_message_box_cancellation_was_clicked( + message_box_type, is_cancellable, clicked_message_box_button + ) + case _: + # Added guard to handle new message box types + assert_never(message_box_type) + + +def _get_buttons_for_message_box_type( + message_box_type: MessageBoxType, is_cancellable: bool +) -> QtWidgets.QMessageBox.StandardButton: + if message_box_type == MessageBoxType.QUESTION: + return ( + (QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No) + if is_cancellable + else QtWidgets.QMessageBox.StandardButton.Yes + ) + return ( + (QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel) + if is_cancellable + else QtWidgets.QMessageBox.StandardButton.Ok + ) + + +# Get the default button that will be pressed if the user presses ENTER in the open message box +def _get_default_button_for_message_box_type( + message_box_type: MessageBoxType, is_cancellable: bool +) -> QtWidgets.QMessageBox.StandardButton: + if message_box_type == MessageBoxType.QUESTION: + return QtWidgets.QMessageBox.StandardButton.No if is_cancellable else QtWidgets.QMessageBox.StandardButton.Yes + return QtWidgets.QMessageBox.StandardButton.Cancel if is_cancellable else QtWidgets.QMessageBox.StandardButton.Ok + + +def _check_whether_message_box_cancellation_was_clicked( + message_box_type: MessageBoxType, + is_cancellable: bool, + clicked_message_box_button: QtWidgets.QMessageBox.StandardButton, +) -> bool: + if message_box_type == MessageBoxType.QUESTION: + return clicked_message_box_button == QtWidgets.QMessageBox.StandardButton.Yes if is_cancellable else False + return clicked_message_box_button == QtWidgets.QMessageBox.StandardButton.Ok if is_cancellable else False diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 03c1e95a..30bc4511 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -15,6 +15,7 @@ from mqt import syrec +from ..logger_utils import log_error_to_console from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel @@ -27,7 +28,6 @@ def __init__(self, expected_input_state_size: int, batch_size: int): @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_generation(self) -> None: - try: AllInputStatesGeneratorWorker._validate_parameters(self.expected_input_state_size, self.batch_size) n_states_to_generate: int = 2**self.expected_input_state_size @@ -56,11 +56,12 @@ def start_generation(self) -> None: ) self.batchCompleted.emit(batch_generation_duration, batch_data.copy()) with QtCore.QMutexLocker(self.batch_ack_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) + if not self.is_cancellation_requested(): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) + # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. time.sleep(0.1) - first_integer_encoding_first_state_of_batch += self.batch_size for i in range(self.batch_size): batch_data[i] = None @@ -78,9 +79,11 @@ def start_generation(self) -> None: ) ) self.batchCompleted.emit(batch_generation_duration, last_batch_data) - except Exception as err: - self.failed.emit(err) - self.finished.emit() + self.finished.emit(self.cancellation_requested) + except Exception as error: + error_msg: Final[str] = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" + log_error_to_console(error_msg, num_additionally_skipped_stack_frames_starting_from_caller_function=0) + self.failed.emit(error) @staticmethod def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: diff --git a/python/mqt/syrec/simulation_view/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/cancellable_base_worker.py index ecc7e6fc..09bd1659 100644 --- a/python/mqt/syrec/simulation_view/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/cancellable_base_worker.py @@ -18,7 +18,16 @@ class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] batch_completed = QtCore.pyqtSignal(float, object, name="batchCompleted") - finished = QtCore.pyqtSignal(name="finished") + # While the cancellation operation is assumed to request the cancellation of the internal worker + # as well as the worker_thread, due to the Qt event loop the QThread.finished signal is received + # after the finished signal of the worker, i.e. the order of events in case of a cancellation or error will be: + # [Optionally error thrown in worker] -> Cancellation requested -> Finished -> QThread.finished + # + # This could lead to the worker attempting to perform a double shutdown of the worker/worker_thread, + # assuming that the cancel/error handler will request the worker shutdown, when the finished slot of the worker + # perform the shutdown of the worker. Thus we introduce an additional flag in the finished signal to perform a + # conditional shutdown of the worker in the slot that is connected to the finished signal. + finished = QtCore.pyqtSignal(bool, name="finished") failed = QtCore.pyqtSignal(Exception, name="failed") def __init__(self, do_batches_require_ack: bool): @@ -32,13 +41,21 @@ def __init__(self, do_batches_require_ack: bool): ) def request_cancellation(self) -> None: - self.set_cancellation_requested_flag(True) - self.ack_batch_processed() + # Since the wait of the QWaitCondition can only be 'cancelled' by either a wakeX call or by providing a timeout value with the + # latter probably leading to a while-loop construct repeatedly performing temporary waits (until the timer elapses), the programmer + # needs to make sure that the cancellation operation will both set the cancellation flag as well as waking the QWaitCondition in a single + # operation (i.e. while locking the batch_ack_mutex) + if self.batch_ack_mutex is not None and self.wait_on_batch_processed_acknowledgement_condition: + with QtCore.QMutexLocker(self.batch_ack_mutex): + self.set_cancellation_requested_flag(True) + self.wait_on_batch_processed_acknowledgement_condition.wakeAll() + else: + self.set_cancellation_requested_flag(True) def ack_batch_processed(self) -> None: if self.batch_ack_mutex is not None and self.wait_on_batch_processed_acknowledgement_condition is not None: with QtCore.QMutexLocker(self.batch_ack_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wakeOne() + self.wait_on_batch_processed_acknowledgement_condition.wakeAll() def is_cancellation_requested(self) -> bool: cancellation_requested: bool = False @@ -62,6 +79,7 @@ def are_list_of_batch_items_of_type(batch_data: Any, expected_batch_element_type def _get_timestamp() -> float: return time.perf_counter() + # TODO: Is this correct @staticmethod def _calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestamp: float) -> float: batch_end_timestamp: Final[float] = CancellableBaseWorker._get_timestamp() diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py new file mode 100644 index 00000000..12a176ed --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -0,0 +1,198 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import Final, Generic, TypeVar + +from PyQt6 import QtCore, QtWidgets + +from ...logger_utils import log_error_to_console, log_info_to_console +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ..cancellable_base_worker import CancellableBaseWorker + +T = TypeVar("T", bound=CancellableBaseWorker) + +DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT: Final[str] = "Total runtime [in seconds]: {total_runtime_in_seconds:f}" +DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( + "Batch of {n_batch_elements:d} completed! Runtime [in seconds]: {batch_duration_in_seconds:f}" +) + + +class BaseProgressDialog(QtWidgets.QDialog, Generic[T]): # type: ignore[misc] + def __init__( + self, + parent: QtWidgets.QWidget, + dialog_title: str, + dialog_size: tuple[int, int] = (600, 600), + optional_progress_bar_text_format: str | None = None, + create_default_layout: bool = True, + ): + super().__init__(parent) + + self.worker_thread: QtCore.QThread | None = None + self.worker: T | None = None + self.stop_processing_recv_batches: bool = False + self.total_runtime_in_seconds: float = 0 + + self.setModal(True) + self.setSizeGripEnabled(True) + self.setWindowTitle(dialog_title) + + dialog_x_pos: Final[int] = 0 + dialog_y_pos: Final[int] = 0 + dialog_width: Final[int] = dialog_size[0] + dialog_height: Final[int] = dialog_size[1] + self.setGeometry(dialog_x_pos, dialog_y_pos, dialog_width, dialog_height) + + layout = QtWidgets.QVBoxLayout() + self.title_lbl = QtWidgets.QLabel() + self.title_lbl.setStyleSheet("QLabel { font-size : 16px; font-weight: bold; }") + self.title_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.progress_info_text_lbl = QtWidgets.QLabel() + self.progress_info_text_lbl.setStyleSheet("QLabel { color : gray; }") + self.progress_info_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.error_text_lbl = QtWidgets.QLabel() + self.error_text_lbl.setStyleSheet("QLabel { color : red; }") + self.error_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.progress_bar: QtWidgets.QProgressBar | None = None + if optional_progress_bar_text_format is not None: + self.progress_bar = QtWidgets.QProgressBar() + # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format + # self.progress_bar.setFormat("Generated %v out of %m input states") + self.progress_bar.setFormat(optional_progress_bar_text_format) + self.progress_bar.setFormat(optional_progress_bar_text_format) + self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.total_runtime_info_text_lbl = QtWidgets.QLabel() + self.total_runtime_info_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.dialog_button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel + ) + self.dialog_button_box.setCenterButtons(True) + self._change_dialog_ok_button_enable_state(False) + self._change_dialog_cancel_button_enable_state(False) + # TODO: Currently the user is responsible for hooking up the signals of the dialog_button_box + + if create_default_layout: + layout.addWidget(self.title_lbl) + layout.addWidget(self.progress_info_text_lbl) + layout.addWidget(self.error_text_lbl) + layout.addStretch() + layout.addWidget(self.progress_bar) + layout.addWidget(self.total_runtime_info_text_lbl) + layout.addWidget(self.dialog_button_box) + self.setLayout(layout) + + def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: + self.progress_info_text_lbl.setText( + DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT.format( + n_batch_elements=n_batch_elements, batch_duration_in_seconds=batch_duration_in_seconds + ) + ) + + def _accumulate_and_update_total_runtime(self, batch_runtime_in_seconds: float) -> None: + if batch_runtime_in_seconds < 0: + return + + self.total_runtime_in_seconds += batch_runtime_in_seconds + self.total_runtime_info_text_lbl.setText( + DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT.format(total_runtime_in_seconds=self.total_runtime_in_seconds) + ) + + def _shutdown_worker_thread_and_await_completion(self) -> None: + if self.worker_thread is None: + return + + log_info_to_console( + "Shutting down worker thread!", num_additionally_skipped_stack_frames_starting_from_caller_function=1 + ) + self.worker_thread.quit() + log_info_to_console( + "Waiting on worker thread completion...", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + self.worker_thread.wait() + log_info_to_console( + "Worker thread finished!", num_additionally_skipped_stack_frames_starting_from_caller_function=1 + ) + self.progress_info_text_lbl.setText("Worker thread finished!") + + def _request_worker_cancellation(self) -> None: + self.stop_processing_recv_batches = True + self.progress_info_text_lbl.setText("Requesting cancellation of long running worker!") + if self.worker is not None: + log_info_to_console( + "Requesting cancellation of long running worker", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + self.worker.request_cancellation() + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) + + def _change_dialog_cancel_button_enable_state(self, should_button_be_enabled: bool) -> None: + BaseProgressDialog._change_dialog_button_enable_state( + self.dialog_button_box, + QtWidgets.QDialogButtonBox.StandardButton.Cancel, + should_button_be_enabled, + btn_not_found_notification_parent=self, + ) + + def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: + BaseProgressDialog._change_dialog_button_enable_state( + self.dialog_button_box, + QtWidgets.QDialogButtonBox.StandardButton.Ok, + should_button_be_enabled, + btn_not_found_notification_parent=self, + ) + + def _update_displayed_error_text( + self, + error: Exception | str, + log_error: bool = True, + num_additionally_skipped_stack_frames_starting_from_this_function: int = 0, + ) -> None: + err_msg: Final[str] = BaseProgressDialog._stringify_error(error) if isinstance(error, Exception) else error + if log_error: + log_error_to_console( + err_msg, + num_additionally_skipped_stack_frames_starting_from_caller_function=num_additionally_skipped_stack_frames_starting_from_this_function, + ) + self.error_text_lbl.setText(err_msg) + + @staticmethod + def _change_dialog_button_enable_state( + dialog_button_box: QtWidgets.QDialogButtonBox, + to_be_modified_button: QtWidgets.QDialogButtonBox.StandardButton, + should_button_be_enabled: bool, + btn_not_found_notification_parent: QtWidgets.QWidget, + ) -> None: + dialog_button: QtWidgets.QPushButton | None = dialog_button_box.button(to_be_modified_button) + + if dialog_button is not None: + dialog_button.setEnabled(should_button_be_enabled) + else: + # TODO: Log button name and not numeric value + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=btn_not_found_notification_parent, + message_box_title="Internal error", + message_box_content=f"Could not find {to_be_modified_button.name} button of dialog, this should not happen", + is_cancellable=False, + ) + + def _reset_workers(self) -> None: + self.worker_thread = None + self.worker = None + + @staticmethod + def _stringify_error(error: Exception) -> str: + return f"Error during long running worker operation! Reason: {type(error)=}, {error=}" diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 585d0550..c73df953 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -10,89 +10,53 @@ from typing import TYPE_CHECKING, Final -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore if TYPE_CHECKING: - from PyQt6 import QtGui + from PyQt6 import QtGui, QtWidgets from .qt_simulation_run_model import QtSimulationRunModel +from ..logger_utils import log_error_to_console, log_info_to_console +from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification from .all_input_states_generator_worker import AllInputStatesGeneratorWorker +from .dialogs.base_progress_dialog import BaseProgressDialog from .qt_simulation_run_model import SimulationRunModel -TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = ( - "Total runtime for input states generation [in seconds]: {total_runtime_in_sec:f}" -) -GENERATED_INPUT_STATE_BATCH_PROGRESS_INFO_TEXT_FORMAT: Final[str] = ( - "Generated batch of input states (runtime [in sec]: {batch_generation_duration_in_seconds:f}!" -) - -class AllInputStatesGeneratorDialog(QtWidgets.QDialog): # type: ignore[misc] +class AllInputStatesGeneratorDialog(BaseProgressDialog[AllInputStatesGeneratorWorker]): def __init__(self, parent: QtWidgets.QWidget): - super().__init__(parent) - + super().__init__( + parent, + dialog_title="Generating simulation runs...", + dialog_size=(400, 200), + optional_progress_bar_text_format="Generated %v out of %m input states", + ) self.shared_simulation_runs_model: QtSimulationRunModel | None = None - self.worker_thread: QtCore.QThread | None = None - self.worker: AllInputStatesGeneratorWorker | None = None - self.num_generated_input_states: int = 0 - self.stop_processing_recv_input_state_batches: bool = False - self.total_input_state_generation_runtime_in_seconds: float = 0 - - self.setModal(True) - self.setSizeGripEnabled(True) - self.setWindowTitle("Generating simulation runs...") - left = 0 - top = 0 - width = 400 - height = 200 - self.setGeometry(left, top, width, height) - - main_layout = QtWidgets.QVBoxLayout() - self.progress_bar = QtWidgets.QProgressBar() - # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format - self.progress_bar.setFormat("Generated %v out of %m input states") - self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - self.err_text_lbl = QtWidgets.QLabel("") - self.err_text_lbl.setStyleSheet("QLabel { color : red; }") - self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - self.progress_text_lbl = QtWidgets.QLabel("") - self.progress_text_lbl.setStyleSheet("QLabel { color : gray; }") - self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - self.dialog_button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) - self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.rejected.connect(self._handle_input_state_generation_cancel_button_click) - self.dialog_button_box.accepted.connect(self.accept) - self._change_dialog_ok_button_enable_state(False) - self._change_dialog_cancellation_button_enable_state(False) - - main_layout.addWidget(self.progress_bar) - main_layout.addWidget(self.progress_text_lbl) - main_layout.addWidget(self.err_text_lbl) - - main_layout.addStretch() - self.total_runtime_text_lbl = QtWidgets.QLabel("") - self.total_runtime_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - main_layout.addWidget(self.total_runtime_text_lbl) - main_layout.addWidget(self.dialog_button_box) - self.setLayout(main_layout) + self.dialog_button_box.accepted.connect(self.accept) + self.dialog_button_box.rejected.connect(self._handle_input_state_generation_cancel_button_click) def start_generation( self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 1000 ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model + self.title_lbl.setText(f"Generating simulation runs with batch size {batch_size}!") # TODO: Validation that maximum value can actually be stored in progress bar maximum (should validation be performed in dialog or by caller?) - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(2**expected_input_state_size) - self.progress_bar.setValue(0) + if self.progress_bar is not None: + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(2**expected_input_state_size) + self.progress_bar.setValue(0) + else: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Required widget not found", + message_box_content="Input states generator was initialized without a progress bar! This should not happen.", + is_cancellable=False, + ) # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) @@ -112,137 +76,128 @@ def start_generation( self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancellation_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(True) def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing - if self.worker is None or self._handle_input_state_generation_cancel_button_click(): + if self._handle_input_state_generation_cancel_button_click(): self.accept() else: event.ignore() @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] def _handle_input_state_generator_failure(self, err: Exception) -> None: - self.progress_text_lbl.setText("") - self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during generation of input states") - if self.shared_simulation_runs_model is not None: - self.shared_simulation_runs_model.delete_all_simulation_run_models() - else: - QtWidgets.QMessageBox.critical( - self, - "Internal state error!", - "Shared simulation runs model was not initialized during handling of input state generator failure!\nThis should not happen.", - buttons=QtWidgets.QMessageBox.StandardButton.Ok, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - self._request_worker_cancellation() - self._await_worker_thread_completion() + self._handle_non_recoverable_error(err) @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] def _handle_generated_input_state_batch( self, batch_generation_duration_in_seconds: float, batch_data: object ) -> None: - if self.stop_processing_recv_input_state_batches: + if self.stop_processing_recv_batches: return if not AllInputStatesGeneratorWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunModel): - # TODO: Error logging? - # TODO: Cancel worker? + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.INFO, + message_box_parent=self, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be a list of SimulationRunModels but was actually {type(batch_data)}. Skipping batch!", + is_cancellable=False, + ) if self.worker is not None: self.worker.ack_batch_processed() return - self.progress_text_lbl.setText( - GENERATED_INPUT_STATE_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( - batch_generation_duration_in_seconds=batch_generation_duration_in_seconds - ) - ) generated_simulation_run_models: Final[list[SimulationRunModel]] = batch_data # type: ignore[assignment] + self._update_progress_text_with_batch_info( + len(generated_simulation_run_models), batch_generation_duration_in_seconds + ) + if self.shared_simulation_runs_model is None: - QtWidgets.QMessageBox.critical( - self, - "Internal state error!", - "Shared simulation runs model was not initialized during handling of generated input state batch!\nThis should not happen.", - buttons=QtWidgets.QMessageBox.StandardButton.Ok, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - self._request_worker_cancellation() + log_error_to_console("Shared simulation runs model was not initialized during handling of batch!") + self._handle_non_recoverable_error(None) + return + + try: + self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) + except Exception as sim_run_model_err: + self._handle_non_recoverable_error(sim_run_model_err) return - # TODO: Error handling - # TODO: Use delayed processing to reduce "laggy"/almost frozen GUI - self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) if self.worker is not None: self.worker.ack_batch_processed() - self.total_input_state_generation_runtime_in_seconds += batch_generation_duration_in_seconds - self.total_runtime_text_lbl.setText( - TOTAL_RUNTIME_TEXT_FORMAT.format(total_runtime_in_sec=self.total_input_state_generation_runtime_in_seconds) - ) + self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) self.num_generated_input_states += len(generated_simulation_run_models) - self.progress_bar.setValue(self.num_generated_input_states) - self.progress_text_lbl.setText("") + if self.progress_bar is not None: + self.progress_bar.setValue(self.num_generated_input_states) + self.progress_info_text_lbl.setText("") - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def _handle_input_state_generator_finished(self) -> None: - self.progress_text_lbl.setText("Input state generator finished!") - self.progress_bar.setVisible(False) - self._request_worker_cancellation() - self._await_worker_thread_completion() - - self._change_dialog_ok_button_enable_state(True) - self._change_dialog_cancellation_button_enable_state(False) - - def _request_worker_cancellation(self) -> None: - self.stop_processing_recv_input_state_batches = True - self.progress_text_lbl.setText("Requesting cancellation of input state generator!") - if self.worker is not None: - self.worker.request_cancellation() - self._change_dialog_cancellation_button_enable_state(False) + @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] + def _handle_input_state_generator_finished(self, was_cancellation_requested: bool) -> None: + self.progress_info_text_lbl.setText("Input state generator finished!") + if self.progress_bar is not None: + self.progress_bar.setVisible(False) + + if not was_cancellation_requested: + if self.worker is not None: + self._request_worker_cancellation() + if self.worker_thread is not None: + self._shutdown_worker_thread_and_await_completion() + + self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_input_state_generation_cancel_button_click(self) -> bool: - clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( - self, - "Cancellation of generation of input states requested!", - "Are you sure that you want to stop the generation of the input states? This will cause the deletion of all already generated input states.", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) + if self.worker is None: + return True - if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: - self._request_worker_cancellation() - if self.shared_simulation_runs_model is not None: - self.shared_simulation_runs_model.delete_all_simulation_run_models() + if show_optionally_cancellable_notification( + message_box_type=MessageBoxType.QUESTION, + message_box_parent=self, + message_box_title="Cancellation of generation of input states requested!", + message_box_content="Are you sure that you want to stop the generation of the input states? This will cause the deletion of all already generated input states.", + is_cancellable=True, + log_contents=False, + ): + log_info_to_console( + "Cancellation of input state generation requested!", + num_additionally_skipped_stack_frames_starting_from_caller_function=0, + ) + self._handle_non_recoverable_error(None) return True return False - def _await_worker_thread_completion(self) -> None: - if self.worker_thread is not None: - self.worker_thread.quit() - self.worker_thread.wait() - self.progress_text_lbl.setText("Input state generator thread finished!") - - def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) - - if dialog_cancel_button is None: - return - - dialog_cancel_button.setEnabled(should_button_be_enabled) - - def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_ok_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Ok - ) + def _handle_non_recoverable_error(self, err: Exception | None) -> None: + self.progress_info_text_lbl.setText("") + if err is not None: + # We want to log the source of the error as close as possible to the origin of the actual error thus we need to skip a few stack frames + # to determine the "source" stack frame. The skip stack frames would be (read from left to right with the leftmost stackframe being at the + # lowest level in the stacktrace): logger (std) -> logger_utils (custom) -> update_displayed_error_text (custom) -> the current function. + self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) - if dialog_ok_button is None: - return - - dialog_ok_button.setEnabled(should_button_be_enabled) + if self.shared_simulation_runs_model is not None: + try: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + except Exception: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Internal error!", + message_box_content="Failed to delete all simulation run models during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", + is_cancellable=False, + ) + else: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Internal state error!", + message_box_content="Shared simulation runs model was not initialized during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", + is_cancellable=False, + ) - def _reset_workers(self) -> None: - self.worker_thread = None - self.worker = None + if self.worker is not None: + self._request_worker_cancellation() + if self.worker_thread is not None: + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py index ef9fe39d..823b54ad 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -160,12 +160,14 @@ def _handle_batch_exported(self, batch_generation_duration_in_seconds: float, ba ) ) - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def _handle_export_completion(self) -> None: + @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] + def _handle_export_completion(self, was_cancellation_requested: bool) -> None: self.progress_bar.setVisible(False) - self._request_worker_cancellation() - self._await_worker_thread_completion() + if not was_cancellation_requested: + self._request_worker_cancellation() + self._await_worker_thread_completion() + self._change_dialog_cancellation_button_enable_state(False) self._change_dialog_ok_button_enable_state(True) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py index 7a5ec1d6..6becabe6 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -214,10 +214,12 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f ) ) - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def _handle_import_completion(self) -> None: - self._request_worker_cancellation() - self._await_worker_thread_completion() + @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] + def _handle_import_completion(self, was_cancellation_requested: bool) -> None: + if not was_cancellation_requested: + self._request_worker_cancellation() + self._await_worker_thread_completion() + self._change_dialog_cancellation_button_enable_state(False) self._change_dialog_ok_button_enable_state(True) diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py index 13a5fc99..fb56736f 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py @@ -77,10 +77,9 @@ def start_export(self) -> None: ) ) self.batchCompleted.emit(batch_generation_duration, batch_idx) - self.finished.emit() + self.finished.emit(self.cancellation_requested) except Exception as err: self.failed.emit(err) - return @staticmethod def serialize_to_json(obj: Any) -> object: diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py index 455e7563..b96521a7 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py @@ -96,7 +96,7 @@ def start_import(self) -> None: ) ) self.batchCompleted.emit(batch_generation_duration, batch_data) - self.finished.emit() + self.finished.emit(self.cancellation_requested) except Exception as err: self.failed.emit(err) diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 481b9bb4..5d4c707f 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -20,6 +20,8 @@ from mqt import syrec +from .logger_utils import configure_default_console_logger + # We are using a relative import here: https://stackoverflow.com/questions/43728431/relative-imports-modulenotfounderror-no-module-named-x from .quantum_circuit_simulation_dialog import QuantumCircuitSimulationDialog @@ -1380,6 +1382,7 @@ def setup_toolbar(self) -> None: def main() -> int: + configure_default_console_logger() a = QtWidgets.QApplication([]) w = MainWindow() From 1e8aec3cbdc5696212cd7831daf386fd249d9678 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 20 Jan 2026 23:32:23 +0100 Subject: [PATCH 35/88] Refactored simulation run export dialog to use reusable BaseProgressDialog --- .../all_input_states_generator_worker.py | 2 +- .../qt_all_input_states_generator_dialog.py | 6 +- .../qt_simulation_run_json_export_dialog.py | 222 +++++++----------- .../simulation_run_json_export_worker.py | 12 +- 4 files changed, 99 insertions(+), 143 deletions(-) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 30bc4511..f6b62331 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -82,7 +82,7 @@ def start_generation(self) -> None: self.finished.emit(self.cancellation_requested) except Exception as error: error_msg: Final[str] = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" - log_error_to_console(error_msg, num_additionally_skipped_stack_frames_starting_from_caller_function=0) + log_error_to_console(error_msg) self.failed.emit(error) @staticmethod diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index c73df953..aa0041c4 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -136,6 +136,7 @@ def _handle_generated_input_state_batch( @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_input_state_generator_finished(self, was_cancellation_requested: bool) -> None: self.progress_info_text_lbl.setText("Input state generator finished!") + log_info_to_console("Input state generator finished!") if self.progress_bar is not None: self.progress_bar.setVisible(False) @@ -161,10 +162,7 @@ def _handle_input_state_generation_cancel_button_click(self) -> bool: is_cancellable=True, log_contents=False, ): - log_info_to_console( - "Cancellation of input state generation requested!", - num_additionally_skipped_stack_frames_starting_from_caller_function=0, - ) + log_info_to_console("Cancellation of input state generation requested!") self._handle_non_recoverable_error(None) return True return False diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py index 823b54ad..c87367b2 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING from PyQt6 import QtCore, QtWidgets @@ -20,73 +20,38 @@ from .qt_simulation_run_model import SimulationRunModel from .simulation_run_json_export_worker import SimulationRunJsonExportWorker +from ..logger_utils import log_info_to_console +from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from .dialogs.base_progress_dialog import BaseProgressDialog from .simulation_run_json_export_worker import SimulationRunJsonExportWorker -AGGREGATE_EXPORT_DATA_TEXT_FORMAT: Final[str] = ( - "Exported simulation runs: {num_exported_simulation_runs:d} | Total runtime for simulation run export [in seconds]: {total_runtime_in_sec:f}" -) -EXPORT_LOCATION_INFO_TEXT_FORMAT: Final[str] = "Exporting simulation runs to file {path_to_json_file:s}" -EXPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT: Final[str] = ( - "Finished export of simulation runs batch to file (runtime [in sec]: {batch_export_duration_in_seconds:f}!" -) - -class SimulationRunJsonExportDialog(QtWidgets.QDialog): # type: ignore[misc] +class SimulationRunJsonExportDialog(BaseProgressDialog[SimulationRunJsonExportWorker]): def __init__(self, parent: QtWidgets.QWidget): - super().__init__(parent) - - self.worker_thread: QtCore.QThread | None = None - self.worker: SimulationRunJsonExportWorker | None = None - - self.num_exported_simulation_runs: int = 0 - self.total_sim_run_export_duration_in_secs: float = 0 - - self.setModal(True) - self.setSizeGripEnabled(True) - self.setWindowTitle("Exporting simulation runs...") - left = 0 - top = 0 - width = 400 - height = 200 - self.setGeometry(left, top, width, height) - - main_layout = QtWidgets.QVBoxLayout() - self.export_location_info_lbl = QtWidgets.QLabel("") - self.export_location_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - self.progress_text_lbl = QtWidgets.QLabel("") - self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.progress_text_lbl.setStyleSheet("QLabel { color : gray; }") - - self.err_text_lbl = QtWidgets.QLabel("") - self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.err_text_lbl.setStyleSheet("QLabel { color : red; }") - - main_layout.addWidget(self.export_location_info_lbl) - main_layout.addWidget(self.progress_text_lbl) - main_layout.addWidget(self.err_text_lbl) - main_layout.addStretch() - - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar.setFormat("Exported simulation run %v of %m") - self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - self.total_runtime_text_lbl = QtWidgets.QLabel("") - self.total_runtime_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - main_layout.addWidget(self.total_runtime_text_lbl) - - self.dialog_button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel + super().__init__( + parent, + dialog_title="Exporting simulation runs...", + dialog_size=(400, 200), + optional_progress_bar_text_format="Exported simulation run %v of %m", + create_default_layout=False, ) - self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) + self.num_exported_simulation_runs: int = 0 self.dialog_button_box.accepted.connect(self.accept) + self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) - self._change_dialog_ok_button_enable_state(False) - self._change_dialog_cancellation_button_enable_state(False) + self.export_location_info_lbl = QtWidgets.QLabel("") + self.export_location_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - main_layout.addWidget(self.dialog_button_box) - self.setLayout(main_layout) + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.title_lbl) + layout.addWidget(self.export_location_info_lbl) + layout.addWidget(self.progress_info_text_lbl) + layout.addWidget(self.error_text_lbl) + layout.addStretch() + layout.addWidget(self.progress_bar) + layout.addWidget(self.total_runtime_info_text_lbl) + layout.addWidget(self.dialog_button_box) + self.setLayout(layout) def start_export( self, @@ -95,10 +60,21 @@ def start_export( num_sim_runs_to_export: int, batch_size: int = 500, ) -> None: - self.export_location_info_lbl.setText( - EXPORT_LOCATION_INFO_TEXT_FORMAT.format(path_to_json_file=str(export_location)) - ) - self.progress_bar.setMaximum(num_sim_runs_to_export) + self.title_lbl.setText(f"Exporting simulation runs with batch size {batch_size}!") + self.export_location_info_lbl.setText(f"Export destination: {export_location!s}") + + if self.progress_bar is not None: + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(num_sim_runs_to_export) + self.progress_bar.setValue(0) + else: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Required widget not found", + message_box_content="Simulation run exporter was initialized without a progress bar! This should not happen.", + is_cancellable=False, + ) # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker = SimulationRunJsonExportWorker(export_location, sim_runs_to_export, batch_size) @@ -115,12 +91,12 @@ def start_export( self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancellation_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(True) def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing - if self.worker is None or self._handle_export_to_file_cancel_button_click(): - if not self.err_text_lbl.text(): + if self._handle_export_to_file_cancel_button_click(): + if not self.error_text_lbl.text(): self.accept() else: self.reject() @@ -129,94 +105,68 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] def _handle_export_failure(self, err: Exception) -> None: - self.progress_text_lbl.setText("") - self.progress_bar.setVisible(False) - - self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during export of simulation runs") - self._request_worker_cancellation() - self._await_worker_thread_completion() - self._change_dialog_cancellation_button_enable_state(False) + self._handle_non_recoverable_error(err) @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] def _handle_batch_exported(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: if not isinstance(batch_data, int): - # TODO: Error logging? - # TODO: Cancel worker? + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.INFO, + message_box_parent=self, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be of type {type(int)} but was actually {type(batch_data)}! This should not happen.", + is_cancellable=False, + ) + if self.worker is not None: + self.worker.ack_batch_processed() return - self.progress_text_lbl.setText( - EXPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( - batch_export_duration_in_seconds=batch_generation_duration_in_seconds - ) - ) + self._update_progress_text_with_batch_info(batch_data, batch_generation_duration_in_seconds) + self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) self.num_exported_simulation_runs += batch_data - self.progress_bar.setValue(self.num_exported_simulation_runs) - self.total_sim_run_export_duration_in_secs += batch_generation_duration_in_seconds - self.progress_text_lbl.setText( - AGGREGATE_EXPORT_DATA_TEXT_FORMAT.format( - num_exported_simulation_runs=self.num_exported_simulation_runs, - total_runtime_in_sec=self.total_sim_run_export_duration_in_secs, - ) - ) + if self.progress_bar is not None: + self.progress_bar.setValue(self.num_exported_simulation_runs) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_export_completion(self, was_cancellation_requested: bool) -> None: - self.progress_bar.setVisible(False) + self.progress_info_text_lbl.setText("Simulation run export finished!") + log_info_to_console("Simulation run export finished!") + + if self.progress_bar is not None: + self.progress_bar.setVisible(False) if not was_cancellation_requested: self._request_worker_cancellation() - self._await_worker_thread_completion() + self._shutdown_worker_thread_and_await_completion() - self._change_dialog_cancellation_button_enable_state(False) + self._change_dialog_cancel_button_enable_state(False) self._change_dialog_ok_button_enable_state(True) - def _request_worker_cancellation(self) -> None: - self.progress_text_lbl.setText("Requesting cancellation of simulation run importer!") - if self.worker is not None: - self.worker.request_cancellation() - - def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) - - if dialog_cancel_button is None: - return - - dialog_cancel_button.setEnabled(should_button_be_enabled) - - def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_ok_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - - if dialog_ok_button is None: - return - - dialog_ok_button.setEnabled(should_button_be_enabled) - - def _reset_workers(self) -> None: - self.worker_thread = None - self.worker = None - - def _await_worker_thread_completion(self) -> None: - if self.worker_thread is not None: - self.worker_thread.quit() - self.worker_thread.wait() - self.progress_text_lbl.setText("Simulation run exporter thread finished!") - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_export_to_file_cancel_button_click(self) -> bool: - clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( - self, - "Cancellation of export to json file!", - "Are you sure that you want to stop the export of simulation runs to the .json file?", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) + if self.worker is None: + return True - if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: - self._request_worker_cancellation() + if show_optionally_cancellable_notification( + message_box_type=MessageBoxType.QUESTION, + message_box_parent=self, + message_box_title="Cancellation of export to json file!", + message_box_content="Are you sure that you want to stop the export of simulation runs to the .json file? Already exported data will not be deleted.", + is_cancellable=True, + log_contents=False, + ): + log_info_to_console("Cancellation of simulation run export requested!") + self._handle_non_recoverable_error(None) return True return False + + def _handle_non_recoverable_error(self, err: Exception | None) -> None: + self.progress_info_text_lbl.setText("") + if err is not None: + self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) + + if self.worker is not None: + self._request_worker_cancellation() + if self.worker_thread is not None: + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py index fb56736f..43b28a4d 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py @@ -13,6 +13,7 @@ from PyQt6 import QtCore +from ..logger_utils import log_error_to_console from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel @@ -68,6 +69,9 @@ def start_export(self) -> None: batch_idx = 0 n_generated_batches += 1 # file.write("\n\t]\n}") + # An error during during the serialization of the simulation runs to their .json representation will cause the content of the + # exported to .json file to be invalid .json due to the simulation runs JSON array as well as the top level JSON object missing + # their closing symbol. file.write("]}") if batch_idx > 0 and not self.is_cancellation_requested(): @@ -78,8 +82,12 @@ def start_export(self) -> None: ) self.batchCompleted.emit(batch_generation_duration, batch_idx) self.finished.emit(self.cancellation_requested) - except Exception as err: - self.failed.emit(err) + except Exception as error: + error_msg: Final[str] = ( + f"Error in simulaton run export worker (exported .json file could be incomplete)! Reason: {type(error)=}, {error=}" + ) + log_error_to_console(error_msg) + self.failed.emit(error) @staticmethod def serialize_to_json(obj: Any) -> object: From 5891e8e8f62e7b74a94a49556b8ac887fd67d27f Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 21 Jan 2026 01:20:57 +0100 Subject: [PATCH 36/88] Refactored simulation run import worker/dialog to use shared worker/progress dialog base classes. Fixed batch start timestamp update in worker base class --- .../all_input_states_generator_worker.py | 27 +- .../cancellable_base_worker.py | 14 +- .../qt_all_input_states_generator_dialog.py | 5 +- .../qt_simulation_run_json_import_dialog.py | 254 ++++++++---------- .../simulation_run_json_export_worker.py | 13 +- .../simulation_run_json_import_worker.py | 55 ++-- 6 files changed, 180 insertions(+), 188 deletions(-) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index f6b62331..6f21fb36 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -9,7 +9,7 @@ from __future__ import annotations import time -from typing import Final +from typing import TYPE_CHECKING, Final from PyQt6 import QtCore @@ -19,6 +19,9 @@ from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel +if TYPE_CHECKING: + from .cancellable_base_worker import BatchTimestamps + class AllInputStatesGeneratorWorker(CancellableBaseWorker): def __init__(self, expected_input_state_size: int, batch_size: int): @@ -32,11 +35,14 @@ def start_generation(self) -> None: AllInputStatesGeneratorWorker._validate_parameters(self.expected_input_state_size, self.batch_size) n_states_to_generate: int = 2**self.expected_input_state_size n_batches: int = n_states_to_generate // self.batch_size - batch_start_timestamp: float = AllInputStatesGeneratorWorker._get_timestamp() - batch_generation_duration: float = 0 + batch_timestamps: BatchTimestamps | None = None + self_raised_error_msg: str | None = None + if self.wait_on_batch_processed_acknowledgement_condition is None: - self.failed.emit(ValueError("Internal batch processed acknowledgement condition was not initialized")) + self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" + log_error_to_console(self_raised_error_msg) + self.failed.emit(ValueError(self_raised_error_msg)) return first_integer_encoding_first_state_of_batch: int = 0 @@ -49,12 +55,13 @@ def start_generation(self) -> None: batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i ) - batch_generation_duration = ( + batch_timestamps = ( AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_generation_duration, batch_data.copy()) + batch_start_timestamp = batch_timestamps.end + self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) with QtCore.QMutexLocker(self.batch_ack_mutex): if not self.is_cancellation_requested(): self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) @@ -73,16 +80,16 @@ def start_generation(self) -> None: last_batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i ) - batch_generation_duration = ( + batch_timestamps = ( AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_generation_duration, last_batch_data) + self.batchCompleted.emit(batch_timestamps.duration, last_batch_data) self.finished.emit(self.cancellation_requested) except Exception as error: - error_msg: Final[str] = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" - log_error_to_console(error_msg) + self_raised_error_msg = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" + log_error_to_console(self_raised_error_msg) self.failed.emit(error) @staticmethod diff --git a/python/mqt/syrec/simulation_view/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/cancellable_base_worker.py index 09bd1659..41b14518 100644 --- a/python/mqt/syrec/simulation_view/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/cancellable_base_worker.py @@ -9,6 +9,7 @@ from __future__ import annotations import time +from dataclasses import dataclass from typing import Any, Final, TypeVar from PyQt6 import QtCore @@ -16,6 +17,13 @@ T = TypeVar("T") +@dataclass(frozen=True) +class BatchTimestamps: + start: float + end: float + duration: float + + class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] batch_completed = QtCore.pyqtSignal(float, object, name="batchCompleted") # While the cancellation operation is assumed to request the cancellation of the internal worker @@ -79,10 +87,8 @@ def are_list_of_batch_items_of_type(batch_data: Any, expected_batch_element_type def _get_timestamp() -> float: return time.perf_counter() - # TODO: Is this correct @staticmethod - def _calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestamp: float) -> float: + def _calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestamp: float) -> BatchTimestamps: batch_end_timestamp: Final[float] = CancellableBaseWorker._get_timestamp() batch_duration = batch_end_timestamp - batch_start_timestamp - batch_start_timestamp = batch_end_timestamp - return batch_duration + return BatchTimestamps(batch_start_timestamp, batch_end_timestamp, batch_duration) diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index aa0041c4..8ac058a7 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -81,7 +81,10 @@ def start_generation( def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing if self._handle_input_state_generation_cancel_button_click(): - self.accept() + if not self.error_text_lbl.text(): + self.accept() + else: + self.reject() else: event.ignore() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py index 6becabe6..4af96fb8 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -19,72 +19,48 @@ from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel +from ..logger_utils import log_error_to_console, log_info_to_console +from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from .dialogs.base_progress_dialog import BaseProgressDialog from .qt_simulation_run_model import SimulationRunModel from .simulation_run_json_import_worker import SimulationRunJsonImportWorker -AGGREGATE_IMPORT_DATA_TEXT_FORMAT: Final[str] = ( - "Imported simulation runs: {num_imported_simulation_runs:d} | Total runtime for simulation run import [in seconds]: {total_runtime_in_sec:f}" -) -IMPORT_ORIGIN_INFO_TEXT_FORMAT: Final[str] = "Importing simulation runs from file {path_to_json_file:s}" -IMPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT: Final[str] = ( - "Finished import of simulation runs batch from file (runtime [in sec]: {batch_generation_duration_in_seconds:f}!" -) - -class SimulationRunJsonImportDialog(QtWidgets.QDialog): # type: ignore[misc] +class SimulationRunJsonImportDialog(BaseProgressDialog[SimulationRunJsonImportWorker]): def __init__(self, parent: QtWidgets.QWidget): - super().__init__(parent) - + super().__init__( + parent, + dialog_title="Importing simulation runs...", + dialog_size=(400, 200), + optional_progress_bar_text_format=None, + create_default_layout=False, + ) + self.num_imported_simulation_runs: int = 0 self.shared_simulation_runs_model: QtSimulationRunModel | None = None - self.worker_thread: QtCore.QThread | None = None - self.worker: SimulationRunJsonImportWorker | None = None - self.num_imported_simulation_runs: int = 0 - self.stop_processing_imported_sim_run_batches: bool = False - self.total_simulation_run_import_runtime_in_seconds: float = 0 - - self.setModal(True) - self.setSizeGripEnabled(True) - self.setWindowTitle("Importing simulation runs...") - left = 0 - top = 0 - width = 400 - height = 200 - self.setGeometry(left, top, width, height) - - main_layout = QtWidgets.QVBoxLayout() + self.dialog_button_box.accepted.connect(self.accept) + self.dialog_button_box.rejected.connect(self._handle_import_from_file_cancel_button_click) + self.import_origin_info_lbl = QtWidgets.QLabel("") self.import_origin_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.progress_text_lbl = QtWidgets.QLabel("") - self.progress_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.progress_text_lbl.setStyleSheet("QLabel { color : gray; }") + self.num_imported_simulation_runs_info_lbl = QtWidgets.QLabel("") + self.num_imported_simulation_runs_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.err_text_lbl = QtWidgets.QLabel("") - self.err_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.err_text_lbl.setStyleSheet("QLabel { color : red; }") + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.title_lbl) + layout.addWidget(self.import_origin_info_lbl) + layout.addWidget(self.progress_info_text_lbl) + layout.addWidget(self.error_text_lbl) + layout.addStretch() - main_layout.addWidget(self.import_origin_info_lbl) - main_layout.addWidget(self.progress_text_lbl) - main_layout.addWidget(self.err_text_lbl) - main_layout.addStretch() + aggregate_stats_controls_layout = QtWidgets.QHBoxLayout() + aggregate_stats_controls_layout.addWidget(self.num_imported_simulation_runs_info_lbl) + aggregate_stats_controls_layout.addWidget(self.total_runtime_info_text_lbl) + layout.addLayout(aggregate_stats_controls_layout) - self.total_runtime_text_lbl = QtWidgets.QLabel("") - self.total_runtime_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - main_layout.addWidget(self.total_runtime_text_lbl) - - self.dialog_button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) - self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.rejected.connect(self._handle_import_from_file_cancel_button_click) - self.dialog_button_box.accepted.connect(self.accept) - - self._change_dialog_ok_button_enable_state(False) - self._change_dialog_cancellation_button_enable_state(False) - - main_layout.addWidget(self.dialog_button_box) - self.setLayout(main_layout) + layout.addWidget(self.dialog_button_box) + self.setLayout(layout) def start_generation( self, @@ -94,9 +70,8 @@ def start_generation( batch_size: int = 1000, ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model - self.import_origin_info_lbl.setText( - IMPORT_ORIGIN_INFO_TEXT_FORMAT.format(path_to_json_file=str(path_to_json_file)) - ) + self.title_lbl.setText(f"Importing simulation runs from .json file with batch size {batch_size}!") + self.import_origin_info_lbl.setText(f"Import source: {path_to_json_file!s}") # Some helpful links are the official QThread documentation but some helpful explanaitions were also found in: # - https://www.haccks.com/posts/how-to-use-qthread-correctly-p1/ @@ -139,12 +114,12 @@ def start_generation( self.worker_thread.finished.connect(self._reset_workers) # Only this call will actually start a new thread self.worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancellation_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(True) def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing - if self.worker is None or self._handle_import_from_file_cancel_button_click(): - if not self.err_text_lbl.text(): + if self._handle_import_from_file_cancel_button_click(): + if not self.error_text_lbl.text(): self.accept() else: self.reject() @@ -153,125 +128,106 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] def _handle_importer_failure(self, err: Exception) -> None: - self.progress_text_lbl.setText("") - self.err_text_lbl.setText(f"Unexpected {err=}, {type(err)=} during import of simulation runs") - if self.shared_simulation_runs_model is not None: - self.shared_simulation_runs_model.delete_all_simulation_run_models() - else: - QtWidgets.QMessageBox.critical( - self, - "Internal state error!", - "Shared simulation runs model was not initialized during handling of importer failure!\nThis should not happen.", - buttons=QtWidgets.QMessageBox.StandardButton.Ok, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - self._request_worker_cancellation() - self._await_worker_thread_completion() - self._change_dialog_cancellation_button_enable_state(False) + self._handle_non_recoverable_error(err) @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: - if self.stop_processing_imported_sim_run_batches: + if self.stop_processing_recv_batches: return if not SimulationRunJsonImportWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunModel): - # TODO: Error logging? - # TODO: Cancel worker? + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.INFO, + message_box_parent=self, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be a list of SimulationRunModels but was actually {type(batch_data)}. Skipping batch!", + is_cancellable=False, + ) if self.worker is not None: self.worker.ack_batch_processed() return - self.progress_text_lbl.setText( - IMPORTED_BATCH_PROGRESS_INFO_TEXT_FORMAT.format( - batch_generation_duration_in_seconds=batch_generation_duration_in_seconds - ) - ) generated_simulation_run_models: Final[list[SimulationRunModel]] = batch_data # type: ignore[assignment] + self._update_progress_text_with_batch_info( + len(generated_simulation_run_models), batch_generation_duration_in_seconds + ) + if self.shared_simulation_runs_model is None: - QtWidgets.QMessageBox.critical( - self, - "Internal state error!", - "Shared simulation runs model was not initialized during handling of generated input state batch!\nThis should not happen.", - buttons=QtWidgets.QMessageBox.StandardButton.Ok, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - self._request_worker_cancellation() - self._await_worker_thread_completion() + log_error_to_console("Shared simulation runs model was not initialized during handling of batch!") + self._handle_non_recoverable_error(None) + return + + try: + self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) + except Exception as sim_run_model_err: + self._handle_non_recoverable_error(sim_run_model_err) return - # TODO: Error handling - # TODO: Use delayed processing to reduce "laggy"/almost frozen GUI - self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) if self.worker is not None: self.worker.ack_batch_processed() - self.total_simulation_run_import_runtime_in_seconds += batch_generation_duration_in_seconds + self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) self.num_imported_simulation_runs += len(generated_simulation_run_models) - self.total_runtime_text_lbl.setText( - AGGREGATE_IMPORT_DATA_TEXT_FORMAT.format( - num_imported_simulation_runs=self.num_imported_simulation_runs, - total_runtime_in_sec=self.total_simulation_run_import_runtime_in_seconds, - ) + self.num_imported_simulation_runs_info_lbl.setText( + f"Num. imported simulation runs: {self.num_imported_simulation_runs}" ) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_import_completion(self, was_cancellation_requested: bool) -> None: + self.progress_info_text_lbl.setText("Simulation run import finished!") + log_info_to_console("Simulation run export finished!") + if not was_cancellation_requested: self._request_worker_cancellation() - self._await_worker_thread_completion() + self._shutdown_worker_thread_and_await_completion() - self._change_dialog_cancellation_button_enable_state(False) + self._change_dialog_cancel_button_enable_state(False) self._change_dialog_ok_button_enable_state(True) - def _request_worker_cancellation(self) -> None: - self.stop_processing_imported_sim_run_batches = True - self.progress_text_lbl.setText("Requesting cancellation of simulation run importer!") - if self.worker is not None: - self.worker.request_cancellation() - - def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) - - if dialog_cancel_button is None: - return - - dialog_cancel_button.setEnabled(should_button_be_enabled) - - def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_ok_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - - if dialog_ok_button is None: - return - - dialog_ok_button.setEnabled(should_button_be_enabled) - - def _reset_workers(self) -> None: - self.worker_thread = None - self.worker = None - - def _await_worker_thread_completion(self) -> None: - if self.worker_thread is not None: - self.worker_thread.quit() - self.worker_thread.wait() - self.progress_text_lbl.setText("Simulation run importer thread finished!") - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_import_from_file_cancel_button_click(self) -> bool: - clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( - self, - "Cancellation of import from json file!", - "Are you sure that you want to stop the import of simulation runs from the file? This will cause the deletion of all already generated simulation runs.", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) + if self.worker is None: + return True - if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: - self._request_worker_cancellation() - if self.shared_simulation_runs_model is not None: - self.shared_simulation_runs_model.delete_all_simulation_run_models() + if show_optionally_cancellable_notification( + message_box_type=MessageBoxType.QUESTION, + message_box_parent=self, + message_box_title="Cancellation of import from json file!", + message_box_content="Are you sure that you want to stop the import of simulation runs from the file? This will cause the deletion of all already generated simulation runs.", + is_cancellable=True, + log_contents=False, + ): + log_info_to_console("Cancellation of simulation run export requested!") + self._handle_non_recoverable_error(None) return True return False + + def _handle_non_recoverable_error(self, err: Exception | None) -> None: + self.progress_info_text_lbl.setText("") + if err is not None: + self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) + + if self.shared_simulation_runs_model is not None: + try: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + except Exception: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Internal error!", + message_box_content="Failed to delete all simulation run models during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", + is_cancellable=False, + ) + else: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Internal state error!", + message_box_content="Shared simulation runs model was not initialized during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", + is_cancellable=False, + ) + + if self.worker is not None: + self._request_worker_cancellation() + if self.worker_thread is not None: + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py index 43b28a4d..4c9160bb 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py @@ -21,6 +21,8 @@ from collections.abc import Iterable from pathlib import Path + from .cancellable_base_worker import BatchTimestamps + class SimulationRunJsonExportWorker(CancellableBaseWorker): def __init__( @@ -45,7 +47,7 @@ def start_export(self) -> None: # file.write("{\n\t\"simulationRuns\": [\n") file.write('{"simulationRuns":[') batch_start_timestamp: float = SimulationRunJsonExportWorker._get_timestamp() - batch_generation_duration: float = 0 + batch_timestamps: BatchTimestamps | None = None for sim_run in self.simulation_runs_to_export: if self.is_cancellation_requested(): break @@ -60,12 +62,13 @@ def start_export(self) -> None: batch_idx += 1 if batch_idx == self.export_batch_size: - batch_generation_duration = ( + batch_timestamps = ( SimulationRunJsonExportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_generation_duration, self.export_batch_size) + batch_start_timestamp = batch_timestamps.end + self.batchCompleted.emit(batch_timestamps.duration, self.export_batch_size) batch_idx = 0 n_generated_batches += 1 # file.write("\n\t]\n}") @@ -75,12 +78,12 @@ def start_export(self) -> None: file.write("]}") if batch_idx > 0 and not self.is_cancellation_requested(): - batch_generation_duration = ( + batch_timestamps = ( SimulationRunJsonExportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_generation_duration, batch_idx) + self.batchCompleted.emit(batch_timestamps.duration, batch_idx) self.finished.emit(self.cancellation_requested) except Exception as error: error_msg: Final[str] = ( diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py index b96521a7..492e84e9 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py @@ -18,12 +18,15 @@ from mqt import syrec +from ..logger_utils import log_error_to_console, log_info_to_console from .cancellable_base_worker import CancellableBaseWorker from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: from pathlib import Path + from .cancellable_base_worker import BatchTimestamps + SIMULATION_RUNS_JSON_KEY: Final[str] = "simulationRuns" INPUT_STATE_JSON_KEY: Final[str] = "in" EXPECTED_OUTPUT_STATE_JSON_KEY: Final[str] = "out" @@ -41,8 +44,12 @@ def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batc def start_import(self) -> None: try: SimulationRunJsonImportWorker._validate_parameters(self.expected_input_state_size, self.batch_size) + self_raised_error_msg: str | None = "" + if self.wait_on_batch_processed_acknowledgement_condition is None: - self.failed.emit(ValueError("Internal batch processed acknowledgement condition was not initialized")) + self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" + log_error_to_console(self_raised_error_msg) + self.failed.emit(ValueError(self_raised_error_msg)) return batch_idx: int = 0 @@ -51,7 +58,7 @@ def start_import(self) -> None: # Reading bytes instead of strings leads to better parser performance with self.path_to_json_file.open("rb") as file: batch_start_timestamp: float = SimulationRunJsonImportWorker._get_timestamp() - batch_generation_duration: float = 0 + batch_timestamps: BatchTimestamps | None = None # The json parser starts at the first element matching the prefix which in our case starts at an element with key 'simulationRuns' that is expected # to be a property of the top level element (i.e. the path to the 'simulationRuns' element is relative to the top level element). # Additionally, with the postfix '.item', only the entries of a JSON array are processed. If the 'simulationRuns' entry value is no @@ -65,40 +72,50 @@ def start_import(self) -> None: # need to discard any other type of JSON elements (integers, strings, array, etc.) by checking whether we are actually processing a # python dictionary. if not isinstance(arr_elem, dict): + log_info_to_console( + f"Expected parsed simulation run JSON array element to be returned as python dictionary from third-party library but its python type was {type(arr_elem)}" + ) continue batch_data[batch_idx] = SimulationRunJsonImportWorker._try_deserialize_simulation_run( self.expected_input_state_size, arr_elem ) batch_idx += 1 - if batch_idx == self.batch_size: - batch_generation_duration = ( - SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( - batch_start_timestamp - ) + if batch_idx < self.batch_size: + continue + + batch_timestamps = ( + SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp ) - self.batchCompleted.emit(batch_generation_duration, batch_data.copy()) - with QtCore.QMutexLocker(self.batch_ack_mutex): + ) + batch_start_timestamp = batch_timestamps.end + self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) + + with QtCore.QMutexLocker(self.batch_ack_mutex): + if not self.is_cancellation_requested(): self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) - # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using - # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. - time.sleep(0.1) + # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using + # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. + time.sleep(0.1) - for i in range(self.batch_size): - batch_data[i] = None - batch_idx = 0 + for i in range(self.batch_size): + batch_data[i] = None + batch_idx = 0 if batch_idx != 0 and not self.is_cancellation_requested(): del batch_data[batch_idx:] - batch_generation_duration = ( + batch_timestamps = ( SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_generation_duration, batch_data) + self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) self.finished.emit(self.cancellation_requested) - except Exception as err: - self.failed.emit(err) + except Exception as error: + self_raised_error_msg = f"Error in simulaton run import worker! Reason: {type(error)=}, {error=}" + log_error_to_console(self_raised_error_msg) + self.failed.emit(error) @staticmethod def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: From 6b1e472d763558582007b884342a8798b3e50010 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 22 Jan 2026 18:17:37 +0100 Subject: [PATCH 37/88] Simulation run execution worker and dialog are now using batch instead of singular data --- .../quantum_circuit_simulation_dialog.py | 5 +- .../dialogs/base_progress_dialog.py | 5 +- .../qt_all_input_states_generator_dialog.py | 1 - .../qt_simulation_run_dialog.py | 372 +++++++++--------- .../qt_simulation_run_model.py | 11 +- .../qt_simulation_run_worker.py | 144 +++++++ .../simulation_view/qt_simulation_worker.py | 129 ------ 7 files changed, 337 insertions(+), 330 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/qt_simulation_run_worker.py delete mode 100644 python/mqt/syrec/simulation_view/qt_simulation_worker.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index ed12d72e..307b20a3 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -577,6 +577,7 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma # TODO: Error logging? return + # TODO: Should this validation be performed in the dialog itself? Can this condition even be met? num_simulation_runs: Final[int] = self.simulation_runs_model.rowCount(QtCore.QModelIndex()) if num_simulation_runs >= sys.maxsize: QtWidgets.QMessageBox.critical( @@ -595,11 +596,11 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma ) return - self.simulation_run_dialog = SimulationRunDialog(self.simulation_runs_model, self) + self.simulation_run_dialog = SimulationRunDialog(self) self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) self.simulation_run_dialog.show() self.simulation_run_dialog.start_simulations( - self.annotatable_quantum_computation, num_simulation_runs, stop_at_first_output_state_mismatch + self.annotatable_quantum_computation, self.simulation_runs_model, stop_at_first_output_state_mismatch ) # TODO: Toggle state after edits in simulation runs were performed? diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 12a176ed..6ff54998 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -18,7 +18,9 @@ T = TypeVar("T", bound=CancellableBaseWorker) -DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT: Final[str] = "Total runtime [in seconds]: {total_runtime_in_seconds:f}" +DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( + "Total runtime [in seconds] (excluding model updates, internal waits): {total_runtime_in_seconds:f}" +) DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( "Batch of {n_batch_elements:d} completed! Runtime [in seconds]: {batch_duration_in_seconds:f}" ) @@ -81,7 +83,6 @@ def __init__( self.dialog_button_box.setCenterButtons(True) self._change_dialog_ok_button_enable_state(False) self._change_dialog_cancel_button_enable_state(False) - # TODO: Currently the user is responsible for hooking up the signals of the dialog_button_box if create_default_layout: layout.addWidget(self.title_lbl) diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index 8ac058a7..cb070968 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -42,7 +42,6 @@ def start_generation( self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 1000 ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model - self.title_lbl.setText(f"Generating simulation runs with batch size {batch_size}!") # TODO: Validation that maximum value can actually be stored in progress bar maximum (should validation be performed in dialog or by caller?) if self.progress_bar is not None: diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index c18e00f8..b925269e 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, cast from PyQt6 import QtCore, QtWidgets @@ -17,57 +17,53 @@ from mqt import syrec - from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel + from .qt_simulation_run_model import QtSimulationRunModel -from .qt_simulation_worker import SimulationRunResult, SimulationWorker, ToBeExecutedSimulationRun +from ..logger_utils import log_error_to_console, log_info_to_console +from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from .dialogs.base_progress_dialog import BaseProgressDialog +from .qt_simulation_run_worker import SimulationRunResult, SimulationRunWorker from .styled_item_delegates.qt_simulation_run_execution_styled_item_delegate import ( SimulationRunExecutionStyledItemDelegate, ) -TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS: Final[int] = 1000 -TOTAL_RUNTIME_TEXT_FORMAT: Final[str] = "Total runtime [in seconds]: {total_runtime_in_seconds:f}" +class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): + def __init__(self, parent: QtWidgets.QWidget): + super().__init__( + parent, + dialog_title="Executing simulation runs...", + dialog_size=(400, 200), + optional_progress_bar_text_format="Executed simulation run %v of %m", + create_default_layout=False, + ) + self.annotatable_quantum_computation: syrec.annotatable_quantum_computation | None = None + self.shared_simulation_runs_model: QtSimulationRunModel | None = None + self.stop_at_first_output_state_mismatch: bool = False + self.num_executed_simulation_runs: int = 0 -class SimulationRunDialog(QtWidgets.QDialog): # type: ignore[misc] - def __init__(self, shared_simulation_run_model: QtSimulationRunModel, parent: QtWidgets.QWidget): - super().__init__(parent) - - # TODO: Member variable could also be initialized in start_simulations - self.simulation_runs_model = shared_simulation_run_model - self.worker_thread: QtCore.QThread | None = None - self.worker: SimulationWorker | None = None - - self.num_completed_simulation_runs: int = 0 - self.expected_total_num_simulation_runs: int = 0 - self.did_simulation_run_fail_due_to_failure: bool = False - - self.setModal(True) - self.setSizeGripEnabled(True) - self.setWindowTitle("Executing simulation runs") - left = 0 - top = 0 - width = 400 - height = 400 - self.setGeometry(left, top, width, height) + self.dialog_button_box.accepted.connect(self.accept) + self.dialog_button_box.rejected.connect(self._handle_simulation_runs_cancel_button_click) - main_layout = QtWidgets.QVBoxLayout() - self.setLayout(main_layout) + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.title_lbl) + layout.addWidget(self.progress_info_text_lbl) + layout.addWidget(self.error_text_lbl) simulation_runs_list_layout = QtWidgets.QHBoxLayout() - simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() - simulation_runs_list_view.setModel(self.simulation_runs_model) - simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) # type: ignore[no-untyped-call] - simulation_runs_list_view.setUniformItemSizes(True) - simulation_runs_list_view.setAutoFillBackground(False) - simulation_runs_list_view.setSpacing(5) - simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() + self.simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) # type: ignore[no-untyped-call] + self.simulation_runs_list_view.setUniformItemSizes(True) + self.simulation_runs_list_view.setAutoFillBackground(False) + self.simulation_runs_list_view.setSpacing(5) + self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) - simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) - # simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) + self.simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + # self.simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) simulation_runs_list_scrollarea = QtWidgets.QScrollArea() simulation_runs_list_scrollarea.setAutoFillBackground(False) - simulation_runs_list_scrollarea.setWidget(simulation_runs_list_view) + simulation_runs_list_scrollarea.setWidget(self.simulation_runs_list_view) simulation_runs_list_scrollarea.setWidgetResizable(True) simulation_runs_list_layout.addItem( @@ -81,193 +77,193 @@ def __init__(self, shared_simulation_run_model: QtSimulationRunModel, parent: Qt 2, 2, QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Minimum ) ) - main_layout.addLayout(simulation_runs_list_layout) - - simulation_progress_controls_layout = QtWidgets.QVBoxLayout() - simulation_success_progress_layout = QtWidgets.QHBoxLayout() - # self.simulation_run_total_runtime_timer = QtWidgets.QTimer(self) - # self.simulation_run_total_runtime_timer.timeout.connect(self.) - # self.simulation_run_total_runtime_info_label = QtWidgets.QLabel(TOTAL_RUNTIME_TEXT_FORMAT.format(total_runtime_in_seconds=0)) - self.simulation_run_progress_bar = QtWidgets.QProgressBar() - # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format - self.simulation_run_progress_bar.setFormat("Executing simulation run %v of %m") - self.simulation_run_progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.simulation_run_progress_lbl = QtWidgets.QLabel("") - self.simulation_run_progress_lbl.setStyleSheet("QLabel { color : gray; }") - - self.simulation_run_err_lbl = QtWidgets.QLabel("") - self.simulation_run_err_lbl.setStyleSheet("QLabel { color : red; }") - - # simulation_progress_layout.addWidget(self.simulation_run_total_runtime_info_label) - simulation_success_progress_layout.addWidget(self.simulation_run_progress_bar) - simulation_success_progress_layout.addWidget(self.simulation_run_progress_lbl) - simulation_progress_controls_layout.addLayout(simulation_success_progress_layout) - simulation_progress_controls_layout.addWidget(self.simulation_run_err_lbl) - - # simulation_progress_layout.addStretch() - main_layout.addLayout(simulation_progress_controls_layout) - - # TODO: One could also offer a close button in the dialog (that warns the user when closing the dialog during a simulation run execution)? - self.dialog_button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.StandardButton.Cancel) - self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.rejected.connect(self._handle_simulation_runs_cancel_button_click) - main_layout.addWidget(self.dialog_button_box) + layout.addLayout(simulation_runs_list_layout) + layout.addWidget(self.progress_bar) + layout.addWidget(self.total_runtime_info_text_lbl) + # layout.addStretch() + layout.addWidget(self.dialog_button_box) + self.setLayout(layout) def start_simulations( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, - expected_total_num_simulation_runs: int, + shared_simulation_run_model: QtSimulationRunModel, stop_at_first_output_state_mismatch: bool, + batch_size: int = 100, ) -> None: - self.num_completed_simulation_runs = 0 - self.expected_total_num_simulation_runs = expected_total_num_simulation_runs + self.annotatable_quantum_computation = annotatable_quantum_computation + self.shared_simulation_runs_model = shared_simulation_run_model + self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) + self.stop_at_first_output_state_mismatch = stop_at_first_output_state_mismatch + log_info_to_console( + f"Starting execution of simulation runs, stopping after first output mismatch flag is set to {self.stop_at_first_output_state_mismatch}" + ) + + expected_input_state_size: Final[int] = self.annotatable_quantum_computation.num_data_qubits + if batch_size <= 0 or expected_input_state_size <= 0: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Invalid input parameters detected", + message_box_content=f"Expected batch size (value={batch_size}) as well as the expected input state size (value={expected_input_state_size}) to be positive integers!", + is_cancellable=False, + ) + self.reject() + return - self.simulation_run_progress_bar.setMinimum(0) - self.simulation_run_progress_bar.setMaximum(expected_total_num_simulation_runs - 1) - self.simulation_run_progress_bar.setValue(0) - self.simulation_run_progress_bar.setVisible(True) + expected_total_num_simulation_runs: Final[int] = shared_simulation_run_model.rowCount(QtCore.QModelIndex()) + self.title_lbl.setText( + f"Executing {expected_total_num_simulation_runs} simulation runs with batch size {batch_size}!" + ) + if self.progress_bar is not None: + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(expected_total_num_simulation_runs) + self.progress_bar.setValue(0) + else: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Required widget not found", + message_box_content="Simulation run dialog was initialized without a progress bar! This should not happen.", + is_cancellable=False, + ) - # self.simulation_run_total_runtime_timer.start(TOTAL_RUNTIME_TIMER_TIMEOUT_IN_MS) # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker = SimulationWorker(annotatable_quantum_computation, stop_at_first_output_state_mismatch) + self.worker = SimulationRunWorker( + self.annotatable_quantum_computation, + self.shared_simulation_runs_model, + expected_input_state_size, + batch_size, + self.stop_at_first_output_state_mismatch, + ) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.allSimulationsDone.connect( - self._handle_all_simulation_runs_done, QtCore.Qt.ConnectionType.QueuedConnection - ) - self.worker.simulationRunCompleted.connect( - self._handle_simulation_run_done, QtCore.Qt.ConnectionType.QueuedConnection - ) - self.worker.simulationRunMismatchBetweenOutputStates.connect( - self._handle_simulation_runs_stopped_after_first_failure, QtCore.Qt.ConnectionType.QueuedConnection + self.worker.finished.connect( + self._handle_all_simulation_run_executions_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.errDuringSimulationRun.connect( - self._handle_simulation_runs_stopped_due_to_err, QtCore.Qt.ConnectionType.QueuedConnection + self.worker.batchCompleted.connect( + self._handle_simulation_run_execution_batch_done, QtCore.Qt.ConnectionType.QueuedConnection ) + self.worker.failed.connect(self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._enqueue_next_simulation_run(0) - self._change_dialog_cancellation_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(True) - # TODO: Mark remaining member functions as private via underscore prefix? - # TODO: Not all simulation runs are executed? (2 out of 10) but no error is printed to the console or shown in the GUI. - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def _handle_all_simulation_runs_done(self) -> None: - self._request_worker_cancellation() - self._await_worker_thread_completion() - - if self.num_completed_simulation_runs == self.expected_total_num_simulation_runs: - self.simulation_run_progress_lbl.setText( - f"Finished all {self.expected_total_num_simulation_runs} simulation runs!" - ) + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + # Ask for confirmation before closing + if self._handle_simulation_runs_cancel_button_click(): + if not self.error_text_lbl.text(): + self.accept() + else: + self.reject() else: - self.simulation_run_progress_lbl.setText( - f"Finished {self.num_completed_simulation_runs} out of all {self.expected_total_num_simulation_runs} simulation runs!" - ) - - @QtCore.pyqtSlot(int, Exception) # type: ignore[untyped-decorator] - def _handle_simulation_runs_stopped_due_to_err(self, simulation_run_num_that_failed: int, err: Exception) -> None: - self.simulation_run_err_lbl.setText( - f"Unexpected {err=}, {type(err)=} during execution of simulation run {simulation_run_num_that_failed}" - ) - self._request_worker_cancellation() - self._await_worker_thread_completion() + event.ignore() - @QtCore.pyqtSlot(ToBeExecutedSimulationRun) # type: ignore[untyped-decorator] - def _handle_simulation_runs_stopped_after_first_failure( - self, simulation_run_causing_err: ToBeExecutedSimulationRun - ) -> None: - self._update_progress_controls(simulation_run_causing_err.simulation_run_number) - self._request_worker_cancellation() - self._await_worker_thread_completion() + @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] + def _handle_all_simulation_run_executions_done(self, was_cancellation_requested: bool) -> None: + self.progress_info_text_lbl.setText("Simulation run execution finished!") + log_info_to_console("Simulation run execution finished!") - def _request_worker_cancellation(self) -> None: - if self.worker is not None: - self.worker.request_cancellation() - self._change_dialog_cancellation_button_enable_state(False) - self.simulation_run_progress_bar.setVisible(False) + if self.progress_bar is not None: + self.progress_bar.setVisible(False) - def _change_dialog_cancellation_button_enable_state(self, should_button_be_enabled: bool) -> None: - dialog_cancel_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( - QtWidgets.QDialogButtonBox.StandardButton.Cancel - ) + if not was_cancellation_requested: + if self.worker is not None: + self._request_worker_cancellation() + if self.worker_thread is not None: + self._shutdown_worker_thread_and_await_completion() - if dialog_cancel_button is None: - return + self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) - dialog_cancel_button.setEnabled(should_button_be_enabled) - - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 - # Ask for confirmation before closing - if self.worker is None or self.handle_simulation_runs_cancel_button_click(): - self.accept() - else: - event.ignore() + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_simulation_runs_failure(self, err: Exception) -> None: + self._handle_non_recoverable_error(err) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_simulation_runs_cancel_button_click(self) -> bool: - clicked_button_in_confirmation_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( - self, - "Cancellation of simulation runs requested!", - "Are you sure that you want to stop the execution of the simulation runs?", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) + if self.worker is None: + return True - if clicked_button_in_confirmation_dialog == QtWidgets.QMessageBox.StandardButton.Ok: - self._request_worker_cancellation() + if show_optionally_cancellable_notification( + message_box_type=MessageBoxType.QUESTION, + message_box_parent=self, + message_box_title="Cancellation of simulation runs requested!", + message_box_content="Are you sure that you want to stop the execution of the simulation runs?", + is_cancellable=True, + log_contents=False, + ): + log_info_to_console("Cancellation of simulation run execution requested!") + self._handle_non_recoverable_error(None) return True return False - @QtCore.pyqtSlot(SimulationRunResult) # type: ignore[untyped-decorator] - def _handle_simulation_run_done(self, simulation_run_result: SimulationRunResult) -> None: - self._update_progress_controls(simulation_run_result.simulation_run_number) - try: - self.simulation_runs_model.update_model_using_simulation_run_result( - self.simulation_runs_model.index(simulation_run_result.simulation_run_number), - simulation_run_result.actual_output_state, - simulation_run_result.do_expected_and_actual_outputs_match, - simulation_run_result.execution_runtime_in_ms, - ) - except ValueError as err: - self.simulation_run_err_lbl.setText( - f"Unexpected {err=}, {type(err)=} during update of simulation run model after successful execution of simulation run {simulation_run_result.simulation_run_number}" - ) - self._request_worker_cancellation() - else: - self._enqueue_next_simulation_run(simulation_run_result.simulation_run_number + 1) + @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] + def _handle_simulation_run_execution_batch_done( + self, simulation_run_execution_duration_in_seconds: float, batch_data: object + ) -> None: + if self.stop_processing_recv_batches: + return - def _enqueue_next_simulation_run(self, simulation_run_number: int) -> None: - next_simulation_run: SimulationRunModel | None = self.simulation_runs_model.get_simulation_run_model( - simulation_run_number - ) + if not SimulationRunWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunResult): + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.INFO, + message_box_parent=self, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be a list of SimulationRunResults but was actually {type(batch_data)}. Skipping batch!", + is_cancellable=False, + ) + if self.worker is not None: + self.worker.ack_batch_processed() + return - if self.worker is None: + casted_batch_data: Final[list[SimulationRunResult]] = cast("list[SimulationRunResult]", batch_data) + generated_batch_size: Final[int] = len(casted_batch_data) + if self.shared_simulation_runs_model is None: + log_error_to_console("Shared simulation runs model was not initialized during handling of batch!") + self._handle_non_recoverable_error(None) return - if next_simulation_run is None: - self._request_worker_cancellation() - else: - self.worker.queue_new_simulation_run( - ToBeExecutedSimulationRun( - simulation_run_number, next_simulation_run.input_state, next_simulation_run.expected_output_state + + to_be_updated_simulation_run_number: int = 0 + try: + for i in range(generated_batch_size): + to_be_updated_simulation_run_number = casted_batch_data[i].simulation_run_number + self.shared_simulation_runs_model.update_model_using_simulation_run_result( + self.shared_simulation_runs_model.index(to_be_updated_simulation_run_number), + casted_batch_data[i].actual_output_state, + casted_batch_data[i].do_expected_and_actual_outputs_match, + simulation_run_execution_duration_in_seconds / 1000 + if simulation_run_execution_duration_in_seconds > 0 + else 0, ) + except Exception as err: + self._handle_non_recoverable_error( + f"Error during update of shared simulation run model with data from simulation run execution result of simulation run #{to_be_updated_simulation_run_number}, reason: {SimulationRunDialog._stringify_error(err)}" ) + return - def _await_worker_thread_completion(self) -> None: + if self.worker is not None: + self.worker.ack_batch_processed() + + self._update_progress_text_with_batch_info(generated_batch_size, simulation_run_execution_duration_in_seconds) + self._accumulate_and_update_total_runtime(simulation_run_execution_duration_in_seconds) + self.num_executed_simulation_runs += generated_batch_size + if self.progress_bar is not None: + self.progress_bar.setValue(self.num_executed_simulation_runs) + + def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: + self.progress_info_text_lbl.setText("") + if self.progress_bar is not None: + self.progress_bar.setVisible(False) + + if err is not None: + self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) + + if self.worker is not None: + self._request_worker_cancellation() if self.worker_thread is not None: - self.worker_thread.quit() - self.worker_thread.wait() - self.simulation_run_progress_lbl.setText("Simulation run thread finished!") - - def _reset_workers(self) -> None: - self.worker_thread = None - self.worker = None - - def _update_progress_controls(self, completed_simulation_run: int) -> None: - self.simulation_run_progress_lbl.setText(f"Completed simulation run {completed_simulation_run}") - self.simulation_run_progress_bar.setValue(self.num_completed_simulation_runs) - self.num_completed_simulation_runs += 1 + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py index 54b7cd12..61a859a4 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_model.py @@ -137,14 +137,9 @@ def do_output_states_match( msg = f"Expected output state to have {expected_output_state.size()} qubits but actual output state contained {actual_output_state.size()} qubits!" raise ValueError(msg) - do_expected_and_actual_input_states_match = True - for qubit in range(actual_output_state.size()): - do_expected_and_actual_input_states_match &= actual_output_state.test(qubit) == expected_output_state.test( - qubit - ) - if not do_expected_and_actual_input_states_match: - break - return do_expected_and_actual_input_states_match + return all( + actual_output_state.test(i) == expected_output_state.test(i) for i in range(actual_output_state.size()) + ) @staticmethod def _update_n_bit_values_container_qubit_value( diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_worker.py b/python/mqt/syrec/simulation_view/qt_simulation_run_worker.py new file mode 100644 index 00000000..b9eb8406 --- /dev/null +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_worker.py @@ -0,0 +1,144 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore + +from mqt import syrec + +from ..logger_utils import log_error_to_console +from .cancellable_base_worker import CancellableBaseWorker +from .qt_simulation_run_model import SimulationRunModel + +if TYPE_CHECKING: + from .cancellable_base_worker import BatchTimestamps + from .qt_simulation_run_model import QtSimulationRunModel + + +@dataclass(frozen=True) +class SimulationRunResult: + simulation_run_number: int + actual_output_state: syrec.n_bit_values_container + do_expected_and_actual_outputs_match: bool | None + + +class SimulationRunWorker(CancellableBaseWorker): + def __init__( + self, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + shared_simulation_runs_model: QtSimulationRunModel, + expected_input_state_size: int, + batch_size: int, + stop_at_first_output_state_mismatch: bool, + ): + super().__init__(do_batches_require_ack=True) + self.batch_size = batch_size + self.expected_input_state_size = expected_input_state_size + self.shared_simulation_runs_model = shared_simulation_runs_model + self.annotatable_quantum_computation = annotatable_quantum_computation + self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_simulations(self) -> None: + try: + SimulationRunWorker._validate_parameters(self.expected_input_state_size, self.batch_size) + self_raised_error_msg: str | None = "" + + if self.wait_on_batch_processed_acknowledgement_condition is None: + self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" + log_error_to_console(self_raised_error_msg) + self.failed.emit(ValueError(self_raised_error_msg)) + return + + batch_data: list[SimulationRunResult | None] = [None for _ in range(self.batch_size)] + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None + n_sim_runs_to_execute: Final[int] = self.shared_simulation_runs_model.rowCount(QtCore.QModelIndex()) + + batch_idx: int = 0 + curr_sim_run_num: int = 0 + do_output_states_match: bool | None = None + while not self.is_cancellation_requested() and curr_sim_run_num < n_sim_runs_to_execute: + batch_start_timestamp = SimulationRunWorker._get_timestamp() + for _ in range(self.batch_size): + if self.is_cancellation_requested() or curr_sim_run_num == n_sim_runs_to_execute: + break + + curr_sim_run_model: SimulationRunModel = SimulationRunWorker._fetch_sim_model_or_throw( + self.shared_simulation_runs_model, curr_sim_run_num + ) + curr_input_state: syrec.n_bit_values_container = curr_sim_run_model.input_state + expected_output_state: syrec.n_bit_values_container | None = ( + curr_sim_run_model.expected_output_state + ) + actual_output_state = syrec.n_bit_values_container(self.expected_input_state_size) + syrec.simple_simulation(actual_output_state, self.annotatable_quantum_computation, curr_input_state) + do_output_states_match = SimulationRunModel.do_output_states_match( + expected_output_state, actual_output_state + ) + batch_data[batch_idx] = SimulationRunResult( + curr_sim_run_num, + actual_output_state, + do_output_states_match, + ) + + if ( + self.should_stop_at_first_output_state_mismatch + and do_output_states_match is not None + and not do_output_states_match + ): + self.set_cancellation_requested_flag(True) + + curr_sim_run_num += 1 + batch_idx += 1 + if batch_idx > 0 and batch_idx != self.batch_size: + del batch_data[batch_idx:] + + batch_timestamps = SimulationRunWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) + with QtCore.QMutexLocker(self.batch_ack_mutex): + if not self.is_cancellation_requested(): + self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) + # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using + # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. + time.sleep(0.2) + + for i in range(len(batch_data)): + batch_data[i] = None + batch_idx = 0 + self.finished.emit(self.cancellation_requested) + except Exception as error: + self_raised_error_msg = f"Error in simulation run execution worker (curr. simulation run idx: {curr_sim_run_num}), reason: {type(error)=}, {error=}" + log_error_to_console(self_raised_error_msg) + self.failed.emit(error) + + @staticmethod + def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: + if expected_input_state_size < 1: + msg = f"Expected state size must be larger than 0 but was actually {expected_input_state_size}" + raise ValueError(msg) + + if batch_size < 1: + msg = f"Batch size must be larger than 0 but was actually {batch_size}" + raise ValueError(msg) + + @staticmethod + def _fetch_sim_model_or_throw(sim_runs_model: QtSimulationRunModel, sim_run_num: int) -> SimulationRunModel: + sim_run_model: SimulationRunModel | None = sim_runs_model.get_simulation_run_model(sim_run_num) + + if sim_run_model is None: + msg = f"Failed to fetch simulation run model #{sim_run_num}" + raise ValueError(msg) + return sim_run_model diff --git a/python/mqt/syrec/simulation_view/qt_simulation_worker.py b/python/mqt/syrec/simulation_view/qt_simulation_worker.py deleted file mode 100644 index 7523e993..00000000 --- a/python/mqt/syrec/simulation_view/qt_simulation_worker.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -from __future__ import annotations - -import queue -import time -from dataclasses import dataclass - -from PyQt6 import QtCore - -from mqt import syrec - -from .qt_simulation_run_model import SimulationRunModel - -# One could simplify the signal and slot declarations by defining the import: from PyQt6.QtCore import pyqtSignal as Signal, pyqtSlot as Slot - - -@dataclass(frozen=True) -class ToBeExecutedSimulationRun: - simulation_run_number: int - input_state: syrec.n_bit_values_container - expected_output_state: syrec.n_bit_values_container | None - - -@dataclass(frozen=True) -class SimulationRunResult: - simulation_run_number: int - expected_output_state: syrec.n_bit_values_container | None - actual_output_state: syrec.n_bit_values_container - do_expected_and_actual_outputs_match: bool | None - execution_runtime_in_ms: float - - -# TODO: Rework to use same cancel functionality and signals as cancellable_base_worker if possible -class SimulationWorker(QtCore.QObject): # type: ignore[misc] - simulation_run_completed = QtCore.pyqtSignal(SimulationRunResult, name="simulationRunCompleted") - simulation_run_mismatch_between_output_states = QtCore.pyqtSignal( - SimulationRunResult, name="simulationRunMismatchBetweenOutputStates" - ) - all_simulations_done = QtCore.pyqtSignal(name="allSimulationsDone") - err_during_simulation_run = QtCore.pyqtSignal(int, Exception, name="errDuringSimulationRun") - - def __init__( - self, - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - stop_at_first_output_state_mismatch: bool, - ): - super().__init__() - - self.annotatable_quantum_computation = annotatable_quantum_computation - self.cancellation_requested: bool = False - self.simulation_run_queue: queue.SimpleQueue[ToBeExecutedSimulationRun | None] = queue.SimpleQueue() - self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch - - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def start_simulations(self) -> None: - while not self.cancellation_requested: - dequeued_element: ToBeExecutedSimulationRun | None = self.simulation_run_queue.get() - if dequeued_element is None: - break - - try: - simulation_execution_start_time_in_seconds: float = time.time() - # simulation_run_execution_statistics = syrec.statistics() - - actual_output_state = syrec.n_bit_values_container(dequeued_element.input_state.size()) - # syrec.simple_simulation(actual_output_state, self.annotatable_quantum_computation, dequeued_element.input_state, simulation_run_execution_statistics) - syrec.simple_simulation( - actual_output_state, self.annotatable_quantum_computation, dequeued_element.input_state - ) - do_expected_and_actual_input_states_match: bool | None = SimulationRunModel.do_output_states_match( - dequeued_element.expected_output_state, actual_output_state - ) - simulation_execution_end_time_in_seconds: float = time.time() - simulation_execution_runtime_in_ms: float = ( - simulation_execution_end_time_in_seconds - simulation_execution_start_time_in_seconds - ) / 1000 - - simulation_run_result = SimulationRunResult( - dequeued_element.simulation_run_number, - dequeued_element.expected_output_state, - actual_output_state, - do_expected_and_actual_input_states_match, - simulation_execution_runtime_in_ms, - ) - - if ( - self.should_stop_at_first_output_state_mismatch - and do_expected_and_actual_input_states_match is not None - and not do_expected_and_actual_input_states_match - ): - self.simulation_run_mismatch_between_output_states.emit(simulation_run_result) - else: - self.simulation_run_completed.emit(simulation_run_result) - # time.sleep(1) - except Exception as err: - self.err_during_simulation_run.emit(dequeued_element.simulation_run_number, err) - break - self.all_simulations_done.emit() - - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def request_cancellation(self) -> None: - self.cancellation_requested = True - self.simulation_run_queue.put(None) - - # TODO: Throw exceptions on validation errors? - @QtCore.pyqtSlot(ToBeExecutedSimulationRun) # type: ignore[untyped-decorator] - def queue_new_simulation_run(self, to_be_executed_simulation_run: ToBeExecutedSimulationRun) -> bool: - if self.cancellation_requested: - return False - - if to_be_executed_simulation_run.simulation_run_number < 0: - return False - - if ( - to_be_executed_simulation_run.expected_output_state is not None - and to_be_executed_simulation_run.expected_output_state.size() - != to_be_executed_simulation_run.input_state.size() - ): - return False - - self.simulation_run_queue.put(to_be_executed_simulation_run) - return True From d7c264052168fe4be7c2c38fa25ab0baf53a56b8 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 22 Jan 2026 18:27:26 +0100 Subject: [PATCH 38/88] Simplified input state generation loop in simulatin run generator worker --- .../all_input_states_generator_worker.py | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py index 6f21fb36..e1feceb2 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py @@ -33,59 +33,52 @@ def __init__(self, expected_input_state_size: int, batch_size: int): def start_generation(self) -> None: try: AllInputStatesGeneratorWorker._validate_parameters(self.expected_input_state_size, self.batch_size) - n_states_to_generate: int = 2**self.expected_input_state_size - n_batches: int = n_states_to_generate // self.batch_size - batch_start_timestamp: float = AllInputStatesGeneratorWorker._get_timestamp() - batch_timestamps: BatchTimestamps | None = None self_raised_error_msg: str | None = None - if self.wait_on_batch_processed_acknowledgement_condition is None: self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" log_error_to_console(self_raised_error_msg) self.failed.emit(ValueError(self_raised_error_msg)) return - first_integer_encoding_first_state_of_batch: int = 0 + integer_encoding_input_state: int = 0 + n_states_to_generate: Final[int] = 2**self.expected_input_state_size + batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] - for _ in range(n_batches): - if self.is_cancellation_requested(): - break + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None - for i in range(self.batch_size): - batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( - self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i + batch_idx: int = 0 + while not self.is_cancellation_requested() and integer_encoding_input_state < n_states_to_generate: + batch_start_timestamp = AllInputStatesGeneratorWorker._get_timestamp() + for _ in range(self.batch_size): + if self.is_cancellation_requested() or integer_encoding_input_state == n_states_to_generate: + break + + batch_data[batch_idx] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self.expected_input_state_size, integer_encoding_input_state ) + integer_encoding_input_state += 1 + batch_idx += 1 + + if batch_idx > 0 and batch_idx != self.batch_size: + del batch_data[batch_idx:] + batch_timestamps = ( AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - batch_start_timestamp = batch_timestamps.end self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) with QtCore.QMutexLocker(self.batch_ack_mutex): if not self.is_cancellation_requested(): self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) - # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. time.sleep(0.1) - first_integer_encoding_first_state_of_batch += self.batch_size - for i in range(self.batch_size): - batch_data[i] = None - n_elems_in_last_batch: int = n_states_to_generate % self.batch_size - if n_elems_in_last_batch != 0 and not self.is_cancellation_requested(): - last_batch_data: list[SimulationRunModel | None] = [None for _ in range(n_elems_in_last_batch)] - for i in range(n_elems_in_last_batch): - last_batch_data[i] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( - self.expected_input_state_size, first_integer_encoding_first_state_of_batch + i - ) - batch_timestamps = ( - AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( - batch_start_timestamp - ) - ) - self.batchCompleted.emit(batch_timestamps.duration, last_batch_data) + for i in range(len(batch_data)): + batch_data[i] = None + batch_idx = 0 self.finished.emit(self.cancellation_requested) except Exception as error: self_raised_error_msg = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" From 0b86952740412b0557b7b8db0b247c6c76ca31d2 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 22 Jan 2026 21:49:20 +0100 Subject: [PATCH 39/88] Split __init__ function creating dialog controls into separate functions for better readability --- .../qt_simulation_run_editor_dialog.py | 700 +++++++++--------- .../qt_simulation_run_json_export_dialog.py | 1 + 2 files changed, 363 insertions(+), 338 deletions(-) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index 31916449..ff540737 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from PyQt6 import QtCore, QtGui, QtWidgets @@ -26,15 +26,6 @@ ) -def stringify_some_qubits_of_n_bit_values_container( - n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int -) -> str: - if first_qubit >= n_bit_values_container.size() or first_qubit + (n_qubits - 1) >= n_bit_values_container.size(): - return "" - - return "".join(["1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits)]) - - class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] focus_out = QtCore.pyqtSignal(name="focusOut") @@ -59,6 +50,28 @@ def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 self.focus_out.emit() +QUBIT_LABEL_NAME_FORMAT: Final[str] = "q_{qubit:d}_lbl" +INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT: Final[str] = "q_{qubit:d}_in_checkB" +OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT: Final[str] = "q_{qubit:d}_out_checkB" + +QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_qubit_values_groupbox" +QREG_LABEL_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_lbl" +QREG_LAYOUT_INFO_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_layout_info_lbl" +QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_input_state" +QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_output_state" +QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_qubit_values_toggle" +QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_qubit_search_input" + +QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME: Final[str] = "output_state_value_toggle" +QREG_SEARCH_INPUT_FIELD_NAME: Final[str] = "qreg_name_search_input_field" +QREG_VALUES_VALIDATION_ERROR_LABEL_NAME: Final[str] = "qreg_values_validation_err_lbl" + +STRINGIFIED_QUBIT_VALUE_FORMAT: Final[str] = "(Value: {stringified_qubit_value:s})" +QREG_VALUES_VALIDATION_ERROR_FORMAT: Final[str] = ( + "Qubit values of quantum register '{qreg_name:s}' can only be defined as a combination of '0' or '1' literals. Additionally, the value of all qubits of the quantum register (n={expected_num_qubit_values:d}) must be specified but only {actual_num_qubit_values:d} were defined in the {input_or_output_state_ident:s} state!" +) + + # TODO: Replace 'simple' returns with QDialog.reject("") to indicate fatal errors and stop editing simulation run but also reject changes made in parent widget that opened dialog class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( @@ -87,7 +100,6 @@ def __init__( self.setSizeGripEnabled(True) self.setWindowTitle("Edit qubit values of quantum registers for simulation run") main_layout = QtWidgets.QVBoxLayout() - self.setLayout(main_layout) self.simulation_run_wrapper_box = QtWidgets.QGroupBox( "Simulation run #" + str(simulation_run_model_index.row()) @@ -95,23 +107,6 @@ def __init__( main_layout.addWidget(self.simulation_run_wrapper_box) # TODO: How to render n-dimensional variables - - self.qubit_label_name_format = "q_{qubit:d}_lbl" - self.input_state_qubit_checkbox_name_format = "q_{qubit:d}_in_checkB" - self.output_state_qubit_checkbox_name_format = "q_{qubit:d}_out_checkB" - self.stringified_qubit_value_format = "(Value: {stringified_qubit_value:s})" - self.qreg_qubit_values_groupbox_format = "qreg_{qreg_name:s}_qubit_values_groupbox" - self.qreg_label_name_format = "qreg_{qreg_name:s}_lbl" - self.qreg_layout_info_label_name_format = "qreg_{qreg_name:s}_layout_info_lbl" - self.qreg_input_state_input_field_name_format = "qreg_{qreg_name:s}_input_state" - self.qreg_output_state_input_field_name_format = "qreg_{qreg_name:s}_output_state" - self.qreg_qubit_values_toggle_button_name_format = "qreg_{qreg_name:s}_qubit_values_toggle" - self.qreg_qubit_search_input_field_name_format = "qreg_{qreg_name:s}_qubit_search_input" - self.qreg_expected_output_state_init_name_format = "output_state_value_toggle" - self.qreg_name_search_input_field_name = "qreg_name_search_input_field" - self.qreg_values_validation_error_lbl_name = "qreg_values_validation_err_lbl" - self.qreg_values_validation_error_format = "Qubit values of quantum register '{qreg_name:s}' can only be defined as a combination of '0' or '1' literals. Additionally, the value of all qubits of the quantum register (n={expected_num_qubit_values:d}) must be specified but only {actual_num_qubit_values:d} were defined in the {input_or_output_state_ident:s} state!" - # TODO: How can we determine whether qubits are readonly self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 self.edit_of_qubit_values_enabled: bool = False @@ -119,35 +114,15 @@ def __init__( # TODO: Add validators quantum_register_controls_grid_layout = QtWidgets.QGridLayout() self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) - - quantum_register_search_controls_layout = QtWidgets.QHBoxLayout() - quantum_register_search_label = QtWidgets.QLabel("Quantum register:") - quantum_register_search_input_field = QtWidgets.QLineEdit(objectName=self.qreg_name_search_input_field_name) - quantum_register_search_input_field.setPlaceholderText("") - - quantum_register_name_regular_expr = QtCore.QRegularExpression(R"(^([_A-Za-z]\w*)?$)") - quantum_register_name_validator = QtGui.QRegularExpressionValidator(quantum_register_name_regular_expr, self) - quantum_register_search_input_field.setValidator(quantum_register_name_validator) - - qreg_name_search_completer = QtWidgets.QCompleter([qreg_layout.qreg_name for qreg_layout in self.qreg_layouts]) - qreg_name_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) - quantum_register_search_input_field.setCompleter(qreg_name_search_completer) - - quantum_register_search_trigger_button = QtWidgets.QPushButton("Search") - quantum_register_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) - - quantum_register_search_controls_layout.addWidget(quantum_register_search_label) - quantum_register_search_controls_layout.addWidget(quantum_register_search_input_field) - quantum_register_search_controls_layout.addWidget(quantum_register_search_trigger_button) quantum_register_controls_grid_layout.addLayout( - quantum_register_search_controls_layout, 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + self.create_qreg_search_controls(), 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) init_expected_output_state_button = QtWidgets.QPushButton( "Init output state" if self.edited_simulation_run_model.expected_output_state is None else "Clear output state", - objectName=self.qreg_expected_output_state_init_name_format, + objectName=QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, ) init_expected_output_state_button.clicked.connect(self.handle_init_expected_output_state_button_click) @@ -170,252 +145,68 @@ def __init__( ) n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^[0-1]*$") - n_bit_values_container_contents_validator = QtGui.QRegularExpressionValidator( - n_bit_values_container_contents_validator_regular_expr, self - ) + QtGui.QRegularExpressionValidator(n_bit_values_container_contents_validator_regular_expr, self) - quantum_register_controls_grid_row: int = 2 + qreg_controls_grid_row: int = 2 for qreg_layout in self.qreg_layouts: - first_qubit_of_qreg: int = qreg_layout.first_qubit_of_qreg - n_qubits_of_qreg: int = qreg_layout.qreg_size qreg_name: str = qreg_layout.qreg_name quantum_register_label = QtWidgets.QLabel( - "Quantum register: " + qreg_name, objectName=self.qreg_label_name_format.format(qreg_name=qreg_name) - ) - - quantum_register_layout_info_label = QtWidgets.QLabel( - f"(First qubit: {first_qubit_of_qreg} - Num. qubits: {n_qubits_of_qreg})", - objectName=self.qreg_layout_info_label_name_format.format(qreg_name=qreg_name), - ) - quantum_register_layout_info_label.setStyleSheet("QLabel { color : grey; }") - - input_state_edit_field = LineEditWithDynamicWidth(n_qubits_of_qreg) - input_state_edit_field.setObjectName( - self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) - ) - input_state_edit_field.setText( - stringify_some_qubits_of_n_bit_values_container( - initial_input_state, first_qubit_of_qreg, n_qubits_of_qreg - ) - ) - input_state_edit_field.setCursorPosition(0) - input_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) - # input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) - input_state_edit_field.setValidator(n_bit_values_container_contents_validator) - # The QLineEdit editingFinished signal is only triggered when its input satisfies the set inputmask/validator or when return/enter is pressed or if the QLineEdit loses focus. - # However, during testing entering an invalid quantum register state in the input/output state validation seems to not trigger after entering an invalid value into an QLineEdit - # and moving focus to another item. Only after entering a second invalid state the validation is triggered. Only when confirming the changes with enter/return is the validation triggered immediately. - # One could in a custom overwrite of the QLineEdit class use the focusOutEvent to emit a custom signal when the QLineEdit element looses focus or use the application-level focusChanged signal to determine - # whether the QLineEdit widget lost focus. - input_state_edit_field.editingFinished.connect( - lambda associated_qreg_name=qreg_name, - expected_text_length=n_qubits_of_qreg, - is_editing_input_state=True: self.handle_input_or_output_state_text_change( - associated_qreg_name, expected_text_length, is_editing_input_state - ) - ) - input_state_edit_field.focusOut.connect( - lambda associated_qreg_name=qreg_name, - expected_text_length=n_qubits_of_qreg, - is_editing_input_state=True: self.handle_input_or_output_state_text_change( - associated_qreg_name, expected_text_length, is_editing_input_state - ) - ) - - output_state_edit_field = LineEditWithDynamicWidth(n_qubits_of_qreg) - output_state_edit_field.setObjectName( - self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) - ) - if initial_expected_output_state is not None: - output_state_edit_field.setText( - stringify_some_qubits_of_n_bit_values_container( - initial_expected_output_state, first_qubit_of_qreg, n_qubits_of_qreg - ) - ) - # output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) - else: - output_state_edit_field.setEnabled(False) - output_state_edit_field.setPlaceholderText("-") - - output_state_edit_field.setCursorPosition(0) - output_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) - output_state_edit_field.setValidator(n_bit_values_container_contents_validator) - output_state_edit_field.editingFinished.connect( - lambda associated_qreg_name=qreg_name, - expected_text_length=n_qubits_of_qreg, - is_editing_input_state=False: self.handle_input_or_output_state_text_change( - associated_qreg_name, expected_text_length, is_editing_input_state - ) - ) - output_state_edit_field.focusOut.connect( - lambda associated_qreg_name=qreg_name, - expected_text_length=n_qubits_of_qreg, - is_editing_input_state=False: self.handle_input_or_output_state_text_change( - associated_qreg_name, expected_text_length, is_editing_input_state - ) - ) - - edit_qubit_values_toggle_button = QtWidgets.QPushButton( - "Edit qubit values", - objectName=self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name), - ) - # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton - edit_qubit_values_toggle_button.clicked.connect( - lambda _, associated_qreg_name=qreg_name: self.handle_qreg_qubit_values_edit_toggle_button_click( - associated_qreg_name - ) + "Quantum register: " + qreg_name, objectName=QREG_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) ) - quantum_register_controls_grid_layout.addWidget( quantum_register_label, - quantum_register_controls_grid_row, + qreg_controls_grid_row, 0, alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) + + input_state_edit_field = self.create_in_or_out_state_edit_field( + qreg_layout, optional_qreg_qubit_values=initial_input_state, is_created_for_input_state=True + ) quantum_register_controls_grid_layout.addWidget( input_state_edit_field, - quantum_register_controls_grid_row, + qreg_controls_grid_row, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) + + output_state_edit_field = self.create_in_or_out_state_edit_field( + qreg_layout, optional_qreg_qubit_values=initial_expected_output_state, is_created_for_input_state=False + ) quantum_register_controls_grid_layout.addWidget( output_state_edit_field, - quantum_register_controls_grid_row, + qreg_controls_grid_row, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) - quantum_register_controls_grid_layout.addWidget( - edit_qubit_values_toggle_button, quantum_register_controls_grid_row, 3 - ) - quantum_register_controls_grid_layout.addWidget( - quantum_register_layout_info_label, quantum_register_controls_grid_row + 1, 0 - ) - quantum_register_controls_grid_row += 1 - n_cols_in_quantum_register_controls_grid_layout: int = 3 - - # TODO: Scroll area - input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( - "Qubit values", objectName=self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg_name) - ) - input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() - input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) - - qubit_search_layout = QtWidgets.QHBoxLayout() - - qubit_search_label = QtWidgets.QLabel("Qubit") - qubit_search_layout.addWidget(qubit_search_label) - qubit_search_input_field = QtWidgets.QLineEdit( - objectName=self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg_name) - ) - qubit_search_input_field.setPlaceholderText("") - - qubit_search_completer = QtWidgets.QCompleter( - SimulationRunEditorDialog.get_internal_qubit_labels_for_qreg( - self.annotatable_quantum_computation, first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg - ) + edit_qubit_values_toggle_button = QtWidgets.QPushButton( + "Edit qubit values", objectName=QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) - qubit_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) - qubit_search_input_field.setCompleter(qubit_search_completer) - - qubit_search_layout.addWidget(qubit_search_input_field) - - qubit_search_trigger_button = QtWidgets.QPushButton("Search") - qubit_search_trigger_button.clicked.connect( - lambda _, associated_qreg_name=qreg_name: self.handle_qubit_search_trigger_button_click( + quantum_register_controls_grid_layout.addWidget(edit_qubit_values_toggle_button, qreg_controls_grid_row, 3) + # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton + edit_qubit_values_toggle_button.clicked.connect( + lambda _, associated_qreg_name=qreg_name: self.handle_qreg_qubit_values_edit_toggle_button_click( associated_qreg_name ) ) - qubit_search_layout.addWidget(qubit_search_trigger_button) - input_output_qubits_value_controls_groupbox_layout.addLayout( - qubit_search_layout, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter + qreg_controls_grid_row += 1 + quantum_register_layout_info_label = QtWidgets.QLabel( + f"(First qubit: {qreg_layout.first_qubit_of_qreg} - Num. qubits: {qreg_layout.qreg_size})", + objectName=QREG_LAYOUT_INFO_NAME_FORMAT.format(qreg_name=qreg_name), + ) + quantum_register_layout_info_label.setStyleSheet("QLabel { color : grey; }") + quantum_register_controls_grid_layout.addWidget( + quantum_register_layout_info_label, qreg_controls_grid_row, 0 ) - for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): - one_based_relative_qubit_idx_in_qreg: int = (qubit - first_qubit_of_qreg) + 1 - fetched_internal_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( - qubit, syrec.qubit_label_type.internal - ) - qubit_label = QtWidgets.QLabel( - "Qubit: " + fetched_internal_qubit_label - if fetched_internal_qubit_label is not None - else "", - objectName=self.qubit_label_name_format.format(qubit=qubit), - ) - input_output_qubits_value_controls_groupbox_layout.addWidget( - qubit_label, one_based_relative_qubit_idx_in_qreg, 0 - ) - - input_state_qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=self.input_state_qubit_checkbox_name_format.format(qubit=qubit) - ) - input_state_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( - stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( - initial_input_state.test(qubit), return_as_high_low_state=True - ) - ) - ) - - input_state_qubit_value_checkbox.checkStateChanged.connect( - lambda _, - associated_qreg_name=qreg_name, - associated_qubit=qubit, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - - 1: self.handle_input_state_qubit_value_checkbox_state_change( - associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register - ) - ) - - input_output_qubits_value_controls_groupbox_layout.addWidget( - input_state_qubit_value_checkbox, - one_based_relative_qubit_idx_in_qreg, - 1, - alignment=QtCore.Qt.AlignmentFlag.AlignLeft, - ) - - output_state_qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=self.output_state_qubit_checkbox_name_format.format(qubit=qubit) - ) - output_state_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( - stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( - None - if initial_expected_output_state is None - else initial_expected_output_state.test(qubit), - return_as_high_low_state=True, - ) - ) - ) - output_state_qubit_value_checkbox.checkStateChanged.connect( - lambda _, - associated_qreg_name=qreg_name, - associated_qubit=qubit, - relative_qubit_index_in_quantum_register=one_based_relative_qubit_idx_in_qreg - - 1: self.handle_output_state_qubit_value_checkbox_state_change( - associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register - ) - ) - output_state_qubit_value_checkbox.setEnabled(initial_expected_output_state is not None) - input_output_qubits_value_controls_groupbox_layout.addWidget( - output_state_qubit_value_checkbox, - one_based_relative_qubit_idx_in_qreg, - 2, - alignment=QtCore.Qt.AlignmentFlag.AlignLeft, - ) - - # TODO: How can the column widths of the input fields and the checkbox columns be synced? - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 1) - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) - input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) - - quantum_register_controls_grid_row += 1 - input_output_qubits_value_controls_groupbox.setVisible(False) + n_cols_in_quantum_register_controls_grid_layout: int = 3 + qreg_controls_grid_row += 1 quantum_register_controls_grid_layout.addWidget( - input_output_qubits_value_controls_groupbox, - quantum_register_controls_grid_row, + self.create_qubit_controls_groupbox(qreg_layout, initial_input_state, initial_expected_output_state), + qreg_controls_grid_row, 0, 1, n_cols_in_quantum_register_controls_grid_layout + 1, @@ -426,14 +217,14 @@ def __init__( 2, 2, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) quantum_register_controls_grid_layout.addItem( - quantum_register_controls_grid_spacer_widget, quantum_register_controls_grid_row, 5 + quantum_register_controls_grid_spacer_widget, qreg_controls_grid_row, 5 ) - quantum_register_controls_grid_row += 1 + qreg_controls_grid_row += 1 # Add spacer item to take up remaining space between last quantum register elements and bottom of parent group box without stretching the spacing between the already added controls in the group box quantum_register_controls_grid_layout.addItem( QtWidgets.QSpacerItem(2, 2, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding), - quantum_register_controls_grid_row, + qreg_controls_grid_row, 0, 1, 5, @@ -449,7 +240,7 @@ def __init__( simulation_run_scroll_area.setWidgetResizable(True) main_layout.addWidget(simulation_run_scroll_area) - qreg_values_validation_error_label = QtWidgets.QLabel(objectName=self.qreg_values_validation_error_lbl_name) + qreg_values_validation_error_label = QtWidgets.QLabel(objectName=QREG_VALUES_VALIDATION_ERROR_LABEL_NAME) qreg_values_validation_error_label.setStyleSheet("QLabel { color : red; }") main_layout.addWidget(qreg_values_validation_error_label) @@ -464,28 +255,29 @@ def __init__( self.dialog_button_box.rejected.connect(self.reject) main_layout.addWidget(self.dialog_button_box) + self.setLayout(main_layout) def handle_quantum_register_name_search(self) -> None: for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name qreg_name_search_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_name_search_input_field_name + QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME ) qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLabel, self.qreg_label_name_format.format(qreg_name=qreg_name) + QtWidgets.QLabel, QREG_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) ) qreg_layout_info_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLabel, self.qreg_layout_info_label_name_format.format(qreg_name=qreg_name) + QtWidgets.QLabel, QREG_LAYOUT_INFO_NAME_FORMAT.format(qreg_name=qreg_name) ) qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) + QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) ) @@ -514,11 +306,11 @@ def handle_input_state_qubit_value_checkbox_state_change( ) -> None: associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - self.input_state_qubit_checkbox_name_format.format(qubit=associated_qubit), + INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), ) qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=associated_qreg_name) + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) if associated_qubit_value_checkbox is None or qreg_input_state_input_field is None: @@ -541,7 +333,7 @@ def handle_input_state_qubit_value_checkbox_state_change( return associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) + STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) ) curr_stringified_input_state: str = qreg_input_state_input_field.text() @@ -556,11 +348,11 @@ def handle_output_state_qubit_value_checkbox_state_change( ) -> None: associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - self.output_state_qubit_checkbox_name_format.format(qubit=associated_qubit), + OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), ) qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=associated_qreg_name) + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: @@ -580,7 +372,7 @@ def handle_output_state_qubit_value_checkbox_state_change( None, return_as_high_low_state=True ) associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) + STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) ) return @@ -594,7 +386,7 @@ def handle_output_state_qubit_value_checkbox_state_change( return associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format(stringified_qubit_value=stringified_updated_qubit_value) + STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) ) curr_stringified_output_state: str = qreg_output_state_input_field.text() qreg_output_state_input_field.setText( @@ -603,29 +395,226 @@ def handle_output_state_qubit_value_checkbox_state_change( + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] ) - def get_internal_qubit_labels_for_qreg( - self: syrec.annotatable_quantum_computation, first_qubit_of_qreg: int, n_qubits_in_qreg: int - ) -> list[str]: - internal_qubit_labels: list[str] = [] - for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_in_qreg): - fetched_internal_qubit_label: str | None = self.get_qubit_label(qubit, syrec.qubit_label_type.internal) - if fetched_internal_qubit_label is None: - continue - internal_qubit_labels.append(fetched_internal_qubit_label) - return internal_qubit_labels - def show_error_msg_dialog(self, title: str, error_msg: str) -> None: QtWidgets.QMessageBox.critical(self, title, error_msg, defaultButton=QtWidgets.QMessageBox.StandardButton.Ok) - @staticmethod - def stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bool) -> str: - if qubit_value is None: - return "UNKNOWN" if return_as_high_low_state else "-" + def create_qreg_search_controls(self) -> QtWidgets.QLayout: + qreg_search_controls_layout = QtWidgets.QHBoxLayout() + qreg_search_label = QtWidgets.QLabel("Quantum register:") + qreg_search_input_field = QtWidgets.QLineEdit(objectName=QREG_SEARCH_INPUT_FIELD_NAME) + qreg_search_input_field.setPlaceholderText("") - if qubit_value is True: - return "HIGH" if return_as_high_low_state else "1" + qreg_name_regular_expr = QtCore.QRegularExpression(R"(^([_A-Za-z]\w*)?$)") + qreg_name_validator = QtGui.QRegularExpressionValidator(qreg_name_regular_expr, self) + qreg_search_input_field.setValidator(qreg_name_validator) - return "LOW" if return_as_high_low_state else "0" + qreg_name_search_completer = QtWidgets.QCompleter([qreg_layout.qreg_name for qreg_layout in self.qreg_layouts]) + qreg_name_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) + qreg_search_input_field.setCompleter(qreg_name_search_completer) + + qreg_search_trigger_button = QtWidgets.QPushButton("Search") + qreg_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) + + qreg_search_controls_layout.addWidget(qreg_search_label) + qreg_search_controls_layout.addWidget(qreg_search_input_field) + qreg_search_controls_layout.addWidget(qreg_search_trigger_button) + return qreg_search_controls_layout + + def create_in_or_out_state_edit_field( + self, + qreg_layout: QuantumRegisterLayout, + optional_qreg_qubit_values: syrec.n_bit_values_container | None, + is_created_for_input_state: bool, + ) -> LineEditWithDynamicWidth: + in_or_out_state_edit_field = LineEditWithDynamicWidth(qreg_layout.qreg_size) + in_or_out_state_edit_field.setObjectName( + QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_layout.qreg_name) + if is_created_for_input_state + else QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_layout.qreg_name) + ) + + if optional_qreg_qubit_values is not None: + in_or_out_state_edit_field.setText( + SimulationRunEditorDialog.stringify_some_qubits_of_n_bit_values_container( + optional_qreg_qubit_values, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size + ) + ) + # output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) + else: + in_or_out_state_edit_field.setEnabled(False) + in_or_out_state_edit_field.setPlaceholderText("-") + + in_or_out_state_edit_field.setCursorPosition(0) + in_or_out_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) + # input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) + in_or_out_state_edit_field.setValidator( + QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(R"^[0-1]*$"), self) + ) + # The QLineEdit editingFinished signal is only triggered when its input satisfies the set inputmask/validator or when return/enter is pressed or if the QLineEdit loses focus. + # However, during testing entering an invalid quantum register state in the input/output state validation seems to not trigger after entering an invalid value into an QLineEdit + # and moving focus to another item. Only after entering a second invalid state the validation is triggered. Only when confirming the changes with enter/return is the validation triggered immediately. + # One could in a custom overwrite of the QLineEdit class use the focusOutEvent to emit a custom signal when the QLineEdit element looses focus or use the application-level focusChanged signal to determine + # whether the QLineEdit widget lost focus. + in_or_out_state_edit_field.editingFinished.connect( + lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_created_for_input_state: ( + self.handle_input_or_output_state_text_change( + associated_qreg_name, expected_text_length, is_editing_input_state + ) + ) + ) + in_or_out_state_edit_field.focusOut.connect( + lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_created_for_input_state: ( + self.handle_input_or_output_state_text_change( + associated_qreg_name, expected_text_length, is_editing_input_state + ) + ) + ) + return in_or_out_state_edit_field + + def create_in_or_out_state_qubit_value_checkbox( + self, + associated_qreg_name: str, + optional_qreg_qubit_values: syrec.n_bit_values_container | None, + associated_qubit: int, + relative_qubit_index_in_qreg: int, + is_qubit_in_input_state: bool, + ) -> QtWidgets.QCheckBox: + qubit_value_checkbox = QtWidgets.QCheckBox( + objectName=INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit) + if is_qubit_in_input_state + else OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit) + ) + qubit_value_checkbox.setText( + STRINGIFIED_QUBIT_VALUE_FORMAT.format( + stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + None + if optional_qreg_qubit_values is None + else optional_qreg_qubit_values.test(relative_qubit_index_in_qreg), + return_as_high_low_state=True, + ) + ) + ) + qubit_value_checkbox.setEnabled(optional_qreg_qubit_values is not None) + qubit_value_checkbox.checkStateChanged.connect( + lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( + self.handle_input_state_qubit_value_checkbox_state_change( + associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + ) + if is_qubit_in_input_state + else self.handle_output_state_qubit_value_checkbox_state_change( + associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + ) + ) + ) + return qubit_value_checkbox + + def create_search_controls_for_qubits_of_qreg( + self, associated_qreg_layout: QuantumRegisterLayout + ) -> QtWidgets.QLayout: + qubit_search_layout = QtWidgets.QHBoxLayout() + qubit_search_label = QtWidgets.QLabel("Qubit") + qubit_search_layout.addWidget(qubit_search_label) + + associated_qreg_name: Final[str] = associated_qreg_layout.qreg_name + first_qreg_qubit: Final[int] = associated_qreg_layout.first_qubit_of_qreg + last_qreg_qubit: Final[int] = first_qreg_qubit + associated_qreg_layout.qreg_size + + qubit_search_input_field = QtWidgets.QLineEdit( + objectName=QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) + ) + qubit_search_input_field.setPlaceholderText("") + + qubit_search_completer = QtWidgets.QCompleter( + SimulationRunEditorDialog.get_internal_qubit_labels_for_qreg( + self.annotatable_quantum_computation, first_qreg_qubit, last_qreg_qubit + ) + ) + qubit_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) + qubit_search_input_field.setCompleter(qubit_search_completer) + + qubit_search_layout.addWidget(qubit_search_input_field) + + qubit_search_trigger_button = QtWidgets.QPushButton("Search") + qubit_search_trigger_button.clicked.connect( + lambda _, associated_qreg_name=associated_qreg_name: self.handle_qubit_search_trigger_button_click( + associated_qreg_name + ) + ) + qubit_search_layout.addWidget(qubit_search_trigger_button) + return qubit_search_layout + + # TODO: Scroll area + def create_qubit_controls_groupbox( + self, + associated_qreg_layout: QuantumRegisterLayout, + initial_input_state: syrec.n_bit_values_container, + initial_expected_output_state: syrec.n_bit_values_container | None, + ) -> QtWidgets.QWidget: + input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() + input_output_qubits_value_controls_groupbox_layout.addLayout( + self.create_search_controls_for_qubits_of_qreg(associated_qreg_layout), + 0, + 0, + 1, + 1, + QtCore.Qt.AlignmentFlag.AlignCenter, + ) + + first_qubit_of_qreg: Final[int] = associated_qreg_layout.first_qubit_of_qreg + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + associated_qreg_layout.qreg_size): + # Due to first row (idx=0) of group box containing the qubit searc controls, the qubit value controls start in row 1. + relative_qubit_index_in_qreg: int = qubit - first_qubit_of_qreg + fetched_internal_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ) + qubit_label = QtWidgets.QLabel( + "Qubit: " + fetched_internal_qubit_label if fetched_internal_qubit_label is not None else "", + objectName=QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit), + ) + input_output_qubits_value_controls_groupbox_layout.addWidget( + qubit_label, 1 + relative_qubit_index_in_qreg, 0 + ) + + input_state_qubit_value_checkbox: QtWidgets.QCheckBox = self.create_in_or_out_state_qubit_value_checkbox( + associated_qreg_layout.qreg_name, + initial_input_state, + associated_qubit=qubit, + relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, + is_qubit_in_input_state=True, + ) + input_output_qubits_value_controls_groupbox_layout.addWidget( + input_state_qubit_value_checkbox, + 1 + relative_qubit_index_in_qreg, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + ) + + output_state_qubit_value_checkbox: QtWidgets.QCheckBox = self.create_in_or_out_state_qubit_value_checkbox( + associated_qreg_layout.qreg_name, + initial_expected_output_state, + associated_qubit=qubit, + relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, + is_qubit_in_input_state=False, + ) + input_output_qubits_value_controls_groupbox_layout.addWidget( + output_state_qubit_value_checkbox, + 1 + relative_qubit_index_in_qreg, + 2, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + ) + + # TODO: How can the column widths of the input fields and the checkbox columns be synced? + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 1) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) + input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) + + input_output_qubits_value_controls_groupbox = QtWidgets.QGroupBox( + "Qubit values", + objectName=QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_qreg_layout.qreg_name), + ) + input_output_qubits_value_controls_groupbox.setVisible(False) + input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) + return input_output_qubits_value_controls_groupbox def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: for qreg_layout in self.qreg_layouts: @@ -634,7 +623,7 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, - self.qreg_qubit_values_groupbox_format.format(qreg_name=associated_quantum_register_name), + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), ) if qreg_qubits_groupbox is None: # TODO: This should not happen @@ -642,7 +631,7 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( QtWidgets.QLineEdit, - self.qreg_qubit_search_input_field_name_format.format(qreg_name=associated_quantum_register_name), + QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), ) if qubit_search_input_field is None: # TODO: This should not happen @@ -652,13 +641,13 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size ): qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( - QtWidgets.QLabel, self.qubit_label_name_format.format(qubit=qubit) + QtWidgets.QLabel, QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit) ) input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, self.input_state_qubit_checkbox_name_format.format(qubit=qubit) + QtWidgets.QCheckBox, INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format(qubit=qubit) + QtWidgets.QCheckBox, OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) if ( qubit_value_label is None @@ -680,26 +669,26 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name qreg_name: str = qreg_layout.qreg_name # TODO: QtCore.Qt.FindDirectChildrenOnly qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, self.qreg_qubit_values_groupbox_format.format(qreg_name=qreg_name) + QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name) ) qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) + QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, - self.qreg_expected_output_state_init_name_format.format(qreg_name=associated_qreg_name), + QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME.format(qreg_name=associated_qreg_name), ) ) qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( qubit_values_groupbox.findChild( - QtWidgets.QLineEdit, self.qreg_qubit_search_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) if qubit_values_groupbox is not None else None @@ -736,7 +725,7 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, - self.qreg_expected_output_state_init_name_format.format(qreg_name=associated_qreg_name), + QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME.format(qreg_name=associated_qreg_name), ) ) @@ -754,11 +743,11 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) if qreg_input_state_input_field is None or qreg_output_state_input_field is None: @@ -770,7 +759,7 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s qreg_output_state_input_field.setEnabled(False) else: qreg_output_state_input_field.setText( - stringify_some_qubits_of_n_bit_values_container( + SimulationRunEditorDialog.stringify_some_qubits_of_n_bit_values_container( self.edited_simulation_run_model.expected_output_state, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, @@ -782,7 +771,7 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size ): associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, self.output_state_qubit_checkbox_name_format.format(qubit=qubit) + QtWidgets.QCheckBox, OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) if associated_qubit_value_checkbox is None: # TODO: This should not happen @@ -795,7 +784,7 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s ) associated_qubit_value_checkbox.setChecked(qubit_value if qubit_value is not None else False) associated_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( + STRINGIFIED_QUBIT_VALUE_FORMAT.format( stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( qubit_value, return_as_high_low_state=True ) @@ -807,27 +796,27 @@ def handle_input_or_output_state_text_change( self, associated_qreg_name: str, expected_qreg_size: int, is_editing_input_state: bool ) -> None: input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=associated_qreg_name) + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=associated_qreg_name) + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, - self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=associated_qreg_name), + QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=associated_qreg_name), ) expected_output_state_init_button: QtWidgets.QPushButton | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_expected_output_state_init_name_format + QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME ) dialog_save_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( QtWidgets.QDialogButtonBox.StandardButton.Save ) qreg_values_validation_error_lbl: QtWidgets.QLabel | None = self.findChild( - QtWidgets.QLabel, self.qreg_values_validation_error_lbl_name + QtWidgets.QLabel, QREG_VALUES_VALIDATION_ERROR_LABEL_NAME ) if ( @@ -865,7 +854,7 @@ def handle_input_or_output_state_text_change( if not are_stringified_qreg_contents_valid: qreg_values_validation_error_lbl.setText( - self.qreg_values_validation_error_format.format( + QREG_VALUES_VALIDATION_ERROR_FORMAT.format( qreg_name=associated_qreg_name, expected_num_qubit_values=expected_qreg_size, actual_num_qubit_values=len(input_state_text_field.text()), @@ -883,7 +872,7 @@ def handle_input_or_output_state_text_change( if not are_stringified_qreg_contents_valid: qreg_values_validation_error_lbl.setText( - self.qreg_values_validation_error_format.format( + QREG_VALUES_VALIDATION_ERROR_FORMAT.format( qreg_name=associated_qreg_name, expected_num_qubit_values=expected_qreg_size, actual_num_qubit_values=len(output_state_text_field.text()), @@ -909,9 +898,7 @@ def handle_input_or_output_state_text_change( associated_input_state_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - self.input_state_qubit_checkbox_name_format.format( - qubit=qubit_in_input_or_output_state - ), + INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit_in_input_or_output_state), ) ) @@ -925,7 +912,7 @@ def handle_input_or_output_state_text_change( ) associated_input_state_qubit_value_checkbox.setChecked(new_qubit_value) associated_input_state_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( + STRINGIFIED_QUBIT_VALUE_FORMAT.format( stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( new_qubit_value, return_as_high_low_state=True ) @@ -937,9 +924,7 @@ def handle_input_or_output_state_text_change( associated_output_state_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - self.output_state_qubit_checkbox_name_format.format( - qubit=qubit_in_input_or_output_state - ), + OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit_in_input_or_output_state), ) ) if associated_output_state_qubit_value_checkbox is None: @@ -952,7 +937,7 @@ def handle_input_or_output_state_text_change( ) associated_output_state_qubit_value_checkbox.setChecked(new_qubit_value) associated_output_state_qubit_value_checkbox.setText( - self.stringified_qubit_value_format.format( + STRINGIFIED_QUBIT_VALUE_FORMAT.format( stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( new_qubit_value, return_as_high_low_state=True ) @@ -961,16 +946,16 @@ def handle_input_or_output_state_text_change( qreg_name: str = qreg_layout.qreg_name not_edited_input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_input_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) not_edited_output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, self.qreg_output_state_input_field_name_format.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) not_edited_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, self.qreg_qubit_values_toggle_button_name_format.format(qreg_name=qreg_name) + QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) ) @@ -989,3 +974,42 @@ def handle_input_or_output_state_text_change( else False ) not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) + + @staticmethod + def stringify_some_qubits_of_n_bit_values_container( + n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int + ) -> str: + if ( + first_qubit >= n_bit_values_container.size() + or first_qubit + (n_qubits - 1) >= n_bit_values_container.size() + ): + return "" + return "".join([ + "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) + ]) + + @staticmethod + def get_internal_qubit_labels_for_qreg( + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + first_qubit_of_qreg: int, + n_qubits_in_qreg: int, + ) -> list[str]: + internal_qubit_labels: list[str] = [] + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_in_qreg): + fetched_internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ) + if fetched_internal_qubit_label is None: + continue + internal_qubit_labels.append(fetched_internal_qubit_label) + return internal_qubit_labels + + @staticmethod + def stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bool) -> str: + if qubit_value is None: + return "UNKNOWN" if return_as_high_low_state else "-" + + if qubit_value is True: + return "HIGH" if return_as_high_low_state else "1" + + return "LOW" if return_as_high_low_state else "0" diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py index c87367b2..4a2a0144 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -110,6 +110,7 @@ def _handle_export_failure(self, err: Exception) -> None: @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] def _handle_batch_exported(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: if not isinstance(batch_data, int): + # TODO: Better logging of mismatched type/s in list show_optionally_cancellable_notification( message_box_type=MessageBoxType.INFO, message_box_parent=self, From 0eb4e74c6bd27aabde7e5dc0d49d5c5b8305db7c Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 22 Jan 2026 23:11:46 +0100 Subject: [PATCH 40/88] All simulation run dialogs are now centered when opened, added functionality to get 'default' big dialog size relative to screen size --- .../quantum_circuit_simulation_dialog.py | 13 +++--- .../dialogs/base_progress_dialog.py | 42 +++++++++++++++---- .../qt_all_input_states_generator_dialog.py | 1 - .../qt_simulation_run_dialog.py | 4 +- .../qt_simulation_run_editor_dialog.py | 15 +++++-- .../qt_simulation_run_json_export_dialog.py | 1 - .../qt_simulation_run_json_import_dialog.py | 1 - ...tion_run_execution_styled_item_delegate.py | 12 ++---- ...ation_run_overview_styled_item_delegate.py | 12 ++---- 9 files changed, 63 insertions(+), 38 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 307b20a3..c181c432 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -16,6 +16,7 @@ from mqt import syrec +from .simulation_view.dialogs.base_progress_dialog import BaseProgressDialog from .simulation_view.qt_all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog @@ -58,11 +59,13 @@ def __init__( self.title = "Define simulation runs for quantum computation" self.setWindowTitle(self.title) - self.left = 0 - self.top = 0 - self.width = 1200 - self.height = 800 - self.setGeometry(self.left, self.top, self.width, self.height) + dialog_size: Final[QtCore.QSize] = BaseProgressDialog.get_default_big_dialog_size() + center_dialog_pos_for_size: Final[QtCore.QPoint] = BaseProgressDialog.get_center_screen_position_for_size( + dialog_size + ) + self.setGeometry( + center_dialog_pos_for_size.x(), center_dialog_pos_for_size.y(), dialog_size.width(), dialog_size.height() + ) self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None self.all_input_states_generator_dialog: AllInputStatesGeneratorDialog | None = None diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 6ff54998..d2632a94 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -10,7 +10,7 @@ from typing import Final, Generic, TypeVar -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets from ...logger_utils import log_error_to_console, log_info_to_console from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification @@ -25,15 +25,19 @@ "Batch of {n_batch_elements:d} completed! Runtime [in seconds]: {batch_duration_in_seconds:f}" ) +SMALL_DIALOG_WIDTH: Final[int] = 600 +SMALL_DIALOG_HEIGHT: Final[int] = 300 + class BaseProgressDialog(QtWidgets.QDialog, Generic[T]): # type: ignore[misc] def __init__( self, parent: QtWidgets.QWidget, dialog_title: str, - dialog_size: tuple[int, int] = (600, 600), optional_progress_bar_text_format: str | None = None, create_default_layout: bool = True, + user_provided_dialog_size: QtCore.QSize | None = None, + center_dialog: bool = True, ): super().__init__(parent) @@ -46,11 +50,21 @@ def __init__( self.setSizeGripEnabled(True) self.setWindowTitle(dialog_title) - dialog_x_pos: Final[int] = 0 - dialog_y_pos: Final[int] = 0 - dialog_width: Final[int] = dialog_size[0] - dialog_height: Final[int] = dialog_size[1] - self.setGeometry(dialog_x_pos, dialog_y_pos, dialog_width, dialog_height) + dialog_x_pos: int = 0 + dialog_y_pos: int = 0 + + to_be_used_dialog_size: Final[QtCore.QSize] = ( + QtCore.QSize(SMALL_DIALOG_WIDTH, SMALL_DIALOG_HEIGHT) + if user_provided_dialog_size is None + else user_provided_dialog_size + ) + if center_dialog: + dialog_pos: Final[QtCore.QPoint] = BaseProgressDialog.get_center_screen_position_for_size( + to_be_used_dialog_size + ) + dialog_x_pos = dialog_pos.x() + dialog_y_pos = dialog_pos.y() + self.setGeometry(dialog_x_pos, dialog_y_pos, to_be_used_dialog_size.width(), to_be_used_dialog_size.height()) layout = QtWidgets.QVBoxLayout() self.title_lbl = QtWidgets.QLabel() @@ -94,6 +108,20 @@ def __init__( layout.addWidget(self.dialog_button_box) self.setLayout(layout) + @staticmethod + def get_default_big_dialog_size() -> QtCore.Size: + return QtCore.QSize( + int(QtGui.QGuiApplication.primaryScreen().availableSize().width() / 1.5), + int(QtGui.QGuiApplication.primaryScreen().availableSize().height() / 1.5), + ) + + @staticmethod + def get_center_screen_position_for_size(dialog_size: QtCore.Size) -> QtCore.QPoint: + return QtCore.QPoint( + (QtGui.QGuiApplication.primaryScreen().availableSize().width() // 2) - (dialog_size.width() // 2), + (QtGui.QGuiApplication.primaryScreen().availableSize().height() // 2) - (dialog_size.height() // 2), + ) + def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: self.progress_info_text_lbl.setText( DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT.format( diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py index cb070968..b10decc3 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py @@ -29,7 +29,6 @@ def __init__(self, parent: QtWidgets.QWidget): super().__init__( parent, dialog_title="Generating simulation runs...", - dialog_size=(400, 200), optional_progress_bar_text_format="Generated %v out of %m input states", ) self.shared_simulation_runs_model: QtSimulationRunModel | None = None diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py index b925269e..20f9a0f7 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Final, cast -from PyQt6 import QtCore, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets if TYPE_CHECKING: from PyQt6 import QtGui @@ -33,9 +33,9 @@ def __init__(self, parent: QtWidgets.QWidget): super().__init__( parent, dialog_title="Executing simulation runs...", - dialog_size=(400, 200), optional_progress_bar_text_format="Executed simulation run %v of %m", create_default_layout=False, + user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) self.annotatable_quantum_computation: syrec.annotatable_quantum_computation | None = None self.shared_simulation_runs_model: QtSimulationRunModel | None = None diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index ff540737..c033f067 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -14,6 +14,7 @@ from mqt import syrec +from .dialogs.base_progress_dialog import BaseProgressDialog from .qt_simulation_run_model import ( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE, QUANTUM_REGISTER_LAYOUT_QT_ROLE, @@ -99,12 +100,21 @@ def __init__( self.setModal(True) self.setSizeGripEnabled(True) self.setWindowTitle("Edit qubit values of quantum registers for simulation run") + + dialog_size: Final[QtCore.QSize] = BaseProgressDialog.get_default_big_dialog_size() + center_dialog_pos_for_size: Final[QtCore.QPoint] = BaseProgressDialog.get_center_screen_position_for_size( + dialog_size + ) + self.setGeometry( + center_dialog_pos_for_size.x(), center_dialog_pos_for_size.y(), dialog_size.width(), dialog_size.height() + ) + main_layout = QtWidgets.QVBoxLayout() self.simulation_run_wrapper_box = QtWidgets.QGroupBox( "Simulation run #" + str(simulation_run_model_index.row()) ) - main_layout.addWidget(self.simulation_run_wrapper_box) + # main_layout.addWidget(self.simulation_run_wrapper_box) # TODO: How to render n-dimensional variables # TODO: How can we determine whether qubits are readonly @@ -238,6 +248,7 @@ def __init__( simulation_run_scroll_area = QtWidgets.QScrollArea() simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) simulation_run_scroll_area.setWidgetResizable(True) + simulation_run_scroll_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) main_layout.addWidget(simulation_run_scroll_area) qreg_values_validation_error_label = QtWidgets.QLabel(objectName=QREG_VALUES_VALIDATION_ERROR_LABEL_NAME) @@ -250,10 +261,8 @@ def __init__( ) self.dialog_button_box.setCenterButtons(True) self.dialog_button_box.accepted.connect(self.accept) - # TODO: Only update copy of simulation run model to be able to discard changes # TODO: Require confirmation to discard changes self.dialog_button_box.rejected.connect(self.reject) - main_layout.addWidget(self.dialog_button_box) self.setLayout(main_layout) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py index 4a2a0144..fd3506e4 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py @@ -31,7 +31,6 @@ def __init__(self, parent: QtWidgets.QWidget): super().__init__( parent, dialog_title="Exporting simulation runs...", - dialog_size=(400, 200), optional_progress_bar_text_format="Exported simulation run %v of %m", create_default_layout=False, ) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py index 4af96fb8..248e0c12 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py @@ -31,7 +31,6 @@ def __init__(self, parent: QtWidgets.QWidget): super().__init__( parent, dialog_title="Importing simulation runs...", - dialog_size=(400, 200), optional_progress_bar_text_format=None, create_default_layout=False, ) diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py index 456a498b..8e8d7994 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -184,13 +184,7 @@ def _get_required_size_for_content( @staticmethod def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - required_content_size: QtCore.QSize = SimulationRunExecutionStyledItemDelegate._get_required_size_for_content( - option, index - ) - return QtCore.QSize( - min(option.rect.bottomRight().x(), required_content_size.width()), - max(option.rect.bottomRight().y(), required_content_size.height()), - ) + return SimulationRunExecutionStyledItemDelegate._get_required_size_for_content(option, index) def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py index e5d02640..22ae3d84 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -151,13 +151,7 @@ def _get_required_size_for_content( @staticmethod def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - required_content_size: QtCore.QSize = SimulationRunOverviewStyledItemDelegate._get_required_size_for_content( - option, index - ) - return QtCore.QSize( - min(option.rect.bottomRight().x(), required_content_size.width()), - min(option.rect.bottomRight().y(), required_content_size.height()), - ) + return SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: From 2c2ebb6e8716bb1ae3389913acc47c6d3969ab31 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 23 Jan 2026 20:56:15 +0100 Subject: [PATCH 41/88] Simulation run editor will now close when any of the required QtWidgets in its internal operations are not found, additionally cancellation now requires confirmation --- .../qt_simulation_run_editor_dialog.py | 753 +++++++++++------- 1 file changed, 463 insertions(+), 290 deletions(-) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py index c033f067..6a8a1300 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py @@ -8,12 +8,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING, Final, cast from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec +from ..logger_utils import log_error_to_console +from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification from .dialogs.base_progress_dialog import BaseProgressDialog from .qt_simulation_run_model import ( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE, @@ -21,6 +23,8 @@ ) if TYPE_CHECKING: + from collections.abc import Iterable + from .qt_simulation_run_model import ( QuantumRegisterLayout, SimulationRunModel, @@ -73,7 +77,6 @@ def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 ) -# TODO: Replace 'simple' returns with QDialog.reject("") to indicate fatal errors and stop editing simulation run but also reject changes made in parent widget that opened dialog class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( self, @@ -82,6 +85,7 @@ def __init__( parent: QtWidgets.QWidget, ): super().__init__(parent) + self.failed_due_to_internal_error: bool = False self.simulation_run_model_index: QtCore.QModelIndex = simulation_run_model_index self.edited_simulation_run_model: SimulationRunModel = copy_of_reference_edit_sim_run_model @@ -114,18 +118,15 @@ def __init__( self.simulation_run_wrapper_box = QtWidgets.QGroupBox( "Simulation run #" + str(simulation_run_model_index.row()) ) - # main_layout.addWidget(self.simulation_run_wrapper_box) # TODO: How to render n-dimensional variables - # TODO: How can we determine whether qubits are readonly self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 self.edit_of_qubit_values_enabled: bool = False - # TODO: Add validators quantum_register_controls_grid_layout = QtWidgets.QGridLayout() self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) quantum_register_controls_grid_layout.addLayout( - self.create_qreg_search_controls(), 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + self._create_qreg_search_controls(), 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) init_expected_output_state_button = QtWidgets.QPushButton( @@ -134,7 +135,7 @@ def __init__( else "Clear output state", objectName=QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, ) - init_expected_output_state_button.clicked.connect(self.handle_init_expected_output_state_button_click) + init_expected_output_state_button.clicked.connect(self._handle_init_expected_output_state_button_click) output_state_value_toggle_controls_layout = QtWidgets.QHBoxLayout() output_state_value_toggle_controls_layout.addWidget(QtWidgets.QLabel("Modify output state value:")) @@ -171,7 +172,7 @@ def __init__( alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) - input_state_edit_field = self.create_in_or_out_state_edit_field( + input_state_edit_field = self._create_in_or_out_state_edit_field( qreg_layout, optional_qreg_qubit_values=initial_input_state, is_created_for_input_state=True ) quantum_register_controls_grid_layout.addWidget( @@ -181,7 +182,7 @@ def __init__( alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) - output_state_edit_field = self.create_in_or_out_state_edit_field( + output_state_edit_field = self._create_in_or_out_state_edit_field( qreg_layout, optional_qreg_qubit_values=initial_expected_output_state, is_created_for_input_state=False ) quantum_register_controls_grid_layout.addWidget( @@ -197,7 +198,7 @@ def __init__( quantum_register_controls_grid_layout.addWidget(edit_qubit_values_toggle_button, qreg_controls_grid_row, 3) # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton edit_qubit_values_toggle_button.clicked.connect( - lambda _, associated_qreg_name=qreg_name: self.handle_qreg_qubit_values_edit_toggle_button_click( + lambda _, associated_qreg_name=qreg_name: self._handle_qreg_qubit_values_edit_toggle_button_click( associated_qreg_name ) ) @@ -215,7 +216,7 @@ def __init__( n_cols_in_quantum_register_controls_grid_layout: int = 3 qreg_controls_grid_row += 1 quantum_register_controls_grid_layout.addWidget( - self.create_qubit_controls_groupbox(qreg_layout, initial_input_state, initial_expected_output_state), + self._create_qubit_controls_groupbox(qreg_layout, initial_input_state, initial_expected_output_state), qreg_controls_grid_row, 0, 1, @@ -261,45 +262,89 @@ def __init__( ) self.dialog_button_box.setCenterButtons(True) self.dialog_button_box.accepted.connect(self.accept) - # TODO: Require confirmation to discard changes self.dialog_button_box.rejected.connect(self.reject) main_layout.addWidget(self.dialog_button_box) self.setLayout(main_layout) - def handle_quantum_register_name_search(self) -> None: + def reject(self) -> None: + # Ask for confirmation before closing dialog + if self.failed_due_to_internal_error or show_optionally_cancellable_notification( + message_box_type=MessageBoxType.QUESTION, + message_box_parent=self, + message_box_title="Confirm close", + message_box_content="Are you sure you want stop editing the simulation run, all unsaved changes will be lost?", + is_cancellable=True, + log_contents=False, + ): + super().reject() + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + if self.failed_due_to_internal_error: + self.reject() + return + + # Ask for confirmation before closing dialog + if show_optionally_cancellable_notification( + message_box_type=MessageBoxType.QUESTION, + message_box_parent=self, + message_box_title="Confirm close", + message_box_content="Are you sure you want stop editing the simulation run, all unsaved changes will be lost?", + is_cancellable=True, + log_contents=False, + ): + self.accept() + else: + event.ignore() + + def _handle_quantum_register_name_search(self) -> None: for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name - qreg_name_search_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME + optional_qreg_name_search_input_field: QtWidgets.QLineEdit | None = ( + self.simulation_run_wrapper_box.findChild(QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME) ) - qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLabel, QREG_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) ) - qreg_layout_info_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_layout_info_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLabel, QREG_LAYOUT_INFO_NAME_FORMAT.format(qreg_name=qreg_name) ) - qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qreg_input_state_input_field: QtWidgets.QLineEdit | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + ) ) - qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qreg_output_state_input_field: QtWidgets.QLineEdit | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + ) ) - qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( + optional_qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) ) - if ( - qreg_name_search_input_field is None - or qreg_name_label is None - or qreg_layout_info_label is None - or qreg_input_state_input_field is None - or qreg_output_state_input_field is None - or qreg_edit_qubit_values_toggle_button is None + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_qreg_name_search_input_field, + optional_qreg_name_label, + optional_qreg_layout_info_label, + optional_qreg_input_state_input_field, + optional_qreg_output_state_input_field, + optional_qreg_edit_qubit_values_toggle_button, + ], + f"Failed to locate all required Qt widgets required for quantum register '{qreg_name}' during quantum register search", ): - # TODO: This should not happen - continue + return + + qreg_name_search_input_field = cast("QtWidgets.QLineEdit", optional_qreg_name_search_input_field) + qreg_name_label = cast("QtWidgets.QLabel", optional_qreg_name_label) + qreg_layout_info_label = cast("QtWidgets.QLabel", optional_qreg_layout_info_label) + qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) + qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) + qreg_edit_qubit_values_toggle_button = cast( + "QtWidgets.QPushButton", optional_qreg_edit_qubit_values_toggle_button + ) should_control_be_visible: bool = qreg_name_search_input_field.text() is None or qreg_name.startswith( qreg_name_search_input_field.text() @@ -310,35 +355,44 @@ def handle_quantum_register_name_search(self) -> None: qreg_output_state_input_field.setVisible(should_control_be_visible) qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) - def handle_input_state_qubit_value_checkbox_state_change( + def _handle_input_state_qubit_value_checkbox_state_change( self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int ) -> None: - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, - INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), + optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, + INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), + ) ) - qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) - if associated_qubit_value_checkbox is None or qreg_input_state_input_field is None: - self.show_error_msg_dialog( - title="Failed to updated qubit value", - error_msg=f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in input state!", - ) + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_associated_qubit_value_checkbox, optional_qreg_input_state_input_field], + f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in input state!", + ): return + associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) + qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) + updated_qubit_value: bool = associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked - stringified_updated_qubit_value: str = SimulationRunEditorDialog.stringify_qubit_value( + stringified_updated_qubit_value: str = SimulationRunEditorDialog._stringify_qubit_value( updated_qubit_value, return_as_high_low_state=True ) if not self.edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): - self.show_error_msg_dialog( - title="Failed to updated qubit value", - error_msg=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in input state to new value{stringified_updated_qubit_value}!", + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Failed to updated qubit value", + message_box_content=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register {associated_qreg_name} in input state to new value{stringified_updated_qubit_value}!", + is_cancellable=False, + log_contents=True, ) + self.reject() return associated_qubit_value_checkbox.setText( @@ -348,36 +402,40 @@ def handle_input_state_qubit_value_checkbox_state_change( curr_stringified_input_state: str = qreg_input_state_input_field.text() qreg_input_state_input_field.setText( curr_stringified_input_state[:relative_qubit_index_in_quantum_register] - + SimulationRunEditorDialog.stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + + SimulationRunEditorDialog._stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] ) - def handle_output_state_qubit_value_checkbox_state_change( + def _handle_output_state_qubit_value_checkbox_state_change( self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int ) -> None: - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, - OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), + optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, + OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), + ) ) - qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) - if associated_qubit_value_checkbox is None or qreg_output_state_input_field is None: - self.show_error_msg_dialog( - title="Failed to updated qubit value", - error_msg=f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in output state!", - ) + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_associated_qubit_value_checkbox, optional_qreg_output_state_input_field], + f"Failed to locate all required Qt widgets required to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in output state!", + ): return + associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) + qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) + updated_qubit_value: bool = associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked - stringified_updated_qubit_value: str = SimulationRunEditorDialog.stringify_qubit_value( + stringified_updated_qubit_value: str = SimulationRunEditorDialog._stringify_qubit_value( updated_qubit_value, return_as_high_low_state=True ) if self.edited_simulation_run_model.expected_output_state is None: - stringified_updated_qubit_value = SimulationRunEditorDialog.stringify_qubit_value( + stringified_updated_qubit_value = SimulationRunEditorDialog._stringify_qubit_value( None, return_as_high_low_state=True ) associated_qubit_value_checkbox.setText( @@ -388,10 +446,15 @@ def handle_output_state_qubit_value_checkbox_state_change( if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( associated_qubit, updated_qubit_value ): - self.show_error_msg_dialog( - title="Failed to updated qubit value", - error_msg=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in output state to new value{stringified_updated_qubit_value}!", + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Failed to updated qubit value", + message_box_content=f"Failed to update value of qubit {relative_qubit_index_in_quantum_register} of quantum register '{associated_qreg_name}' in output state to new value{stringified_updated_qubit_value}!", + is_cancellable=False, + log_contents=True, ) + self.reject() return associated_qubit_value_checkbox.setText( @@ -400,14 +463,11 @@ def handle_output_state_qubit_value_checkbox_state_change( curr_stringified_output_state: str = qreg_output_state_input_field.text() qreg_output_state_input_field.setText( curr_stringified_output_state[:relative_qubit_index_in_quantum_register] - + SimulationRunEditorDialog.stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + + SimulationRunEditorDialog._stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] ) - def show_error_msg_dialog(self, title: str, error_msg: str) -> None: - QtWidgets.QMessageBox.critical(self, title, error_msg, defaultButton=QtWidgets.QMessageBox.StandardButton.Ok) - - def create_qreg_search_controls(self) -> QtWidgets.QLayout: + def _create_qreg_search_controls(self) -> QtWidgets.QLayout: qreg_search_controls_layout = QtWidgets.QHBoxLayout() qreg_search_label = QtWidgets.QLabel("Quantum register:") qreg_search_input_field = QtWidgets.QLineEdit(objectName=QREG_SEARCH_INPUT_FIELD_NAME) @@ -422,14 +482,14 @@ def create_qreg_search_controls(self) -> QtWidgets.QLayout: qreg_search_input_field.setCompleter(qreg_name_search_completer) qreg_search_trigger_button = QtWidgets.QPushButton("Search") - qreg_search_trigger_button.clicked.connect(self.handle_quantum_register_name_search) + qreg_search_trigger_button.clicked.connect(self._handle_quantum_register_name_search) qreg_search_controls_layout.addWidget(qreg_search_label) qreg_search_controls_layout.addWidget(qreg_search_input_field) qreg_search_controls_layout.addWidget(qreg_search_trigger_button) return qreg_search_controls_layout - def create_in_or_out_state_edit_field( + def _create_in_or_out_state_edit_field( self, qreg_layout: QuantumRegisterLayout, optional_qreg_qubit_values: syrec.n_bit_values_container | None, @@ -444,18 +504,16 @@ def create_in_or_out_state_edit_field( if optional_qreg_qubit_values is not None: in_or_out_state_edit_field.setText( - SimulationRunEditorDialog.stringify_some_qubits_of_n_bit_values_container( + SimulationRunEditorDialog._stringify_some_qubits_of_n_bit_values_container( optional_qreg_qubit_values, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size ) ) - # output_state_edit_field.setEnabled(not self.are_qubits_values_readonly) else: in_or_out_state_edit_field.setEnabled(False) in_or_out_state_edit_field.setPlaceholderText("-") in_or_out_state_edit_field.setCursorPosition(0) in_or_out_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) - # input_state_edit_field.setEnabled(not self.are_qubits_values_readonly and not self.is_input_state_readonly) in_or_out_state_edit_field.setValidator( QtGui.QRegularExpressionValidator(QtCore.QRegularExpression(R"^[0-1]*$"), self) ) @@ -466,21 +524,21 @@ def create_in_or_out_state_edit_field( # whether the QLineEdit widget lost focus. in_or_out_state_edit_field.editingFinished.connect( lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_created_for_input_state: ( - self.handle_input_or_output_state_text_change( + self._handle_input_or_output_state_text_change( associated_qreg_name, expected_text_length, is_editing_input_state ) ) ) in_or_out_state_edit_field.focusOut.connect( lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_created_for_input_state: ( - self.handle_input_or_output_state_text_change( + self._handle_input_or_output_state_text_change( associated_qreg_name, expected_text_length, is_editing_input_state ) ) ) return in_or_out_state_edit_field - def create_in_or_out_state_qubit_value_checkbox( + def _create_in_or_out_state_qubit_value_checkbox( self, associated_qreg_name: str, optional_qreg_qubit_values: syrec.n_bit_values_container | None, @@ -495,7 +553,7 @@ def create_in_or_out_state_qubit_value_checkbox( ) qubit_value_checkbox.setText( STRINGIFIED_QUBIT_VALUE_FORMAT.format( - stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + stringified_qubit_value=SimulationRunEditorDialog._stringify_qubit_value( None if optional_qreg_qubit_values is None else optional_qreg_qubit_values.test(relative_qubit_index_in_qreg), @@ -506,18 +564,18 @@ def create_in_or_out_state_qubit_value_checkbox( qubit_value_checkbox.setEnabled(optional_qreg_qubit_values is not None) qubit_value_checkbox.checkStateChanged.connect( lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( - self.handle_input_state_qubit_value_checkbox_state_change( + self._handle_input_state_qubit_value_checkbox_state_change( associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register ) if is_qubit_in_input_state - else self.handle_output_state_qubit_value_checkbox_state_change( + else self._handle_output_state_qubit_value_checkbox_state_change( associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register ) ) ) return qubit_value_checkbox - def create_search_controls_for_qubits_of_qreg( + def _create_search_controls_for_qubits_of_qreg( self, associated_qreg_layout: QuantumRegisterLayout ) -> QtWidgets.QLayout: qubit_search_layout = QtWidgets.QHBoxLayout() @@ -534,7 +592,7 @@ def create_search_controls_for_qubits_of_qreg( qubit_search_input_field.setPlaceholderText("") qubit_search_completer = QtWidgets.QCompleter( - SimulationRunEditorDialog.get_internal_qubit_labels_for_qreg( + SimulationRunEditorDialog._get_internal_qubit_labels_for_qreg( self.annotatable_quantum_computation, first_qreg_qubit, last_qreg_qubit ) ) @@ -545,15 +603,14 @@ def create_search_controls_for_qubits_of_qreg( qubit_search_trigger_button = QtWidgets.QPushButton("Search") qubit_search_trigger_button.clicked.connect( - lambda _, associated_qreg_name=associated_qreg_name: self.handle_qubit_search_trigger_button_click( + lambda _, associated_qreg_name=associated_qreg_name: self._handle_qubit_search_trigger_button_click( associated_qreg_name ) ) qubit_search_layout.addWidget(qubit_search_trigger_button) return qubit_search_layout - # TODO: Scroll area - def create_qubit_controls_groupbox( + def _create_qubit_controls_groupbox( self, associated_qreg_layout: QuantumRegisterLayout, initial_input_state: syrec.n_bit_values_container, @@ -561,7 +618,7 @@ def create_qubit_controls_groupbox( ) -> QtWidgets.QWidget: input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() input_output_qubits_value_controls_groupbox_layout.addLayout( - self.create_search_controls_for_qubits_of_qreg(associated_qreg_layout), + self._create_search_controls_for_qubits_of_qreg(associated_qreg_layout), 0, 0, 1, @@ -584,7 +641,7 @@ def create_qubit_controls_groupbox( qubit_label, 1 + relative_qubit_index_in_qreg, 0 ) - input_state_qubit_value_checkbox: QtWidgets.QCheckBox = self.create_in_or_out_state_qubit_value_checkbox( + input_state_qubit_value_checkbox: QtWidgets.QCheckBox = self._create_in_or_out_state_qubit_value_checkbox( associated_qreg_layout.qreg_name, initial_input_state, associated_qubit=qubit, @@ -598,7 +655,7 @@ def create_qubit_controls_groupbox( alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) - output_state_qubit_value_checkbox: QtWidgets.QCheckBox = self.create_in_or_out_state_qubit_value_checkbox( + output_state_qubit_value_checkbox: QtWidgets.QCheckBox = self._create_in_or_out_state_qubit_value_checkbox( associated_qreg_layout.qreg_name, initial_expected_output_state, associated_qubit=qubit, @@ -612,7 +669,6 @@ def create_qubit_controls_groupbox( alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) - # TODO: How can the column widths of the input fields and the checkbox columns be synced? input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 1) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(1, 1) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(2, 1) @@ -625,46 +681,69 @@ def create_qubit_controls_groupbox( input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) return input_output_qubits_value_controls_groupbox - def handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: + def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: for qreg_layout in self.qreg_layouts: if qreg_layout.qreg_name != associated_quantum_register_name: continue - qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - if qreg_qubits_groupbox is None: - # TODO: This should not happen - continue - qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_qreg_qubits_groupbox], + f"Failed to find required qubits groupbox for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", + ): + return + + qreg_qubits_groupbox = cast("QtWidgets.QGroupBox", optional_qreg_qubits_groupbox) + optional_qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( QtWidgets.QLineEdit, QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - if qubit_search_input_field is None: - # TODO: This should not happen - continue + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_qubit_search_input_field], + f"Failed to find required qubit label search input field for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", + ): + return + + qubit_search_input_field = cast("QtWidgets.QLineEdit", optional_qubit_search_input_field) for qubit in range( qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size ): - qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( - QtWidgets.QLabel, QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit) + optional_qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, + QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + optional_input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, + INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + optional_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, + OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - if ( - qubit_value_label is None - or input_state_qubit_checkbox is None - or output_state_qubit_checkbox is None + + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_qubit_value_label, + optional_input_state_qubit_checkbox, + optional_output_state_qubit_checkbox, + ], + f"Failed to find required controls for qubits for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", ): - # TODO: This should not happen - continue + return + + qubit_value_label = cast("QtWidgets.QLabel", optional_qubit_value_label) + input_state_qubit_checkbox = cast("QtWidgets.QCheckBox", optional_input_state_qubit_checkbox) + output_state_qubit_checkbox = cast("QtWidgets.QCheckBox", optional_output_state_qubit_checkbox) does_qubit_label_match_search_text: bool = self.annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal @@ -673,47 +752,73 @@ def handle_qubit_search_trigger_button_click(self, associated_quantum_register_n input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) - def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: + def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name - # TODO: QtCore.Qt.FindDirectChildrenOnly - qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, + QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) ) - qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qreg_output_state_input_field: QtWidgets.QtWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, + QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) ) - qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( - qubit_values_groupbox.findChild( - QtWidgets.QLineEdit, QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( + optional_qubit_values_groupbox.findChild( + QtWidgets.QLineEdit, + QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - if qubit_values_groupbox is not None + if optional_qubit_values_groupbox is not None else None ) - if ( - qreg_input_state_input_field is None - or qreg_output_state_input_field is None - or qubit_values_groupbox is None - or qubit_values_toggle_button is None - or qubit_values_toggle_button is None - or expected_output_state_value_toggle_button is None - or qubit_values_groupbox_qubit_search_field is None + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_qreg_input_state_input_field, + optional_qreg_output_state_input_field, + optional_qubit_values_groupbox, + optional_qubit_values_toggle_button, + optional_expected_output_state_value_toggle_button, + optional_qubit_values_groupbox_qubit_search_field, + ], + f"Failed to find all required QtWidgets for quantum register '{qreg_name}' during handling of edit toggle of output state!", ): - # TODO: This should not happen - continue + return + + qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) + qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) + qubit_values_groupbox = cast("QtWidgets.QCheckBox", optional_qubit_values_groupbox) + qubit_values_toggle_button = cast("QtWidgets.QPushButton", optional_qubit_values_toggle_button) + expected_output_state_value_toggle_button = cast( + "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button + ) + qubit_values_groupbox_qubit_search_field = cast( + "QtWidgets.QLineEdit", optional_qubit_values_groupbox_qubit_search_field + ) if qreg_name == associated_qreg_name and not qubit_values_groupbox.isVisible(): qubit_values_groupbox.setVisible(True) @@ -728,19 +833,27 @@ def handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name qreg_input_state_input_field.setEnabled(True) qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 qubit_values_groupbox_qubit_search_field.setText("") - self.handle_qubit_search_trigger_button_click(associated_qreg_name) + self._handle_qubit_search_trigger_button_click(associated_qreg_name) - def handle_init_expected_output_state_button_click(self, associated_qreg_name: str) -> None: - expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + def _handle_init_expected_output_state_button_click(self, associated_qreg_name: str) -> None: + optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - if expected_output_state_value_toggle_button is None: + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_expected_output_state_value_toggle_button], + f"Failed to find all required QtWidgets for quantum register '{associated_qreg_name}' during handling of initialization/clearing of output state!", + ): return + expected_output_state_value_toggle_button = cast( + "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button + ) + should_reset_output_state: bool = self.edited_simulation_run_model.expected_output_state is not None if should_reset_output_state: self.edited_simulation_run_model.expected_output_state = None @@ -751,24 +864,33 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name - qreg_input_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + ) ) - qreg_output_state_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + optional_qreg_output_state_input_field: QtWidgets.QtWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + ) ) - if qreg_input_state_input_field is None or qreg_output_state_input_field is None: - # TODO: This should not happen + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_qreg_input_state_input_field, optional_qreg_output_state_input_field], + f"Failed to find all required QtWidgets for quantum register '{qreg_name}' during handling of initialization/clearing of output state!", + ): return + qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) + qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) + if should_reset_output_state: qreg_output_state_input_field.setText("") qreg_output_state_input_field.setEnabled(False) else: qreg_output_state_input_field.setText( - SimulationRunEditorDialog.stringify_some_qubits_of_n_bit_values_container( + SimulationRunEditorDialog._stringify_some_qubits_of_n_bit_values_container( self.edited_simulation_run_model.expected_output_state, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, @@ -779,13 +901,19 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s for qubit in range( qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size ): - associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) ) - if associated_qubit_value_checkbox is None: - # TODO: This should not happen + + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_associated_qubit_value_checkbox], + f"Failed to find all required QtWidget for qubit of output state of quantum register '{qreg_name}' during initialization/clearing of output state!", + ): return + associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) qubit_value: bool | None = ( self.edited_simulation_run_model.expected_output_state.test(qubit) # type: ignore[union-attr] if not should_reset_output_state @@ -794,198 +922,216 @@ def handle_init_expected_output_state_button_click(self, associated_qreg_name: s associated_qubit_value_checkbox.setChecked(qubit_value if qubit_value is not None else False) associated_qubit_value_checkbox.setText( STRINGIFIED_QUBIT_VALUE_FORMAT.format( - stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( + stringified_qubit_value=SimulationRunEditorDialog._stringify_qubit_value( qubit_value, return_as_high_low_state=True ) ) ) associated_qubit_value_checkbox.setEnabled(not should_reset_output_state) - def handle_input_or_output_state_text_change( + def _handle_input_or_output_state_text_change( self, associated_qreg_name: str, expected_qreg_size: int, is_editing_input_state: bool ) -> None: - input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) + optional_input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, + QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) + optional_output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, + QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, - QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=associated_qreg_name), + optional_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) ) - expected_output_state_init_button: QtWidgets.QPushButton | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME + optional_expected_output_state_init_button: QtWidgets.QPushButton | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) ) - dialog_save_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + optional_dialog_save_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( QtWidgets.QDialogButtonBox.StandardButton.Save ) - qreg_values_validation_error_lbl: QtWidgets.QLabel | None = self.findChild( + optional_qreg_values_validation_error_lbl: QtWidgets.QLabel | None = self.findChild( QtWidgets.QLabel, QREG_VALUES_VALIDATION_ERROR_LABEL_NAME ) - if ( - input_state_text_field is None - or output_state_text_field is None - or qreg_qubit_values_edit_toggle_button is None - or expected_output_state_init_button is None - or dialog_save_button is None - or qreg_values_validation_error_lbl is None + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_input_state_text_field, + optional_output_state_text_field, + optional_qreg_qubit_values_edit_toggle_button, + optional_expected_output_state_init_button, + optional_dialog_save_button, + optional_qreg_values_validation_error_lbl, + ], + f"Failed to find all required QtWidgets for edited quantum register '{associated_qreg_name}' during handling of input/output state edit!", ): - # TODO: This should not happen return - are_stringified_qreg_contents_valid: bool = False - if is_editing_input_state: - are_stringified_qreg_contents_valid = ( - input_state_text_field.hasAcceptableInput() and len(input_state_text_field.text()) == expected_qreg_size - ) - else: - are_stringified_qreg_contents_valid = ( - output_state_text_field.hasAcceptableInput() - and len(output_state_text_field.text()) == expected_qreg_size - ) + input_state_text_field = cast("QtWidgets.QLineEdit", optional_input_state_text_field) + output_state_text_field = cast("QtWidgets.QLineEdit", optional_output_state_text_field) + qreg_qubit_values_edit_toggle_button = cast( + "QtWidgets.QPushButton", optional_qreg_qubit_values_edit_toggle_button + ) + expected_output_state_init_button = cast("QtWidgets.QPushButton", optional_expected_output_state_init_button) + dialog_save_button = cast("QtWidgets.QPushButton", optional_dialog_save_button) + qreg_values_validation_error_lbl = cast("QtWidgets.QLabel", optional_qreg_values_validation_error_lbl) + + curr_edited_input_text_field: QtWidgets.QLineEdit = ( + input_state_text_field if is_editing_input_state else output_state_text_field + ) + curr_not_edited_input_text_field: QtWidgets.QLineEdit = ( + output_state_text_field if is_editing_input_state else input_state_text_field + ) + are_stringified_qreg_contents_valid: Final[bool] = ( + curr_edited_input_text_field.hasAcceptableInput() + and len(curr_edited_input_text_field.text()) == expected_qreg_size + ) qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) expected_output_state_init_button.setEnabled(are_stringified_qreg_contents_valid) dialog_save_button.setEnabled(are_stringified_qreg_contents_valid) - if is_editing_input_state: - output_state_text_field.setEnabled( - are_stringified_qreg_contents_valid - if self.edited_simulation_run_model.expected_output_state is not None - else False + qreg_values_validation_error_lbl.setText( + QREG_VALUES_VALIDATION_ERROR_FORMAT.format( + qreg_name=associated_qreg_name, + expected_num_qubit_values=expected_qreg_size, + actual_num_qubit_values=len(curr_edited_input_text_field.text()), + input_or_output_state_ident="input" if is_editing_input_state else "output", ) + if not are_stringified_qreg_contents_valid + else "" + ) + curr_not_edited_input_text_field.setEnabled( + are_stringified_qreg_contents_valid + and (self.edited_simulation_run_model.expected_output_state is not None or not is_editing_input_state) + ) - if not are_stringified_qreg_contents_valid: - qreg_values_validation_error_lbl.setText( - QREG_VALUES_VALIDATION_ERROR_FORMAT.format( - qreg_name=associated_qreg_name, - expected_num_qubit_values=expected_qreg_size, - actual_num_qubit_values=len(input_state_text_field.text()), - input_or_output_state_ident="input", + for qreg_layout in self.qreg_layouts: + qreg_name: str = qreg_layout.qreg_name + if qreg_name != associated_qreg_name: + optional_not_edited_input_state_text_field: QtWidgets.QLineEdit | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, + QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - else: - qreg_values_validation_error_lbl.setText("") - else: - input_state_text_field.setEnabled( - are_stringified_qreg_contents_valid - if self.edited_simulation_run_model.expected_output_state is not None - else False - ) - if not are_stringified_qreg_contents_valid: - qreg_values_validation_error_lbl.setText( - QREG_VALUES_VALIDATION_ERROR_FORMAT.format( - qreg_name=associated_qreg_name, - expected_num_qubit_values=expected_qreg_size, - actual_num_qubit_values=len(output_state_text_field.text()), - input_or_output_state_ident="output", + optional_not_edited_output_state_text_field: QtWidgets.QLineEdit | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, + QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - else: - qreg_values_validation_error_lbl.setText("") - - for qreg_layout in self.qreg_layouts: - if qreg_layout.qreg_name == associated_qreg_name: - if not are_stringified_qreg_contents_valid: - continue - - first_qubit_of_qreg: int = qreg_layout.first_qubit_of_qreg - n_qubits_of_qreg: int = qreg_layout.qreg_size - - qubit_in_input_or_output_state: int - new_qubit_value: bool - if is_editing_input_state: - for relative_qubit_idx_in_qreg in range(n_qubits_of_qreg): - qubit_in_input_or_output_state = first_qubit_of_qreg + relative_qubit_idx_in_qreg - associated_input_state_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( - self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, - INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit_in_input_or_output_state), - ) - ) - if associated_input_state_qubit_value_checkbox is None: - # TODO: This should not happen - return - - new_qubit_value = input_state_text_field.text()[relative_qubit_idx_in_qreg] == "1" - self.edited_simulation_run_model.update_input_state_qubit_value( - qubit_in_input_or_output_state, new_qubit_value - ) - associated_input_state_qubit_value_checkbox.setChecked(new_qubit_value) - associated_input_state_qubit_value_checkbox.setText( - STRINGIFIED_QUBIT_VALUE_FORMAT.format( - stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( - new_qubit_value, return_as_high_low_state=True - ) - ) - ) - else: - for relative_qubit_idx_in_qreg in range(n_qubits_of_qreg): - qubit_in_input_or_output_state = first_qubit_of_qreg + relative_qubit_idx_in_qreg - associated_output_state_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( - self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, - OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit_in_input_or_output_state), - ) - ) - if associated_output_state_qubit_value_checkbox is None: - # TODO: This should not happen - return + optional_not_edited_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) - new_qubit_value = output_state_text_field.text()[relative_qubit_idx_in_qreg] == "1" - self.edited_simulation_run_model.update_expected_output_state_qubit_value( - qubit_in_input_or_output_state, new_qubit_value - ) - associated_output_state_qubit_value_checkbox.setChecked(new_qubit_value) - associated_output_state_qubit_value_checkbox.setText( - STRINGIFIED_QUBIT_VALUE_FORMAT.format( - stringified_qubit_value=SimulationRunEditorDialog.stringify_qubit_value( - new_qubit_value, return_as_high_low_state=True - ) - ) - ) + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_not_edited_input_state_text_field, + optional_not_edited_output_state_text_field, + optional_not_edited_qreg_qubit_values_edit_toggle_button, + ], + f"Failed to find all required QtWidgets for not edited quantum register '{qreg_name}' during handling of input/output state edit!", + ): + return - qreg_name: str = qreg_layout.qreg_name - not_edited_input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) - ) + not_edited_input_state_text_field = cast( + "QtWidgets.QLineEdit", optional_not_edited_input_state_text_field + ) + not_edited_output_state_text_field = cast( + "QtWidgets.QLineEdit", optional_not_edited_output_state_text_field + ) + not_edited_qreg_qubit_values_edit_toggle_button = cast( + "QtWidgets.QPushButton", optional_not_edited_qreg_qubit_values_edit_toggle_button + ) - not_edited_output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) - ) + not_edited_input_state_text_field.setEnabled(are_stringified_qreg_contents_valid) + not_edited_output_state_text_field.setEnabled( + are_stringified_qreg_contents_valid + and self.edited_simulation_run_model.expected_output_state is not None + ) + not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) - not_edited_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( + optional_not_edited_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) + QtWidgets.QGroupBox, + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - if ( - not_edited_input_state_text_field is None - or not_edited_output_state_text_field is None - or not_edited_qreg_qubit_values_edit_toggle_button is None + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_not_edited_qreg_qubit_values_groupbox], + f"Failed to find required group box for not edited quantum register '{qreg_name}' QtWidgets during handling of input/output state edit!", ): - # TODO: This should not happen return - not_edited_input_state_text_field.setEnabled(are_stringified_qreg_contents_valid) - not_edited_output_state_text_field.setEnabled( - are_stringified_qreg_contents_valid - if self.edited_simulation_run_model.expected_output_state is not None - else False + not_edited_qreg_qubit_values_groupbox = cast( + "QtWidgets.QGroupBox", optional_not_edited_qreg_qubit_values_groupbox ) - not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) + if not not_edited_qreg_qubit_values_groupbox.isVisible(): + continue + + first_qubit_of_qreg: int = qreg_layout.first_qubit_of_qreg + n_qubits_of_qreg: int = qreg_layout.qreg_size + for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): + optional_not_edited_input_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( + not_edited_qreg_qubit_values_groupbox.findChild( + QtWidgets.QCheckBox, + INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + optional_not_edited_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( + not_edited_qreg_qubit_values_groupbox.findChild( + QtWidgets.QCheckBox, + OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_not_edited_input_state_qubit_checkbox, optional_not_edited_output_state_qubit_checkbox], + f"Failed to find required QtWidgets for not edited input/output state qubit checkboxes of not edited quantum register '{qreg_name}' during handling of input/output state edit!", + ): + return + + not_edited_input_state_qubit_checkbox = cast( + "QtWidgets.QGroupBox", optional_not_edited_input_state_qubit_checkbox + ) + not_edited_output_state_qubit_checkbox = cast( + "QtWidgets.QGroupBox", optional_not_edited_output_state_qubit_checkbox + ) + + not_edited_input_state_qubit_checkbox.setEnabled(are_stringified_qreg_contents_valid) + not_edited_output_state_qubit_checkbox.setEnabled( + are_stringified_qreg_contents_valid + and self.edited_simulation_run_model.expected_output_state is not None + ) @staticmethod - def stringify_some_qubits_of_n_bit_values_container( + def _stringify_some_qubits_of_n_bit_values_container( n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int ) -> str: if ( @@ -998,7 +1144,7 @@ def stringify_some_qubits_of_n_bit_values_container( ]) @staticmethod - def get_internal_qubit_labels_for_qreg( + def _get_internal_qubit_labels_for_qreg( annotatable_quantum_computation: syrec.annotatable_quantum_computation, first_qubit_of_qreg: int, n_qubits_in_qreg: int, @@ -1014,7 +1160,7 @@ def get_internal_qubit_labels_for_qreg( return internal_qubit_labels @staticmethod - def stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bool) -> str: + def _stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bool) -> str: if qubit_value is None: return "UNKNOWN" if return_as_high_low_state else "-" @@ -1022,3 +1168,30 @@ def stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bo return "HIGH" if return_as_high_low_state else "1" return "LOW" if return_as_high_low_state else "0" + + def _assert_all_required_widgets_found_or_close_dialog( + self, required_widgets: Iterable[QtWidgets.QWidget], error_dialog_content: str + ) -> bool: + if all(widget is not None for widget in required_widgets): + return True + + self.failed_due_to_internal_error = True + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Not all required Qt widgets found!", + message_box_content=f"{error_dialog_content}\nUnsaved changed will be lost and edit dialog will be closed!", + is_cancellable=False, + log_contents=False, + ) + + stringified_found_widgets_object_names: Final[str] = "Object names of found widgets: " + ( + ",".join([widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets)]) + ) + # We want to log the caller of this function as the origin of the error instead of this function itself. + log_error_to_console( + f"Not all required Qt widgets found, provided error text: {error_dialog_content}!\n{stringified_found_widgets_object_names}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + self.reject() + return False From 7d14e7a119c54c514290c10fbed60884c277da7a Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 23 Jan 2026 21:14:19 +0100 Subject: [PATCH 42/88] Refactoring of simulation_view folder by creating subfolders to group associated files --- .../mqt/syrec/quantum_circuit_simulation_dialog.py | 14 +++++++------- .../all_input_states_generator_dialog.py} | 12 ++++++------ .../dialogs/base_progress_dialog.py | 2 +- .../simulation_run_dialog.py} | 12 ++++++------ .../simulation_run_editor_dialog.py} | 10 +++++----- .../simulation_run_json_export_dialog.py} | 12 ++++++------ .../simulation_run_json_import_dialog.py} | 12 ++++++------ ...lation_run_model.py => simulation_run_model.py} | 0 ...mulation_run_execution_styled_item_delegate.py} | 4 ++-- ...imulation_run_overview_styled_item_delegate.py} | 4 ++-- .../all_input_states_generator_worker.py | 4 ++-- .../{ => workers}/cancellable_base_worker.py | 0 .../simulation_run_json_export_worker.py | 4 ++-- .../simulation_run_json_import_worker.py | 4 ++-- .../simulation_run_worker.py} | 6 +++--- 15 files changed, 50 insertions(+), 50 deletions(-) rename python/mqt/syrec/simulation_view/{qt_all_input_states_generator_dialog.py => dialogs/all_input_states_generator_dialog.py} (95%) rename python/mqt/syrec/simulation_view/{qt_simulation_run_dialog.py => dialogs/simulation_run_dialog.py} (96%) rename python/mqt/syrec/simulation_view/{qt_simulation_run_editor_dialog.py => dialogs/simulation_run_editor_dialog.py} (99%) rename python/mqt/syrec/simulation_view/{qt_simulation_run_json_export_dialog.py => dialogs/simulation_run_json_export_dialog.py} (94%) rename python/mqt/syrec/simulation_view/{qt_simulation_run_json_import_dialog.py => dialogs/simulation_run_json_import_dialog.py} (96%) rename python/mqt/syrec/simulation_view/{qt_simulation_run_model.py => simulation_run_model.py} (100%) rename python/mqt/syrec/simulation_view/styled_item_delegates/{qt_simulation_run_execution_styled_item_delegate.py => simulation_run_execution_styled_item_delegate.py} (99%) rename python/mqt/syrec/simulation_view/styled_item_delegates/{qt_simulation_run_overview_styled_item_delegate.py => simulation_run_overview_styled_item_delegate.py} (99%) rename python/mqt/syrec/simulation_view/{ => workers}/all_input_states_generator_worker.py (97%) rename python/mqt/syrec/simulation_view/{ => workers}/cancellable_base_worker.py (100%) rename python/mqt/syrec/simulation_view/{ => workers}/simulation_run_json_export_worker.py (97%) rename python/mqt/syrec/simulation_view/{ => workers}/simulation_run_json_import_worker.py (98%) rename python/mqt/syrec/simulation_view/{qt_simulation_run_worker.py => workers/simulation_run_worker.py} (97%) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index c181c432..d38da3cd 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -16,18 +16,18 @@ from mqt import syrec +from .simulation_view.dialogs.all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.dialogs.base_progress_dialog import BaseProgressDialog -from .simulation_view.qt_all_input_states_generator_dialog import AllInputStatesGeneratorDialog -from .simulation_view.qt_simulation_run_dialog import SimulationRunDialog -from .simulation_view.qt_simulation_run_editor_dialog import SimulationRunEditorDialog -from .simulation_view.qt_simulation_run_json_export_dialog import SimulationRunJsonExportDialog -from .simulation_view.qt_simulation_run_json_import_dialog import SimulationRunJsonImportDialog -from .simulation_view.qt_simulation_run_model import ( +from .simulation_view.dialogs.simulation_run_dialog import SimulationRunDialog +from .simulation_view.dialogs.simulation_run_editor_dialog import SimulationRunEditorDialog +from .simulation_view.dialogs.simulation_run_json_export_dialog import SimulationRunJsonExportDialog +from .simulation_view.dialogs.simulation_run_json_import_dialog import SimulationRunJsonImportDialog +from .simulation_view.simulation_run_model import ( SIMULATION_RUN_IO_STATE_QT_ROLE, QtSimulationRunModel, SimulationRunModel, ) -from .simulation_view.styled_item_delegates.qt_simulation_run_overview_styled_item_delegate import ( +from .simulation_view.styled_item_delegates.simulation_run_overview_styled_item_delegate import ( SimulationRunOverviewStyledItemDelegate, ) diff --git a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py similarity index 95% rename from python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py rename to python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index b10decc3..f512e13d 100644 --- a/python/mqt/syrec/simulation_view/qt_all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -15,13 +15,13 @@ if TYPE_CHECKING: from PyQt6 import QtGui, QtWidgets - from .qt_simulation_run_model import QtSimulationRunModel + from ..simulation_run_model import QtSimulationRunModel -from ..logger_utils import log_error_to_console, log_info_to_console -from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from .all_input_states_generator_worker import AllInputStatesGeneratorWorker -from .dialogs.base_progress_dialog import BaseProgressDialog -from .qt_simulation_run_model import SimulationRunModel +from ...logger_utils import log_error_to_console, log_info_to_console +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ..simulation_run_model import SimulationRunModel +from ..workers.all_input_states_generator_worker import AllInputStatesGeneratorWorker +from .base_progress_dialog import BaseProgressDialog class AllInputStatesGeneratorDialog(BaseProgressDialog[AllInputStatesGeneratorWorker]): diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index d2632a94..6b2e7823 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -14,7 +14,7 @@ from ...logger_utils import log_error_to_console, log_info_to_console from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from ..cancellable_base_worker import CancellableBaseWorker +from ..workers.cancellable_base_worker import CancellableBaseWorker T = TypeVar("T", bound=CancellableBaseWorker) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py similarity index 96% rename from python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py rename to python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 20f9a0f7..0d254356 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -17,15 +17,15 @@ from mqt import syrec - from .qt_simulation_run_model import QtSimulationRunModel + from ..simulation_run_model import QtSimulationRunModel -from ..logger_utils import log_error_to_console, log_info_to_console -from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from .dialogs.base_progress_dialog import BaseProgressDialog -from .qt_simulation_run_worker import SimulationRunResult, SimulationRunWorker -from .styled_item_delegates.qt_simulation_run_execution_styled_item_delegate import ( +from ...logger_utils import log_error_to_console, log_info_to_console +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ..styled_item_delegates.simulation_run_execution_styled_item_delegate import ( SimulationRunExecutionStyledItemDelegate, ) +from ..workers.simulation_run_worker import SimulationRunResult, SimulationRunWorker +from .base_progress_dialog import BaseProgressDialog class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py similarity index 99% rename from python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py rename to python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 6a8a1300..dc491861 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -14,18 +14,18 @@ from mqt import syrec -from ..logger_utils import log_error_to_console -from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from .dialogs.base_progress_dialog import BaseProgressDialog -from .qt_simulation_run_model import ( +from ...logger_utils import log_error_to_console +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ..simulation_run_model import ( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE, QUANTUM_REGISTER_LAYOUT_QT_ROLE, ) +from .base_progress_dialog import BaseProgressDialog if TYPE_CHECKING: from collections.abc import Iterable - from .qt_simulation_run_model import ( + from ..simulation_run_model import ( QuantumRegisterLayout, SimulationRunModel, ) diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py similarity index 94% rename from python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py rename to python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index fd3506e4..4e7ba440 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -18,12 +18,12 @@ from PyQt6 import QtGui - from .qt_simulation_run_model import SimulationRunModel - from .simulation_run_json_export_worker import SimulationRunJsonExportWorker -from ..logger_utils import log_info_to_console -from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from .dialogs.base_progress_dialog import BaseProgressDialog -from .simulation_run_json_export_worker import SimulationRunJsonExportWorker + from ..simulation_run_model import SimulationRunModel + from ..workers.simulation_run_json_export_worker import SimulationRunJsonExportWorker +from ...logger_utils import log_info_to_console +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ..workers.simulation_run_json_export_worker import SimulationRunJsonExportWorker +from .base_progress_dialog import BaseProgressDialog class SimulationRunJsonExportDialog(BaseProgressDialog[SimulationRunJsonExportWorker]): diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py similarity index 96% rename from python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py rename to python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 248e0c12..493dc500 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -17,13 +17,13 @@ from PyQt6 import QtGui - from .qt_simulation_run_model import QtSimulationRunModel, SimulationRunModel + from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel -from ..logger_utils import log_error_to_console, log_info_to_console -from ..message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from .dialogs.base_progress_dialog import BaseProgressDialog -from .qt_simulation_run_model import SimulationRunModel -from .simulation_run_json_import_worker import SimulationRunJsonImportWorker +from ...logger_utils import log_error_to_console, log_info_to_console +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ..simulation_run_model import SimulationRunModel +from ..workers.simulation_run_json_import_worker import SimulationRunJsonImportWorker +from .base_progress_dialog import BaseProgressDialog class SimulationRunJsonImportDialog(BaseProgressDialog[SimulationRunJsonImportWorker]): diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py similarity index 100% rename from python/mqt/syrec/simulation_view/qt_simulation_run_model.py rename to python/mqt/syrec/simulation_view/simulation_run_model.py diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py similarity index 99% rename from python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py rename to python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index 8e8d7994..0f6dd9ec 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets -from ..qt_simulation_run_model import ( +from ..simulation_run_model import ( LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, LARGEST_SIM_RUN_NUMBER_QT_ROLE, @@ -35,7 +35,7 @@ ) if TYPE_CHECKING: - from ..qt_simulation_run_model import ( + from ..simulation_run_model import ( SimulationRunModel, ) diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py similarity index 99% rename from python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py rename to python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index 22ae3d84..4a6c7197 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/qt_simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets -from ..qt_simulation_run_model import ( +from ..simulation_run_model import ( LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE, LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE, LARGEST_SIM_RUN_NUMBER_QT_ROLE, @@ -38,7 +38,7 @@ ) if TYPE_CHECKING: - from ..qt_simulation_run_model import ( + from ..simulation_run_model import ( SimulationRunModel, ) diff --git a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py similarity index 97% rename from python/mqt/syrec/simulation_view/all_input_states_generator_worker.py rename to python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index e1feceb2..67382d92 100644 --- a/python/mqt/syrec/simulation_view/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -15,9 +15,9 @@ from mqt import syrec -from ..logger_utils import log_error_to_console +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel from .cancellable_base_worker import CancellableBaseWorker -from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: from .cancellable_base_worker import BatchTimestamps diff --git a/python/mqt/syrec/simulation_view/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py similarity index 100% rename from python/mqt/syrec/simulation_view/cancellable_base_worker.py rename to python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py similarity index 97% rename from python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py rename to python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 4c9160bb..301da150 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -13,9 +13,9 @@ from PyQt6 import QtCore -from ..logger_utils import log_error_to_console +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel from .cancellable_base_worker import CancellableBaseWorker -from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: from collections.abc import Iterable diff --git a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py similarity index 98% rename from python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py rename to python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 492e84e9..e7294cbc 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -18,9 +18,9 @@ from mqt import syrec -from ..logger_utils import log_error_to_console, log_info_to_console +from ...logger_utils import log_error_to_console, log_info_to_console +from ..simulation_run_model import SimulationRunModel from .cancellable_base_worker import CancellableBaseWorker -from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: from pathlib import Path diff --git a/python/mqt/syrec/simulation_view/qt_simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py similarity index 97% rename from python/mqt/syrec/simulation_view/qt_simulation_run_worker.py rename to python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index b9eb8406..5263d4c2 100644 --- a/python/mqt/syrec/simulation_view/qt_simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -16,13 +16,13 @@ from mqt import syrec -from ..logger_utils import log_error_to_console +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel from .cancellable_base_worker import CancellableBaseWorker -from .qt_simulation_run_model import SimulationRunModel if TYPE_CHECKING: + from ..simulation_run_model import QtSimulationRunModel from .cancellable_base_worker import BatchTimestamps - from .qt_simulation_run_model import QtSimulationRunModel @dataclass(frozen=True) From a8f02ca7f21a13194778ffc09fc6abef54479eed Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 23 Jan 2026 22:46:18 +0100 Subject: [PATCH 43/88] QuantumCircuitSimulationDialog is now opened using offically recommended .show() function instread of .exec() with dialog being new member variable of editor. Fixed small bottom spacing issue in height calculation in simulation run overview styled item delegate --- .../quantum_circuit_simulation_dialog.py | 34 ++++++------------- .../dialogs/base_progress_dialog.py | 1 - .../dialogs/simulation_run_dialog.py | 1 + ...ation_run_overview_styled_item_delegate.py | 5 +-- python/mqt/syrec/syrec_editor.py | 29 +++++++++++----- 5 files changed, 36 insertions(+), 34 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index d38da3cd..08df2c7b 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -16,6 +16,7 @@ from mqt import syrec +from .message_box_utils import MessageBoxType, show_optionally_cancellable_notification from .simulation_view.dialogs.all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.dialogs.base_progress_dialog import BaseProgressDialog from .simulation_view.dialogs.simulation_run_dialog import SimulationRunDialog @@ -58,6 +59,7 @@ def __init__( self.title = "Define simulation runs for quantum computation" self.setWindowTitle(self.title) + self.setModal(True) dialog_size: Final[QtCore.QSize] = BaseProgressDialog.get_default_big_dialog_size() center_dialog_pos_for_size: Final[QtCore.QPoint] = BaseProgressDialog.get_center_screen_position_for_size( @@ -96,17 +98,20 @@ def __init__( "Check input-output mapping combinations from file", ) - n_simulation_runs_to_add: Final[int] = 10 - QuantumCircuitSimulationDialog.generate_some_simulation_runs( - n_simulation_runs_to_add, self.annotatable_quantum_computation, self.simulation_runs_model - ) - self.layout = QtWidgets.QVBoxLayout() self.layout.addWidget(self.simulation_runs_tab_widget) self.setLayout(self.layout) self.setSizeGripEnabled(True) - # TODO: Load from file controls + def show_save_changes_reminder(self) -> None: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.INFO, + message_box_parent=self, + message_box_title="Remember to save your changes", + message_box_content="All simulation runs not saved via the save to file option are removed when this dialog closes!", + is_cancellable=False, + log_contents=False, + ) def initialize_simulation_runs_tab_widget( self, @@ -479,23 +484,6 @@ def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayou controls_layout.addStretch() return controls_layout - def generate_some_simulation_runs( - self: int, - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - shared_simulation_runs_model: QtSimulationRunModel, - ) -> None: - for i in range(self): - in_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) - expected_out_state = syrec.n_bit_values_container(annotatable_quantum_computation.num_data_qubits) - - sim_run_model: SimulationRunModel | None = None - if i < 2: - sim_run_model = SimulationRunModel(in_state, None) - else: - sim_run_model = SimulationRunModel(in_state, expected_out_state) - - shared_simulation_runs_model.add_simulation_run_model(sim_run_model) - # TODO: Check that number of generate simulation runs does not exceed sys.maxsize def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: int) -> None: if switched_to_tab_index == -1: diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 6b2e7823..9aae00df 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -209,7 +209,6 @@ def _change_dialog_button_enable_state( if dialog_button is not None: dialog_button.setEnabled(should_button_be_enabled) else: - # TODO: Log button name and not numeric value show_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=btn_not_found_notification_parent, diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 0d254356..76c98bb4 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -28,6 +28,7 @@ from .base_progress_dialog import BaseProgressDialog +# TODO: Should the user be able to transfer the determined actual output qubit values as the expected output qubit values? class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): def __init__(self, parent: QtWidgets.QWidget): super().__init__( diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index 4a6c7197..6c7d424e 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -102,7 +102,7 @@ def _get_required_size_for_content( + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, QREG_LAYOUT_INFO_FONT_SIZE) ) column_header_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( - option.font, QREG_LAYOUT_INFO_FONT_SIZE + option.font, CARD_CONTENT_FONT_SIZE ) total_qreg_contents_text_height: int = n_qregs * qreg_contents_text_height total_simulation_run_group_box_height = ( @@ -110,6 +110,7 @@ def _get_required_size_for_content( + group_box_title_height + CARD_TITLE_BOTTOM_Y_MARGIN + column_header_height + + QREG_CONTENT_Y_SPACING + total_qreg_contents_text_height + CARD_CONTENT_PADDING ) @@ -319,7 +320,7 @@ def _draw_and_determine_column_headers( header_row_column_one_rect = QtCore.QRect( header_text_bottom_left_point.x(), - header_text_bottom_left_point.y() + 2 * CARD_TITLE_BOTTOM_Y_MARGIN, + header_text_bottom_left_point.y() + CARD_TITLE_BOTTOM_Y_MARGIN, qreg_name_and_layout_info_column_width, SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), ) diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 5d4c707f..b324ff0d 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -21,6 +21,7 @@ from mqt import syrec from .logger_utils import configure_default_console_logger +from .message_box_utils import MessageBoxType, show_optionally_cancellable_notification # We are using a relative import here: https://stackoverflow.com/questions/43728431/relative-imports-modulenotfounderror-no-module-named-x from .quantum_circuit_simulation_dialog import QuantumCircuitSimulationDialog @@ -1279,6 +1280,7 @@ class MainWindow(QtWidgets.QMainWindow): # type: ignore[misc] def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: QtWidgets.QWidget.__init__(self, parent) + self.quantum_circuit_sim_runs_dialog: QuantumCircuitSimulationDialog | None = None self.setWindowTitle("SyReC Editor") self.setup_widgets() @@ -1354,14 +1356,25 @@ def update_circuit_view_and_qubit_information( self.viewer.load(annotatable_quantum_computation) self.qubits_information_lookup.set_lookup_information(annotatable_quantum_computation) - # TODO: Check other calls of dialog.exec(), see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QDialog.html#PySide6.QtWidgets.QDialog.exec - # TODO: Store internal dialog instance as dialog: T | None and use show() instead of exec() call since the latter start a separate event loop that might not be supported by all platforms - # TODO: Move setting modal declaration to QuantumCircuitSimulationDialog - dialog = QuantumCircuitSimulationDialog(annotatable_quantum_computation, parent=self) - dialog.modal = True - dialog.setWindowTitle("Update configurable options") - # dialog.show() - dialog.exec() + if self.quantum_circuit_sim_runs_dialog is not None: + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Internal error", + message_box_content="Single use simulation run dialog was not correctly reset, expected no instance to exist!", + is_cancellable=False, + ) + return + + self.quantum_circuit_sim_runs_dialog = QuantumCircuitSimulationDialog( + annotatable_quantum_computation, parent=self + ) + self.quantum_circuit_sim_runs_dialog.finished.connect(self.handle_quantum_circuit_sim_runs_dialog_finished) + self.quantum_circuit_sim_runs_dialog.open() + self.quantum_circuit_sim_runs_dialog.show_save_changes_reminder() + + def handle_quantum_circuit_sim_runs_dialog_finished(self, _: int) -> None: + self.quantum_circuit_sim_runs_dialog = None def clear_error_log_and_circuit_view(self) -> None: self.logWidget.clear() From 045593d339c26458fce475a82bf67d50e2b74af7 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 25 Jan 2026 00:00:55 +0100 Subject: [PATCH 44/88] Simulation run styled item delegate now draws expected and actual quantum register contents instead of only the expected one --- .../quantum_circuit_simulation_dialog.py | 1 + .../dialogs/simulation_run_dialog.py | 2 +- .../simulation_view/simulation_run_model.py | 1 - ...ase_simulation_run_styled_item_delegate.py | 31 ++- ...tion_run_execution_styled_item_delegate.py | 10 +- ...ation_run_overview_styled_item_delegate.py | 215 +++++++++++++++--- 6 files changed, 222 insertions(+), 38 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 08df2c7b..bf189e4e 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -136,6 +136,7 @@ def initialize_simulation_runs_tab_widget( simulation_runs_list_view.setModel(shared_simulation_runs_model) simulation_runs_list_view.setItemDelegate(SimulationRunOverviewStyledItemDelegate()) # type: ignore[no-untyped-call] simulation_runs_list_view.setUniformItemSizes(True) + simulation_runs_list_view.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust) simulation_runs_list_view.setAutoFillBackground(False) simulation_runs_list_view.setSpacing(5) simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 76c98bb4..9b11f2cc 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -28,7 +28,6 @@ from .base_progress_dialog import BaseProgressDialog -# TODO: Should the user be able to transfer the determined actual output qubit values as the expected output qubit values? class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): def __init__(self, parent: QtWidgets.QWidget): super().__init__( @@ -55,6 +54,7 @@ def __init__(self, parent: QtWidgets.QWidget): self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() self.simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) # type: ignore[no-untyped-call] self.simulation_runs_list_view.setUniformItemSizes(True) + self.simulation_runs_list_view.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust) self.simulation_runs_list_view.setAutoFillBackground(False) self.simulation_runs_list_view.setSpacing(5) self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 61a859a4..968f59c6 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -18,7 +18,6 @@ if TYPE_CHECKING: from collections.abc import Iterable -# TODO: Mark as const: https://stackoverflow.com/a/57596202 # Some debugging tips: https://www.eso.org/~eltmgr/ECS/documents-latest/CUT/sphinx_doc/latest/docs/500_gui_development.html#gdb # First custom item data role usable according to: https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py index b5362e73..0c9f5db5 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -1,5 +1,5 @@ -# Copyright (c) 2023 - 2025 Chair for Design Automation, TUM -# Copyright (c) 2025 Munich Quantum Software Company GmbH +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH # All rights reserved. # # SPDX-License-Identifier: MIT @@ -21,7 +21,9 @@ DEFAULT_QREG_LAYOUT_TEXT_FORMAT: Final[str] = "(First qubit: {first_qubit:d} - Num. qubits: {n_qubits:d})" DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT: Final[str] = "Simulation run #{simulation_run_number:d}" DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "INPUT" -DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "EXPECTED OUTPUT" +DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER: Final[str] = "OUTPUT" +DEFAULT_EXPECTED_OUTPUT_STATE_QREG_CONTENT_PREFIX: Final[str] = "Expected:" +DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX: Final[str] = "Actual:" DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT: Final[str] = "" CARD_TITLE_FONT_SIZE: Final[int] = 14 @@ -77,16 +79,31 @@ def _stringify_some_qubits_of_n_bit_values_container( @staticmethod def _get_estimated_quantum_register_contents_column_width( - option: QtWidgets.QStyleOptionViewItem, largest_quantum_register_size_in_qubits: int, font_size: int + option: QtWidgets.QStyleOptionViewItem, + largest_quantum_register_size_in_qubits: int, + font_size: int, + does_content_include_prefix: bool = False, + prefix_and_content_x_spacing: int = QREG_CONTENT_X_SPACING, ) -> int: - text_width_for_largest_qreg: int = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + prefix_text_width: Final[int] = max( + BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_EXPECTED_OUTPUT_STATE_QREG_CONTENT_PREFIX, option.font, font_size + ), + BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX, option.font, font_size + ), + ) + + text_width_for_largest_qreg: Final[int] = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( "".join(["0" for i in range(largest_quantum_register_size_in_qubits)]), option.font, font_size ) - text_width_for_unknown_qreg_content: int = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + text_width_for_unknown_qreg_content: Final[int] = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( "", option.font, font_size ) # We can ignore the text width of the headers of the INPUT and OUTPUT columns since the placeholder text for unknown quantum register contents is larger than both header texts - return max(text_width_for_largest_qreg, text_width_for_unknown_qreg_content) + return (prefix_text_width + prefix_and_content_x_spacing if does_content_include_prefix else 0) + max( + text_width_for_largest_qreg, text_width_for_unknown_qreg_content + ) @staticmethod def _get_column_width_scaled_by_ratio_to_total_available_width( diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index 0f6dd9ec..f7c31152 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -184,8 +184,16 @@ def _get_required_size_for_content( @staticmethod def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - return SimulationRunExecutionStyledItemDelegate._get_required_size_for_content(option, index) + required_content_size: Final[QtCore.QSize] = ( + SimulationRunExecutionStyledItemDelegate._get_required_size_for_content(option, index) + ) + available_content_rect: Final[QtCore.QRect] = option.rect + return QtCore.QSize( + min(available_content_rect.width(), required_content_size.width()), + min(available_content_rect.height(), required_content_size.height()), + ) + # TODO: If the available height is smaller than required one, omit to print some quantum registers with a placeholder (like ...) while still printing the aggregate results? def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: return diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index 6c7d424e..cac79713 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -25,6 +25,8 @@ CARD_CONTENT_PADDING, CARD_TITLE_BOTTOM_Y_MARGIN, CARD_TITLE_FONT_SIZE, + DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX, + DEFAULT_EXPECTED_OUTPUT_STATE_QREG_CONTENT_PREFIX, DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER, DEFAULT_QREG_LAYOUT_TEXT_FORMAT, @@ -38,10 +40,17 @@ ) if TYPE_CHECKING: + from mqt import syrec + from ..simulation_run_model import ( SimulationRunModel, ) +HELP_TEXT: Final[str] = ( + "Qubit values of quantum registers have to be read from left to right with the leftmost character (0 or 1) being equal to the value of the first qubit of the quantum register while the rightmost character displays the value of the last qubit of the quantum register. Unknown quantum register qubit values are replaced with a placeholder text." +) +HELP_TEXT_SIZE: Final[int] = 8 + # Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html class SimulationRunOverviewStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] @@ -100,6 +109,7 @@ def _get_required_size_for_content( + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + QREG_CONTENT_Y_SPACING + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, QREG_LAYOUT_INFO_FONT_SIZE) + + QREG_CONTENT_Y_SPACING ) column_header_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( option.font, CARD_CONTENT_FONT_SIZE @@ -112,6 +122,9 @@ def _get_required_size_for_content( + column_header_height + QREG_CONTENT_Y_SPACING + total_qreg_contents_text_height + + QREG_CONTENT_Y_SPACING + + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + + QREG_CONTENT_Y_SPACING + CARD_CONTENT_PADDING ) @@ -130,9 +143,13 @@ def _get_required_size_for_content( option, index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), CARD_CONTENT_FONT_SIZE, + does_content_include_prefix=True, ) ) + required_help_text_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + HELP_TEXT, option.font, HELP_TEXT_SIZE + ) max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) total_simulation_run_group_box_width = max( group_box_title_width, @@ -147,12 +164,20 @@ def _get_required_size_for_content( + QREG_CONTENT_X_SPACING + CARD_CONTENT_PADDING ), + required_help_text_width, ) return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) @staticmethod def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 - return SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) + required_content_size: Final[QtCore.QSize] = ( + SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) + ) + available_content_rect: Final[QtCore.QRect] = option.rect + return QtCore.QSize( + min(available_content_rect.width(), required_content_size.width()), + min(available_content_rect.height(), required_content_size.height()), + ) def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: @@ -188,17 +213,23 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, header_column_rects: list[QtCore.QRect] = self._draw_and_determine_column_headers( painter, option, index, header_text_bottom_left_point, available_rect_for_content.width() ) - header_row_column_one_text_rect = header_column_rects[0] - header_row_column_two_text_rect = header_column_rects[1] - header_row_column_three_text_rect = header_column_rects[2] - row_idx: int = 1 - per_row_y_offset: int = ( - QREG_CONTENT_Y_SPACING - + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + header_text_height: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, CARD_CONTENT_FONT_SIZE + ) + header_row_column_one_text_rect = header_column_rects[0].adjusted(0, header_text_height, 0, header_text_height) + header_row_column_two_text_rect = header_column_rects[1].adjusted(0, header_text_height, 0, header_text_height) + header_row_column_three_text_rect = header_column_rects[2].adjusted( + 0, header_text_height, 0, header_text_height + ) + + per_row_y_offset: Final[int] = ( + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + + QREG_CONTENT_Y_SPACING ) - for qreg_layout in index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE): - curr_row_y_offset: int = row_idx * per_row_y_offset + per_qreg_contents_y_offset: Final[int] = (2 * per_row_y_offset) + QREG_CONTENT_Y_SPACING + for qreg_idx, qreg_layout in enumerate(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)): + curr_row_y_offset: int = qreg_idx * per_qreg_contents_y_offset SimulationRunOverviewStyledItemDelegate._draw_elided_text( painter, qreg_layout.qreg_name, @@ -217,19 +248,6 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, CARD_CONTENT_FONT_SIZE, ) - SimulationRunOverviewStyledItemDelegate._draw_elided_text( - painter, - SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( - associated_sim_run_model.expected_output_state, - qreg_layout.first_qubit_of_qreg, - qreg_layout.qreg_size, - ) - if associated_sim_run_model.expected_output_state is not None - else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, - header_row_column_three_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), - CARD_CONTENT_FONT_SIZE, - ) - SimulationRunOverviewStyledItemDelegate._draw_elided_text( painter, DEFAULT_QREG_LAYOUT_TEXT_FORMAT.format( @@ -241,7 +259,49 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, font_size=QREG_LAYOUT_INFO_FONT_SIZE, text_color=QtCore.Qt.GlobalColor.gray, ) - row_idx += 2 + + SimulationRunOverviewStyledItemDelegate._draw_qreg_contents( + painter, + option, + DEFAULT_EXPECTED_OUTPUT_STATE_QREG_CONTENT_PREFIX, + associated_sim_run_model.expected_output_state, + first_qubit_of_qreg=qreg_layout.first_qubit_of_qreg, + qreg_size=qreg_layout.qreg_size, + available_content_rect=header_row_column_three_text_rect.adjusted( + 0, curr_row_y_offset, 0, curr_row_y_offset + ), + font_size=CARD_CONTENT_FONT_SIZE, + ) + + SimulationRunOverviewStyledItemDelegate._draw_qreg_contents( + painter, + option, + DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX, + associated_sim_run_model.actual_output_state, + first_qubit_of_qreg=qreg_layout.first_qubit_of_qreg, + qreg_size=qreg_layout.qreg_size, + available_content_rect=header_row_column_three_text_rect.adjusted( + 0, curr_row_y_offset + per_row_y_offset, 0, curr_row_y_offset + per_row_y_offset + ), + font_size=CARD_CONTENT_FONT_SIZE, + ) + + n_qregs: Final[int] = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + y_offset_to_help_text: Final[int] = (n_qregs * per_qreg_contents_y_offset) + QREG_CONTENT_Y_SPACING + + help_text_content_rect: Final[QtCore.QRect] = QtCore.QRect( + header_row_column_one_text_rect.topLeft().x(), + header_row_column_one_text_rect.topLeft().y() + y_offset_to_help_text, + available_rect_for_content.width(), + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, HELP_TEXT_SIZE), + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + HELP_TEXT, + help_text_content_rect, + font_size=HELP_TEXT_SIZE, + text_color=QtCore.Qt.GlobalColor.gray, + ) painter.restore() @staticmethod @@ -268,7 +328,6 @@ def _draw_card_border_and_header( painter.setBrush(option.palette.highlightedText()) header_text: str = DEFAULT_SIMULATION_RUN_CARD_HEADER_FORMAT.format(simulation_run_number=simulation_run_number) - SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text(header_text, option.font, CARD_TITLE_FONT_SIZE) header_text_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( option.font, CARD_TITLE_FONT_SIZE ) @@ -317,12 +376,15 @@ def _draw_and_determine_column_headers( qreg_name_and_layout_info_column_width = scaled_column_widths[0] input_state_qreg_content_column_width = scaled_column_widths[1] output_state_qreg_content_column_width = scaled_column_widths[2] + header_text_height: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, CARD_CONTENT_FONT_SIZE + ) header_row_column_one_rect = QtCore.QRect( header_text_bottom_left_point.x(), header_text_bottom_left_point.y() + CARD_TITLE_BOTTOM_Y_MARGIN, qreg_name_and_layout_info_column_width, - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), + header_text_height, ) header_row_column_one_text_rect: QtCore.QRect = header_row_column_one_rect.adjusted( QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 @@ -339,7 +401,7 @@ def _draw_and_determine_column_headers( header_row_column_one_rect.topRight().x(), header_row_column_one_rect.topRight().y(), input_state_qreg_content_column_width, - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), + header_text_height, ) header_row_column_two_text_rect: QtCore.QRect = header_row_column_two_rect.adjusted( QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 @@ -356,7 +418,7 @@ def _draw_and_determine_column_headers( header_row_column_two_rect.topRight().x(), header_row_column_two_rect.topRight().y(), output_state_qreg_content_column_width, - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE), + header_text_height, ) header_row_column_three_text_rect: QtCore.QRect = header_row_column_three_rect.adjusted( QREG_CONTENT_X_SPACING, 0, -QREG_CONTENT_X_SPACING, 0 @@ -380,3 +442,100 @@ def _draw_and_determine_column_headers( painter, header_row_column_three_rect, 5, QtCore.Qt.GlobalColor.green, index.row() ) return [header_row_column_one_text_rect, header_row_column_two_text_rect, header_row_column_three_text_rect] + + @staticmethod + def _draw_qreg_contents( + painter: QtGui.QPainter, + option: QtWidgets.QStyleOptionViewItem, + qreg_contents_prefix: str, + qreg_contents_container: syrec.n_bit_values_container | None, + first_qubit_of_qreg: int, + qreg_size: int, + available_content_rect: QtCore.QRect, + font_size: int, + ) -> None: + # If we do not even have enough available space to print whitespace between the prefix label and the qreg contents then simply draw nothing + if available_content_rect.width() <= QREG_CONTENT_X_SPACING: + return + + qreg_contents_text_height: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, font_size + ) + stringified_qreg_contents: Final[str] = ( + SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + qreg_contents_container, + first_qubit_of_qreg, + qreg_size, + ) + if qreg_contents_container is not None + else DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT + ) + + required_qreg_prefix_width: Final[int] = max( + SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_EXPECTED_OUTPUT_STATE_QREG_CONTENT_PREFIX, option.font, font_size + ), + SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX, option.font, font_size + ), + ) + required_qreg_contents_width: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + stringified_qreg_contents, option.font, font_size + ) + total_qreg_contents_width: Final[int] = ( + required_qreg_prefix_width + QREG_CONTENT_X_SPACING + required_qreg_contents_width + ) + + actual_qreg_prefix_width: int = 0 + actual_qreg_contents_width: int = 0 + qreg_contents_text_start_pos: QtCore.QPoint = QtCore.QPoint(0, 0) + + if total_qreg_contents_width >= available_content_rect.width(): + truncated_ratio_based_content_widths: Final[list[int]] = ( + SimulationRunOverviewStyledItemDelegate._scale_column_widths_based_on_ratio_to_total_available_width( + [required_qreg_prefix_width, required_qreg_contents_width], + available_content_rect.width() - QREG_CONTENT_X_SPACING, + ) + ) + actual_qreg_prefix_width = truncated_ratio_based_content_widths[0] + actual_qreg_contents_width = truncated_ratio_based_content_widths[1] + + qreg_contents_text_start_pos = available_content_rect.topLeft() + else: + actual_qreg_prefix_width = min(available_content_rect.width(), required_qreg_prefix_width) + actual_qreg_contents_width = min( + available_content_rect.width() - actual_qreg_prefix_width, required_qreg_contents_width + ) + + # If the content can 'easily' fit in the available rectangle then we can center out content inside of said rectangle + qreg_contents_text_start_pos_offset: Final[int] = (available_content_rect.width() // 2) - ( + total_qreg_contents_width // 2 + ) + qreg_contents_text_start_pos = QtCore.QPoint( + available_content_rect.topLeft().x() + qreg_contents_text_start_pos_offset, + available_content_rect.topLeft().y(), + ) + + qreg_prefix_text_rect = QtCore.QRect( + qreg_contents_text_start_pos.x(), + qreg_contents_text_start_pos.y(), + actual_qreg_prefix_width, + qreg_contents_text_height, + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + qreg_contents_prefix, + qreg_prefix_text_rect, + font_size, + text_color=QtCore.Qt.GlobalColor.gray, + ) + + qreg_contents_text_rect = QtCore.QRect( + qreg_prefix_text_rect.topRight().x() + QREG_CONTENT_X_SPACING, + qreg_prefix_text_rect.topRight().y(), + actual_qreg_contents_width, + qreg_contents_text_height, + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, stringified_qreg_contents, qreg_contents_text_rect, font_size + ) From 3adb48d2fe0d6b2d69ec34a0e1ce0d0e20f59fb1 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 25 Jan 2026 14:00:53 +0100 Subject: [PATCH 45/88] Simulation run execution dialog listview now also lists help text regarding qreg contents. Switch QListViews vertical scroll mode from per item to per pixel to handle contents larger than the viewport size correctly when drawing the contents with QStyledItemDelegates --- .../quantum_circuit_simulation_dialog.py | 2 + .../dialogs/simulation_run_dialog.py | 13 +--- ...ase_simulation_run_styled_item_delegate.py | 4 ++ ...tion_run_execution_styled_item_delegate.py | 53 ++++++++++++---- ...ation_run_overview_styled_item_delegate.py | 62 ++++++++++++------- 5 files changed, 86 insertions(+), 48 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index bf189e4e..cfe158d6 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -140,6 +140,8 @@ def initialize_simulation_runs_tab_widget( simulation_runs_list_view.setAutoFillBackground(False) simulation_runs_list_view.setSpacing(5) simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + # By default the vertical scroll mode is set to ScrollPerItem which will prevent the user to view not displayed if the vertical viewport size is larger than the required height of the list view item. + simulation_runs_list_view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 9b11f2cc..064a2952 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -58,6 +58,8 @@ def __init__(self, parent: QtWidgets.QWidget): self.simulation_runs_list_view.setAutoFillBackground(False) self.simulation_runs_list_view.setSpacing(5) self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + # By default the vertical scroll mode is set to ScrollPerItem which will prevent the user to view not displayed if the vertical viewport size is larger than the required height of the list view item. + self.simulation_runs_list_view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) self.simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) # self.simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) @@ -66,18 +68,7 @@ def __init__(self, parent: QtWidgets.QWidget): simulation_runs_list_scrollarea.setAutoFillBackground(False) simulation_runs_list_scrollarea.setWidget(self.simulation_runs_list_view) simulation_runs_list_scrollarea.setWidgetResizable(True) - - simulation_runs_list_layout.addItem( - QtWidgets.QSpacerItem( - 2, 2, QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Minimum - ) - ) simulation_runs_list_layout.addWidget(simulation_runs_list_scrollarea) - simulation_runs_list_layout.addItem( - QtWidgets.QSpacerItem( - 2, 2, QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Minimum - ) - ) layout.addLayout(simulation_runs_list_layout) layout.addWidget(self.progress_bar) layout.addWidget(self.total_runtime_info_text_lbl) diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py index 0c9f5db5..d4049ee6 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -25,6 +25,9 @@ DEFAULT_EXPECTED_OUTPUT_STATE_QREG_CONTENT_PREFIX: Final[str] = "Expected:" DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX: Final[str] = "Actual:" DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT: Final[str] = "" +QREG_CONTENTS_HELP_TEXT: Final[str] = ( + "Qubit values of quantum registers have to be read from left to right with the leftmost character (0 or 1) being equal to the value of the first qubit of the quantum register while the rightmost character displays the value of the last qubit of the quantum register. Unknown quantum register qubit values are replaced with a placeholder text." +) CARD_TITLE_FONT_SIZE: Final[int] = 14 CARD_CONTENT_FONT_SIZE: Final[int] = 10 @@ -33,6 +36,7 @@ QREG_CONTENT_Y_SPACING: Final[int] = 4 QREG_CONTENT_X_SPACING: Final[int] = 6 CARD_CONTENT_PADDING: Final[int] = 20 +QREG_CONTENTS_HELP_TEXT_FONT_SIZE: Final[int] = 8 class BaseSimulationRunStyledItemDelegate: diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index f7c31152..a3219342 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -31,6 +31,8 @@ DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, QREG_CONTENT_X_SPACING, QREG_CONTENT_Y_SPACING, + QREG_CONTENTS_HELP_TEXT, + QREG_CONTENTS_HELP_TEXT_FONT_SIZE, BaseSimulationRunStyledItemDelegate, ) @@ -138,15 +140,17 @@ def _get_required_size_for_content( if not index.isValid(): return QtCore.QSize(0, 0) - card_title_height: int = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + card_title_height: Final[int] = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( option.font, CARD_TITLE_FONT_SIZE ) - card_title_width: int = SimulationRunExecutionStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( - index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE + card_title_width: Final[int] = ( + SimulationRunExecutionStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( + index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE + ) ) - n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + n_qregs: Final[int] = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) # Quantum register contents are displayed in the following format for every quantum register: # R0: : # R1: : @@ -156,29 +160,38 @@ def _get_required_size_for_content( # Additionally, below the content of all quantum registers the aggregate result of the simulation run is displayed as: # R5: : # R6: : - required_text_line_height: int = ( + required_text_line_height: Final[int] = ( QREG_CONTENT_Y_SPACING + SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) ) - required_qreg_contents_height: int = 4 * required_text_line_height - required_total_qreg_contents_height: int = (n_qregs * required_qreg_contents_height) + ( + required_qreg_contents_height: Final[int] = 4 * required_text_line_height + required_total_qreg_contents_height: Final[int] = (n_qregs * required_qreg_contents_height) + ( (n_qregs - 1) * QREG_CONTENT_Y_SPACING if n_qregs > 1 else 0 ) - required_aggregate_result_text_height: int = 2 * required_text_line_height - required_total_card_height: int = ( + required_aggregate_result_text_height: Final[int] = 2 * required_text_line_height + required_help_text_height: Final[int] = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + option.font, QREG_CONTENTS_HELP_TEXT_FONT_SIZE + ) + required_total_card_height: Final[int] = ( CARD_CONTENT_PADDING + card_title_height + CARD_TITLE_BOTTOM_Y_MARGIN + required_total_qreg_contents_height + AGGREGATE_RESULT_TOP_Y_MARGIN + required_aggregate_result_text_height + + QREG_CONTENT_Y_SPACING + + required_help_text_height + CARD_CONTENT_PADDING ) - required_total_card_width: int = max( + required_help_text_width: Final[int] = SimulationRunExecutionStyledItemDelegate._get_pixel_width_of_text( + QREG_CONTENTS_HELP_TEXT, option.font, QREG_CONTENTS_HELP_TEXT_FONT_SIZE + ) + required_total_card_width: Final[int] = max( card_title_width, SimulationRunExecutionStyledItemDelegate._get_required_width_for_qreg_contents_and_outputs_match_result( option, index, CARD_CONTENT_FONT_SIZE ), + required_help_text_width, ) return QtCore.QSize(required_total_card_width, required_total_card_height) @@ -189,11 +202,9 @@ def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) ) available_content_rect: Final[QtCore.QRect] = option.rect return QtCore.QSize( - min(available_content_rect.width(), required_content_size.width()), - min(available_content_rect.height(), required_content_size.height()), + min(required_content_size.width(), available_content_rect.width()), required_content_size.height() ) - # TODO: If the available height is smaller than required one, omit to print some quantum registers with a placeholder (like ...) while still printing the aggregate results? def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: return @@ -462,6 +473,22 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, if associated_input_output_mapping.execution_runtime_in_ms is None else QtCore.Qt.GlobalColor.black, ) + + help_text_content_rect: QtCore.QRect = QtCore.QRect( + available_rect_for_content.x(), + aggregate_result_row_runtime_label_col_rect.bottomLeft().y() + QREG_CONTENT_Y_SPACING, + available_rect_for_content.width(), + SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + option.font, QREG_CONTENTS_HELP_TEXT_FONT_SIZE + ), + ) + SimulationRunExecutionStyledItemDelegate._draw_elided_text( + painter, + QREG_CONTENTS_HELP_TEXT, + help_text_content_rect, + font_size=QREG_CONTENTS_HELP_TEXT_FONT_SIZE, + text_color=QtCore.Qt.GlobalColor.gray, + ) painter.restore() @staticmethod diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index cac79713..70ffd7e2 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -35,6 +35,8 @@ DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT, QREG_CONTENT_X_SPACING, QREG_CONTENT_Y_SPACING, + QREG_CONTENTS_HELP_TEXT, + QREG_CONTENTS_HELP_TEXT_FONT_SIZE, QREG_LAYOUT_INFO_FONT_SIZE, BaseSimulationRunStyledItemDelegate, ) @@ -46,11 +48,6 @@ SimulationRunModel, ) -HELP_TEXT: Final[str] = ( - "Qubit values of quantum registers have to be read from left to right with the leftmost character (0 or 1) being equal to the value of the first qubit of the quantum register while the rightmost character displays the value of the last qubit of the quantum register. Unknown quantum register qubit values are replaced with a placeholder text." -) -HELP_TEXT_SIZE: Final[int] = 8 - # Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html class SimulationRunOverviewStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] @@ -91,20 +88,20 @@ def _get_required_size_for_content( if not index.isValid(): return QtCore.QSize(0, 0) - n_qregs: int = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + n_qregs: Final[int] = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) # Quantum register contents are displayed as two rows containing the following information: # R0: # R1: - group_box_title_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + group_box_title_height: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( option.font, CARD_TITLE_FONT_SIZE ) - group_box_title_width: int = ( + group_box_title_width: Final[int] = ( SimulationRunOverviewStyledItemDelegate._get_pixel_width_for_longest_sim_run_header( index.data(LARGEST_SIM_RUN_NUMBER_QT_ROLE), option.font, CARD_TITLE_FONT_SIZE ) ) - qreg_contents_text_height: int = ( + qreg_contents_text_height: Final[int] = ( QREG_CONTENT_Y_SPACING + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, CARD_CONTENT_FONT_SIZE) + QREG_CONTENT_Y_SPACING @@ -114,7 +111,7 @@ def _get_required_size_for_content( column_header_height: int = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( option.font, CARD_CONTENT_FONT_SIZE ) - total_qreg_contents_text_height: int = n_qregs * qreg_contents_text_height + total_qreg_contents_text_height: Final[int] = n_qregs * qreg_contents_text_height total_simulation_run_group_box_height = ( CARD_CONTENT_PADDING + group_box_title_height @@ -128,17 +125,17 @@ def _get_required_size_for_content( + CARD_CONTENT_PADDING ) - qreg_name_and_layout_info_column_width: int = ( + qreg_name_and_layout_info_column_width: Final[int] = ( SimulationRunOverviewStyledItemDelegate._get_required_qreg_name_and_layout_column_width( option, index, CARD_TITLE_FONT_SIZE ) ) - qreg_content_header_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + qreg_content_header_width: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, option.font, CARD_CONTENT_FONT_SIZE ) - max_qreg_qubits_column_width: int = ( + max_qreg_qubits_column_width: Final[int] = ( SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( option, index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), @@ -147,10 +144,10 @@ def _get_required_size_for_content( ) ) - required_help_text_width: int = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( - HELP_TEXT, option.font, HELP_TEXT_SIZE + required_help_text_width: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( + QREG_CONTENTS_HELP_TEXT, option.font, QREG_CONTENTS_HELP_TEXT_FONT_SIZE ) - max_qreg_content_column_width: int = max(qreg_content_header_width, max_qreg_qubits_column_width) + max_qreg_content_column_width: Final[int] = max(qreg_content_header_width, max_qreg_qubits_column_width) total_simulation_run_group_box_width = max( group_box_title_width, ( @@ -175,8 +172,7 @@ def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) ) available_content_rect: Final[QtCore.QRect] = option.rect return QtCore.QSize( - min(available_content_rect.width(), required_content_size.width()), - min(available_content_rect.height(), required_content_size.height()), + min(required_content_size.width(), available_content_rect.width()), required_content_size.height() ) def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: @@ -184,6 +180,8 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, return associated_sim_run_model: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) + largest_qreg_size: Final[int] = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) + SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) available_rect_for_content: QtCore.QRect = option.rect.adjusted( CARD_CONTENT_PADDING, @@ -267,6 +265,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, associated_sim_run_model.expected_output_state, first_qubit_of_qreg=qreg_layout.first_qubit_of_qreg, qreg_size=qreg_layout.qreg_size, + largest_qreg_size=largest_qreg_size, available_content_rect=header_row_column_three_text_rect.adjusted( 0, curr_row_y_offset, 0, curr_row_y_offset ), @@ -280,6 +279,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, associated_sim_run_model.actual_output_state, first_qubit_of_qreg=qreg_layout.first_qubit_of_qreg, qreg_size=qreg_layout.qreg_size, + largest_qreg_size=largest_qreg_size, available_content_rect=header_row_column_three_text_rect.adjusted( 0, curr_row_y_offset + per_row_y_offset, 0, curr_row_y_offset + per_row_y_offset ), @@ -293,13 +293,15 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, header_row_column_one_text_rect.topLeft().x(), header_row_column_one_text_rect.topLeft().y() + y_offset_to_help_text, available_rect_for_content.width(), - SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text(option.font, HELP_TEXT_SIZE), + SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, QREG_CONTENTS_HELP_TEXT_FONT_SIZE + ), ) SimulationRunOverviewStyledItemDelegate._draw_elided_text( painter, - HELP_TEXT, + QREG_CONTENTS_HELP_TEXT, help_text_content_rect, - font_size=HELP_TEXT_SIZE, + font_size=QREG_CONTENTS_HELP_TEXT_FONT_SIZE, text_color=QtCore.Qt.GlobalColor.gray, ) painter.restore() @@ -361,7 +363,16 @@ def _draw_and_determine_column_headers( ) + QREG_CONTENT_X_SPACING ) - output_state_qreg_content_column_width: int = input_state_qreg_content_column_width + output_state_qreg_content_column_width: int = ( + QREG_CONTENT_X_SPACING + + SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, + index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE), + CARD_CONTENT_FONT_SIZE, + does_content_include_prefix=True, + ) + + QREG_CONTENT_X_SPACING + ) scaled_column_widths: list[int] = ( SimulationRunOverviewStyledItemDelegate._scale_column_widths_based_on_ratio_to_total_available_width( @@ -451,6 +462,7 @@ def _draw_qreg_contents( qreg_contents_container: syrec.n_bit_values_container | None, first_qubit_of_qreg: int, qreg_size: int, + largest_qreg_size: int, available_content_rect: QtCore.QRect, font_size: int, ) -> None: @@ -479,8 +491,10 @@ def _draw_qreg_contents( DEFAULT_ACTUAL_OUTPUT_STATE_QREG_CONTENT_PREFIX, option.font, font_size ), ) - required_qreg_contents_width: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_width_of_text( - stringified_qreg_contents, option.font, font_size + required_qreg_contents_width: Final[int] = ( + SimulationRunOverviewStyledItemDelegate._get_estimated_quantum_register_contents_column_width( + option, largest_qreg_size, font_size + ) ) total_qreg_contents_width: Final[int] = ( required_qreg_prefix_width + QREG_CONTENT_X_SPACING + required_qreg_contents_width From cd04d5ace4b1fb8b25789af0576d971177102ed6 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 25 Jan 2026 18:43:46 +0100 Subject: [PATCH 46/88] Simulation run editor now displays both the expected as well as the actual output state --- .../dialogs/simulation_run_editor_dialog.py | 407 ++++++++++++++---- 1 file changed, 317 insertions(+), 90 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index dc491861..6971049b 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -8,9 +8,12 @@ from __future__ import annotations +from dataclasses import dataclass +from enum import Enum from typing import TYPE_CHECKING, Final, cast from PyQt6 import QtCore, QtGui, QtWidgets +from typing_extensions import assert_never from mqt import syrec @@ -31,6 +34,18 @@ ) +class QubitLocation(Enum): + INPUT_STATE = 0 + EXPECTED_OUTPUT_STATE = 1 + ACTUAL_OUTPUT_STATE = 2 + + +@dataclass(frozen=True) +class QubitValueLabelAndCheckbox: + optional_label: QtWidgets.QLabel | None + checkbox: QtWidgets.QCheckBox + + class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] focus_out = QtCore.pyqtSignal(name="focusOut") @@ -55,15 +70,30 @@ def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 self.focus_out.emit() +@dataclass(frozen=True) +class QRegContentsLabelAndCheckbox: + optional_label: QtWidgets.QLabel | None + contents_widget: QtWidgets.QLabel | LineEditWithDynamicWidth + + QUBIT_LABEL_NAME_FORMAT: Final[str] = "q_{qubit:d}_lbl" INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT: Final[str] = "q_{qubit:d}_in_checkB" -OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT: Final[str] = "q_{qubit:d}_out_checkB" +EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT: Final[str] = "q_{qubit:d}_expected_out_checkB" +EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT: Final[str] = "q_{qubit:d}_expected_out_checkB_lbl" +ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT: Final[str] = "q_{qubit:d}_actual_out_checkB" +ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT: Final[str] = "q_{qubit:d}_actual_out_checkB_lbl" QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_qubit_values_groupbox" QREG_LABEL_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_lbl" QREG_LAYOUT_INFO_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_layout_info_lbl" QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_input_state" -QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_output_state" + +EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_expected_output_state" +EXPECTED_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_expected_output_state_lbl" + +ACTUAL_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_actual_output_state" +ACTUAL_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_actual_output_state_lbl" + QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_qubit_values_toggle" QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT: Final[str] = "qreg_{qreg_name:s}_qubit_search_input" @@ -100,6 +130,9 @@ def __init__( initial_expected_output_state: syrec.n_bit_values_container | None = ( self.edited_simulation_run_model.expected_output_state ) + initial_actual_output_state: syrec.n_bit_values_container | None = ( + self.edited_simulation_run_model.actual_output_state + ) self.setModal(True) self.setSizeGripEnabled(True) @@ -172,30 +205,51 @@ def __init__( alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) - input_state_edit_field = self._create_in_or_out_state_edit_field( - qreg_layout, optional_qreg_qubit_values=initial_input_state, is_created_for_input_state=True + input_state_widgets: QRegContentsLabelAndCheckbox = self._create_in_or_out_state_edit_field( + qreg_layout, optional_qreg_qubit_values=initial_input_state, qubit_location=QubitLocation.INPUT_STATE ) quantum_register_controls_grid_layout.addWidget( - input_state_edit_field, + input_state_widgets.contents_widget, qreg_controls_grid_row, 1, alignment=QtCore.Qt.AlignmentFlag.AlignCenter, ) - output_state_edit_field = self._create_in_or_out_state_edit_field( - qreg_layout, optional_qreg_qubit_values=initial_expected_output_state, is_created_for_input_state=False + expected_output_state_widgets: QRegContentsLabelAndCheckbox = self._create_in_or_out_state_edit_field( + qreg_layout, + optional_qreg_qubit_values=initial_expected_output_state, + qubit_location=QubitLocation.EXPECTED_OUTPUT_STATE, ) - quantum_register_controls_grid_layout.addWidget( - output_state_edit_field, + actual_output_state_widgets: QRegContentsLabelAndCheckbox = self._create_in_or_out_state_edit_field( + qreg_layout, + optional_qreg_qubit_values=initial_actual_output_state, + qubit_location=QubitLocation.ACTUAL_OUTPUT_STATE, + ) + + output_state_widgets_layout: QtWidgets.QGridLayout = QtWidgets.QGridLayout() + output_state_widgets_layout.addWidget(expected_output_state_widgets.optional_label, 0, 0) + output_state_widgets_layout.addWidget(expected_output_state_widgets.contents_widget, 0, 1) + output_state_widgets_layout.addWidget(actual_output_state_widgets.optional_label, 1, 0) + output_state_widgets_layout.addWidget(actual_output_state_widgets.contents_widget, 1, 1) + + output_state_widgets_layout.setColumnStretch(0, 1) + output_state_widgets_layout.setColumnStretch(1, 0) + output_state_widgets_layout.setColumnStretch(2, 1) + quantum_register_controls_grid_layout.addLayout( + output_state_widgets_layout, qreg_controls_grid_row, 2, - alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + 2, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) edit_qubit_values_toggle_button = QtWidgets.QPushButton( "Edit qubit values", objectName=QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) - quantum_register_controls_grid_layout.addWidget(edit_qubit_values_toggle_button, qreg_controls_grid_row, 3) + quantum_register_controls_grid_layout.addWidget( + edit_qubit_values_toggle_button, qreg_controls_grid_row, 3, 2, 1 + ) # We need to ignore the checked parameter that is passed to the clicked slot of the QPushButton edit_qubit_values_toggle_button.clicked.connect( lambda _, associated_qreg_name=qreg_name: self._handle_qreg_qubit_values_edit_toggle_button_click( @@ -216,7 +270,9 @@ def __init__( n_cols_in_quantum_register_controls_grid_layout: int = 3 qreg_controls_grid_row += 1 quantum_register_controls_grid_layout.addWidget( - self._create_qubit_controls_groupbox(qreg_layout, initial_input_state, initial_expected_output_state), + self._create_qubit_controls_groupbox( + qreg_layout, initial_input_state, initial_expected_output_state, initial_actual_output_state + ), qreg_controls_grid_row, 0, 1, @@ -313,9 +369,24 @@ def _handle_quantum_register_name_search(self) -> None: QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) - optional_qreg_output_state_input_field: QtWidgets.QLineEdit | None = ( + optional_qreg_expected_output_state_label: QtWidgets.QLabel | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLabel, EXPECTED_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) + ) + ) + optional_qreg_expected_output_state_input_field: QtWidgets.QLineEdit | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + ) + ) + optional_qreg_actual_output_state_label: QtWidgets.QLabel | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLabel, ACTUAL_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) + ) + ) + optional_qreg_actual_output_state_widget: QtWidgets.QLabel | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QLabel, ACTUAL_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( @@ -330,7 +401,10 @@ def _handle_quantum_register_name_search(self) -> None: optional_qreg_name_label, optional_qreg_layout_info_label, optional_qreg_input_state_input_field, - optional_qreg_output_state_input_field, + optional_qreg_expected_output_state_label, + optional_qreg_expected_output_state_input_field, + optional_qreg_actual_output_state_label, + optional_qreg_actual_output_state_widget, optional_qreg_edit_qubit_values_toggle_button, ], f"Failed to locate all required Qt widgets required for quantum register '{qreg_name}' during quantum register search", @@ -341,7 +415,12 @@ def _handle_quantum_register_name_search(self) -> None: qreg_name_label = cast("QtWidgets.QLabel", optional_qreg_name_label) qreg_layout_info_label = cast("QtWidgets.QLabel", optional_qreg_layout_info_label) qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) - qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) + qreg_expected_output_state_label = cast("QtWidgets.QLabel", optional_qreg_expected_output_state_label) + qreg_expected_output_state_input_field = cast( + "QtWidgets.QLineEdit", optional_qreg_expected_output_state_input_field + ) + qreg_actual_output_state_label = cast("QtWidgets.QLabel", optional_qreg_actual_output_state_label) + qreg_actual_output_state_widget = cast("QtWidgets.QLabel", optional_qreg_actual_output_state_widget) qreg_edit_qubit_values_toggle_button = cast( "QtWidgets.QPushButton", optional_qreg_edit_qubit_values_toggle_button ) @@ -352,7 +431,10 @@ def _handle_quantum_register_name_search(self) -> None: qreg_name_label.setVisible(should_control_be_visible) qreg_layout_info_label.setVisible(should_control_be_visible) qreg_input_state_input_field.setVisible(should_control_be_visible) - qreg_output_state_input_field.setVisible(should_control_be_visible) + qreg_expected_output_state_label.setVisible(should_control_be_visible) + qreg_expected_output_state_input_field.setVisible(should_control_be_visible) + qreg_actual_output_state_label.setVisible(should_control_be_visible) + qreg_actual_output_state_widget.setVisible(should_control_be_visible) qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) def _handle_input_state_qubit_value_checkbox_state_change( @@ -406,18 +488,19 @@ def _handle_input_state_qubit_value_checkbox_state_change( + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] ) - def _handle_output_state_qubit_value_checkbox_state_change( + def _handle_expected_output_state_qubit_value_checkbox_state_change( self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, - OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), + EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), ) ) optional_qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) + QtWidgets.QLineEdit, + EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), ) if not self._assert_all_required_widgets_found_or_close_dialog( @@ -493,13 +576,47 @@ def _create_in_or_out_state_edit_field( self, qreg_layout: QuantumRegisterLayout, optional_qreg_qubit_values: syrec.n_bit_values_container | None, - is_created_for_input_state: bool, - ) -> LineEditWithDynamicWidth: + qubit_location: QubitLocation, + ) -> QRegContentsLabelAndCheckbox: + + unknown_qreg_contents_placeholder: Final[str] = "-" + prefix_label: QtWidgets.QLabel | None = None + match qubit_location: + case QubitLocation.INPUT_STATE: + prefix_label = None + case QubitLocation.EXPECTED_OUTPUT_STATE: + prefix_label = QtWidgets.QLabel( + "Expected:", + objectName=EXPECTED_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT.format( + qreg_name=qreg_layout.qreg_name + ), + ) + case QubitLocation.ACTUAL_OUTPUT_STATE: + prefix_label = QtWidgets.QLabel( + "Actual:", + objectName=ACTUAL_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT.format( + qreg_name=qreg_layout.qreg_name + ), + ) + actual_contents_widget = QtWidgets.QLabel( + SimulationRunEditorDialog._stringify_some_qubits_of_n_bit_values_container( + optional_qreg_qubit_values, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size + ) + if optional_qreg_qubit_values is not None + else unknown_qreg_contents_placeholder, + objectName=ACTUAL_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_layout.qreg_name), + ) + return QRegContentsLabelAndCheckbox(prefix_label, actual_contents_widget) + case _: + # Added guard to fail on unhandled new qubit location enum values + assert_never(qubit_location) + + is_control_created_for_input_state: Final[bool] = qubit_location == QubitLocation.INPUT_STATE in_or_out_state_edit_field = LineEditWithDynamicWidth(qreg_layout.qreg_size) in_or_out_state_edit_field.setObjectName( QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_layout.qreg_name) - if is_created_for_input_state - else QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_layout.qreg_name) + if is_control_created_for_input_state + else EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_layout.qreg_name) ) if optional_qreg_qubit_values is not None: @@ -523,20 +640,20 @@ def _create_in_or_out_state_edit_field( # One could in a custom overwrite of the QLineEdit class use the focusOutEvent to emit a custom signal when the QLineEdit element looses focus or use the application-level focusChanged signal to determine # whether the QLineEdit widget lost focus. in_or_out_state_edit_field.editingFinished.connect( - lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_created_for_input_state: ( + lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_control_created_for_input_state: ( self._handle_input_or_output_state_text_change( associated_qreg_name, expected_text_length, is_editing_input_state ) ) ) in_or_out_state_edit_field.focusOut.connect( - lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_created_for_input_state: ( + lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_control_created_for_input_state: ( self._handle_input_or_output_state_text_change( associated_qreg_name, expected_text_length, is_editing_input_state ) ) ) - return in_or_out_state_edit_field + return QRegContentsLabelAndCheckbox(prefix_label, in_or_out_state_edit_field) def _create_in_or_out_state_qubit_value_checkbox( self, @@ -544,13 +661,26 @@ def _create_in_or_out_state_qubit_value_checkbox( optional_qreg_qubit_values: syrec.n_bit_values_container | None, associated_qubit: int, relative_qubit_index_in_qreg: int, - is_qubit_in_input_state: bool, - ) -> QtWidgets.QCheckBox: - qubit_value_checkbox = QtWidgets.QCheckBox( - objectName=INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit) - if is_qubit_in_input_state - else OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit) - ) + qubit_location: QubitLocation, + ) -> QubitValueLabelAndCheckbox: + + qubit_value_checkbox_objectname: str = "" + match qubit_location: + case QubitLocation.INPUT_STATE: + qubit_value_checkbox_objectname = INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit) + case QubitLocation.EXPECTED_OUTPUT_STATE: + qubit_value_checkbox_objectname = EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format( + qubit=associated_qubit + ) + case QubitLocation.ACTUAL_OUTPUT_STATE: + qubit_value_checkbox_objectname = ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format( + qubit=associated_qubit + ) + case _: + # Added guard to fail on unhandled new qubit location enum values + assert_never(qubit_location) + + qubit_value_checkbox = QtWidgets.QCheckBox(objectName=qubit_value_checkbox_objectname) qubit_value_checkbox.setText( STRINGIFIED_QUBIT_VALUE_FORMAT.format( stringified_qubit_value=SimulationRunEditorDialog._stringify_qubit_value( @@ -561,19 +691,37 @@ def _create_in_or_out_state_qubit_value_checkbox( ) ) ) - qubit_value_checkbox.setEnabled(optional_qreg_qubit_values is not None) - qubit_value_checkbox.checkStateChanged.connect( - lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( - self._handle_input_state_qubit_value_checkbox_state_change( - associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + qubit_value_checkbox.setEnabled( + optional_qreg_qubit_values is not None and qubit_location != QubitLocation.ACTUAL_OUTPUT_STATE + ) + qubit_value_label: QtWidgets.QLabel | None = None + match qubit_location: + case QubitLocation.INPUT_STATE: + qubit_value_checkbox.checkStateChanged.connect( + lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( + self._handle_input_state_qubit_value_checkbox_state_change( + associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + ) + ) ) - if is_qubit_in_input_state - else self._handle_output_state_qubit_value_checkbox_state_change( - associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + case QubitLocation.EXPECTED_OUTPUT_STATE: + qubit_value_checkbox.checkStateChanged.connect( + lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( + self._handle_expected_output_state_qubit_value_checkbox_state_change( + associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + ) + ) ) - ) - ) - return qubit_value_checkbox + qubit_value_label = QtWidgets.QLabel( + "Expected:", + objectName=EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=associated_qubit), + ) + case QubitLocation.ACTUAL_OUTPUT_STATE: + qubit_value_label = QtWidgets.QLabel( + "Actual:", + objectName=ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=associated_qubit), + ) + return QubitValueLabelAndCheckbox(qubit_value_label, qubit_value_checkbox) def _create_search_controls_for_qubits_of_qreg( self, associated_qreg_layout: QuantumRegisterLayout @@ -615,8 +763,11 @@ def _create_qubit_controls_groupbox( associated_qreg_layout: QuantumRegisterLayout, initial_input_state: syrec.n_bit_values_container, initial_expected_output_state: syrec.n_bit_values_container | None, + initial_actual_output_state: syrec.n_bit_values_container | None, ) -> QtWidgets.QWidget: input_output_qubits_value_controls_groupbox_layout = QtWidgets.QGridLayout() + # The inability to use named parameters for the addWidget(...) or addLayout(...) calls makes the code a bit more harder read (https://forum.qt.io/topic/160589/pyside6-unsupported-keyword-on-grid-layout/2) + # when used in combination with a QGridLayout. input_output_qubits_value_controls_groupbox_layout.addLayout( self._create_search_controls_for_qubits_of_qreg(associated_qreg_layout), 0, @@ -637,36 +788,83 @@ def _create_qubit_controls_groupbox( "Qubit: " + fetched_internal_qubit_label if fetched_internal_qubit_label is not None else "", objectName=QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit), ) + + qubit_controls_grid_layout_row: int = 1 + (2 * relative_qubit_index_in_qreg) input_output_qubits_value_controls_groupbox_layout.addWidget( - qubit_label, 1 + relative_qubit_index_in_qreg, 0 + qubit_label, qubit_controls_grid_layout_row, 0, 2, 1 ) - input_state_qubit_value_checkbox: QtWidgets.QCheckBox = self._create_in_or_out_state_qubit_value_checkbox( - associated_qreg_layout.qreg_name, - initial_input_state, - associated_qubit=qubit, - relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, - is_qubit_in_input_state=True, + input_state_qubit_value_checkbox_and_lbl: QubitValueLabelAndCheckbox = ( + self._create_in_or_out_state_qubit_value_checkbox( + associated_qreg_layout.qreg_name, + initial_input_state, + associated_qubit=qubit, + relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, + qubit_location=QubitLocation.INPUT_STATE, + ) ) input_output_qubits_value_controls_groupbox_layout.addWidget( - input_state_qubit_value_checkbox, - 1 + relative_qubit_index_in_qreg, - 1, - alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + input_state_qubit_value_checkbox_and_lbl.checkbox, qubit_controls_grid_layout_row, 1, 2, 1 ) - output_state_qubit_value_checkbox: QtWidgets.QCheckBox = self._create_in_or_out_state_qubit_value_checkbox( - associated_qreg_layout.qreg_name, - initial_expected_output_state, - associated_qubit=qubit, - relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, - is_qubit_in_input_state=False, + expected_output_state_qubit_value_checkbox_and_lbl: QubitValueLabelAndCheckbox = ( + self._create_in_or_out_state_qubit_value_checkbox( + associated_qreg_layout.qreg_name, + initial_expected_output_state, + associated_qubit=qubit, + relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, + qubit_location=QubitLocation.EXPECTED_OUTPUT_STATE, + ) ) - input_output_qubits_value_controls_groupbox_layout.addWidget( - output_state_qubit_value_checkbox, - 1 + relative_qubit_index_in_qreg, + + output_qubits_controls_layout = QtWidgets.QGridLayout() + if expected_output_state_qubit_value_checkbox_and_lbl.optional_label is None: + log_error_to_console( + f"Failed to create label for expected output state qubit {relative_qubit_index_in_qreg}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + else: + output_qubits_controls_layout.addWidget( + expected_output_state_qubit_value_checkbox_and_lbl.optional_label, 0, 0 + ) + output_qubits_controls_layout.addWidget(expected_output_state_qubit_value_checkbox_and_lbl.checkbox, 0, 1) + + actual_output_state_qubit_value_checkbox_and_lbl: QubitValueLabelAndCheckbox = ( + self._create_in_or_out_state_qubit_value_checkbox( + associated_qreg_layout.qreg_name, + initial_actual_output_state, + associated_qubit=qubit, + relative_qubit_index_in_qreg=relative_qubit_index_in_qreg, + qubit_location=QubitLocation.ACTUAL_OUTPUT_STATE, + ) + ) + + if actual_output_state_qubit_value_checkbox_and_lbl.optional_label is None: + log_error_to_console( + f"Failed to create label for actual output state qubit {relative_qubit_index_in_qreg}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + else: + output_qubits_controls_layout.addWidget( + actual_output_state_qubit_value_checkbox_and_lbl.optional_label, 1, 0 + ) + output_qubits_controls_layout.addWidget(actual_output_state_qubit_value_checkbox_and_lbl.checkbox, 1, 1) + + output_qubits_controls_layout.addItem( + QtWidgets.QSpacerItem( + 2, 2, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding + ), + qubit_controls_grid_layout_row, 2, - alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + 1, + 1, + ) + + output_qubits_controls_layout.setColumnStretch(0, 0) + output_qubits_controls_layout.setColumnStretch(1, 1) + output_qubits_controls_layout.setColumnStretch(2, 1) + input_output_qubits_value_controls_groupbox_layout.addLayout( + output_qubits_controls_layout, qubit_controls_grid_layout_row, 2, 2, 1 ) input_output_qubits_value_controls_groupbox_layout.setColumnStretch(0, 1) @@ -725,17 +923,34 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - optional_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, - OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + optional_expected_output_state_qubit_checkbox_label: QtWidgets.QLabel | None = ( + qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=qubit) + ) + ) + optional_expected_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( + qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) + ) + optional_actual_output_state_qubit_checkbox_label: QtWidgets.QLabel | None = ( + qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=qubit) + ) + ) + optional_actual_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( + qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) ) - if not self._assert_all_required_widgets_found_or_close_dialog( [ optional_qubit_value_label, optional_input_state_qubit_checkbox, - optional_output_state_qubit_checkbox, + optional_expected_output_state_qubit_checkbox_label, + optional_expected_output_state_qubit_checkbox, + optional_actual_output_state_qubit_checkbox_label, + optional_actual_output_state_qubit_checkbox, ], f"Failed to find required controls for qubits for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", ): @@ -743,14 +958,28 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ qubit_value_label = cast("QtWidgets.QLabel", optional_qubit_value_label) input_state_qubit_checkbox = cast("QtWidgets.QCheckBox", optional_input_state_qubit_checkbox) - output_state_qubit_checkbox = cast("QtWidgets.QCheckBox", optional_output_state_qubit_checkbox) + expected_output_state_qubit_checkbox_label = cast( + "QtWidgets.QLabel", optional_expected_output_state_qubit_checkbox_label + ) + expected_output_state_qubit_checkbox = cast( + "QtWidgets.QCheckBox", optional_expected_output_state_qubit_checkbox + ) + actual_output_state_qubit_checkbox_label = cast( + "QtWidgets.QLabel", optional_actual_output_state_qubit_checkbox_label + ) + actual_output_state_qubit_checkbox = cast( + "QtWidgets.QCheckBox", optional_actual_output_state_qubit_checkbox + ) does_qubit_label_match_search_text: bool = self.annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal ).startswith(qubit_search_input_field.text()) qubit_value_label.setVisible(does_qubit_label_match_search_text) input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) - output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + expected_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) + expected_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + actual_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) + actual_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: for qreg_layout in self.qreg_layouts: @@ -762,11 +991,9 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - optional_qreg_output_state_input_field: QtWidgets.QtWidget | None = ( + optional_qreg_expected_output_state_input_field: QtWidgets.QtWidget | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, - QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( @@ -799,7 +1026,7 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam if not self._assert_all_required_widgets_found_or_close_dialog( [ optional_qreg_input_state_input_field, - optional_qreg_output_state_input_field, + optional_qreg_expected_output_state_input_field, optional_qubit_values_groupbox, optional_qubit_values_toggle_button, optional_expected_output_state_value_toggle_button, @@ -810,7 +1037,9 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam return qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) - qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) + qreg_expected_output_state_input_field = cast( + "QtWidgets.QLineEdit", optional_qreg_expected_output_state_input_field + ) qubit_values_groupbox = cast("QtWidgets.QCheckBox", optional_qubit_values_groupbox) qubit_values_toggle_button = cast("QtWidgets.QPushButton", optional_qubit_values_toggle_button) expected_output_state_value_toggle_button = cast( @@ -824,14 +1053,14 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam qubit_values_groupbox.setVisible(True) qubit_values_toggle_button.setText("Toggle qubit values edit") qreg_input_state_input_field.setEnabled(False) - qreg_output_state_input_field.setEnabled(False) + qreg_expected_output_state_input_field.setEnabled(False) expected_output_state_value_toggle_button.setEnabled(False) else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText("Edit qubit values") expected_output_state_value_toggle_button.setEnabled(True) qreg_input_state_input_field.setEnabled(True) - qreg_output_state_input_field.setEnabled(qreg_output_state_input_field.text() != "") # noqa: PLC1901 + qreg_expected_output_state_input_field.setEnabled(qreg_expected_output_state_input_field.text() != "") # noqa: PLC1901 qubit_values_groupbox_qubit_search_field.setText("") self._handle_qubit_search_trigger_button_click(associated_qreg_name) @@ -872,7 +1101,7 @@ def _handle_init_expected_output_state_button_click(self, associated_qreg_name: optional_qreg_output_state_input_field: QtWidgets.QtWidget | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QLineEdit, QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) @@ -903,7 +1132,7 @@ def _handle_init_expected_output_state_button_click(self, associated_qreg_name: ): optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( self.simulation_run_wrapper_box.findChild( - QtWidgets.QCheckBox, OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) ) @@ -940,7 +1169,7 @@ def _handle_input_or_output_state_text_change( optional_output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, - QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), + EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) @@ -1033,7 +1262,7 @@ def _handle_input_or_output_state_text_change( optional_not_edited_output_state_text_field: QtWidgets.QLineEdit | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, - QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) @@ -1105,9 +1334,7 @@ def _handle_input_or_output_state_text_change( ) optional_not_edited_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( not_edited_qreg_qubit_values_groupbox.findChild( - QtWidgets.QCheckBox, - OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) ) @@ -1190,7 +1417,7 @@ def _assert_all_required_widgets_found_or_close_dialog( ) # We want to log the caller of this function as the origin of the error instead of this function itself. log_error_to_console( - f"Not all required Qt widgets found, provided error text: {error_dialog_content}!\n{stringified_found_widgets_object_names}", + f"{error_dialog_content}\n{stringified_found_widgets_object_names}", num_additionally_skipped_stack_frames_starting_from_caller_function=1, ) self.reject() From 4e9f401677c52cc05f4dd1367a79ffe8f459bdeb Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 25 Jan 2026 23:19:03 +0100 Subject: [PATCH 47/88] Quantum register and qubit search functionality now also includes expected/actual output qubit labels and checkboxes. Added reset of simulation run results prior to simulation run execution in worker. Simplified some validations in simulation run model. --- .../quantum_circuit_simulation_dialog.py | 17 +- .../dialogs/simulation_run_editor_dialog.py | 185 +++++++++++++----- .../simulation_view/simulation_run_model.py | 114 ++++++----- ...ation_run_overview_styled_item_delegate.py | 7 +- .../workers/simulation_run_worker.py | 23 ++- 5 files changed, 235 insertions(+), 111 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index cfe158d6..3c4d6356 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -197,6 +197,9 @@ def initialize_simulation_runs_tab_widget( ) save_simulation_runs_to_file_button.clicked.connect(self.handle_sim_run_save_to_file_btn_click) save_simulation_runs_to_file_button.setEnabled(False) + save_simulation_runs_to_file_button.setToolTip( + "Save the defined simulation runs to a .json file (only input state and expected output qubit values are exported)" + ) simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) run_simulation_runs_button = QtWidgets.QPushButton( @@ -210,13 +213,16 @@ def initialize_simulation_runs_tab_widget( run_simulation_runs_stop_at_first_failure_button = QtWidgets.QPushButton( QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart), - "Run simulation runs (stop at first failure)", + "Run simulation runs (stop at first output qubit values mismatch)", objectName=RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME, ) run_simulation_runs_stop_at_first_failure_button.setEnabled(False) run_simulation_runs_stop_at_first_failure_button.clicked.connect( self.handle_run_all_simulation_runs_stop_at_first_failure_button_click ) + run_simulation_runs_stop_at_first_failure_button.setToolTip( + "Perform a simulation of all defined simulation runs until a mismatch between the expected and actual output state qubit values is detected (the value of both output states needs to be known)" + ) simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_stop_at_first_failure_button) simulation_runs_execution_buttons_layout.addStretch() @@ -316,6 +322,7 @@ def handle_simulation_run_edit_btn_click(self) -> None: copy_of_reference_sim_run_model: SimulationRunModel = SimulationRunModel( reference_sim_run_model.input_state, reference_sim_run_model.expected_output_state, + reference_sim_run_model.actual_output_state, create_new_n_bit_values_container_instances=True, ) @@ -530,7 +537,6 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: n_input_state_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits - # TODO: A large number of state combinations will lag the UI thread since the generation runs on the UI thread pressed_message_box_button_in_all_sim_run_generation_warning: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( self, "Generating all possible input state combinations!", @@ -548,13 +554,6 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self.handle_open_and_start_all_input_states_generator_dialog( self.annotatable_quantum_computation.num_data_qubits ) - - # QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - # to_be_switched_to_tab_widget, True - # ) - # # TODO: Can we ignore return value? - # self.simulation_runs_model.add_all_possible_simulation_run_models() - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( prev_active_tab_widget, False ) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 6971049b..a1677ea9 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -106,6 +106,9 @@ class QRegContentsLabelAndCheckbox: "Qubit values of quantum register '{qreg_name:s}' can only be defined as a combination of '0' or '1' literals. Additionally, the value of all qubits of the quantum register (n={expected_num_qubit_values:d}) must be specified but only {actual_num_qubit_values:d} were defined in the {input_or_output_state_ident:s} state!" ) +EDIT_OUTPUT_STATE_QUBIT_VALUES: Final[str] = "Edit qubit values" +TOGGLE_OUTPUT_STATE_QUBIT_VALUES_EDIT: Final[str] = "Toggle qubit values edit" + class SimulationRunEditorDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( @@ -438,7 +441,12 @@ def _handle_quantum_register_name_search(self) -> None: qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) def _handle_input_state_qubit_value_checkbox_state_change( - self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int + self, + new_checkbox_state: QtCore.CheckState, + associated_qreg_name: str, + associated_qubit: int, + relative_qubit_index_in_quantum_register: int, + update_associated_state_input_field: bool = False, ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( self.simulation_run_wrapper_box.findChild( @@ -460,7 +468,7 @@ def _handle_input_state_qubit_value_checkbox_state_change( associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) - updated_qubit_value: bool = associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked + updated_qubit_value: bool = new_checkbox_state == QtCore.Qt.CheckState.Checked stringified_updated_qubit_value: str = SimulationRunEditorDialog._stringify_qubit_value( updated_qubit_value, return_as_high_low_state=True ) @@ -481,15 +489,23 @@ def _handle_input_state_qubit_value_checkbox_state_change( STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) ) - curr_stringified_input_state: str = qreg_input_state_input_field.text() - qreg_input_state_input_field.setText( - curr_stringified_input_state[:relative_qubit_index_in_quantum_register] - + SimulationRunEditorDialog._stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) - + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] - ) + if update_associated_state_input_field: + curr_stringified_input_state: str = qreg_input_state_input_field.text() + qreg_input_state_input_field.setText( + curr_stringified_input_state[:relative_qubit_index_in_quantum_register] + + SimulationRunEditorDialog._stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + + curr_stringified_input_state[relative_qubit_index_in_quantum_register + 1 :] + ) + else: + associated_qubit_value_checkbox.setCheckState(new_checkbox_state) def _handle_expected_output_state_qubit_value_checkbox_state_change( - self, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int + self, + new_checkbox_state: QtCore.CheckState, + associated_qreg_name: str, + associated_qubit: int, + relative_qubit_index_in_quantum_register: int, + update_associated_state_input_field: bool, ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( self.simulation_run_wrapper_box.findChild( @@ -512,7 +528,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) qreg_output_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_output_state_input_field) - updated_qubit_value: bool = associated_qubit_value_checkbox.checkState() == QtCore.Qt.CheckState.Checked + updated_qubit_value: bool = new_checkbox_state == QtCore.Qt.CheckState.Checked stringified_updated_qubit_value: str = SimulationRunEditorDialog._stringify_qubit_value( updated_qubit_value, return_as_high_low_state=True ) @@ -543,12 +559,15 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( associated_qubit_value_checkbox.setText( STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) ) - curr_stringified_output_state: str = qreg_output_state_input_field.text() - qreg_output_state_input_field.setText( - curr_stringified_output_state[:relative_qubit_index_in_quantum_register] - + SimulationRunEditorDialog._stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) - + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] - ) + if update_associated_state_input_field: + curr_stringified_output_state: str = qreg_output_state_input_field.text() + qreg_output_state_input_field.setText( + curr_stringified_output_state[:relative_qubit_index_in_quantum_register] + + SimulationRunEditorDialog._stringify_qubit_value(updated_qubit_value, return_as_high_low_state=False) + + curr_stringified_output_state[relative_qubit_index_in_quantum_register + 1 :] + ) + else: + associated_qubit_value_checkbox.setCheckState(new_checkbox_state) def _create_qreg_search_controls(self) -> QtWidgets.QLayout: qreg_search_controls_layout = QtWidgets.QHBoxLayout() @@ -684,13 +703,14 @@ def _create_in_or_out_state_qubit_value_checkbox( qubit_value_checkbox.setText( STRINGIFIED_QUBIT_VALUE_FORMAT.format( stringified_qubit_value=SimulationRunEditorDialog._stringify_qubit_value( - None - if optional_qreg_qubit_values is None - else optional_qreg_qubit_values.test(relative_qubit_index_in_qreg), + None if optional_qreg_qubit_values is None else optional_qreg_qubit_values.test(associated_qubit), return_as_high_low_state=True, ) ) ) + if optional_qreg_qubit_values is not None: + qubit_value_checkbox.setChecked(optional_qreg_qubit_values.test(associated_qubit)) + qubit_value_checkbox.setEnabled( optional_qreg_qubit_values is not None and qubit_location != QubitLocation.ACTUAL_OUTPUT_STATE ) @@ -698,17 +718,25 @@ def _create_in_or_out_state_qubit_value_checkbox( match qubit_location: case QubitLocation.INPUT_STATE: qubit_value_checkbox.checkStateChanged.connect( - lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( + lambda new_check_state, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( self._handle_input_state_qubit_value_checkbox_state_change( - associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + new_check_state, + associated_qreg_name, + associated_qubit, + relative_qubit_index_in_quantum_register, + update_associated_state_input_field=True, ) ) ) case QubitLocation.EXPECTED_OUTPUT_STATE: qubit_value_checkbox.checkStateChanged.connect( - lambda _, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( + lambda new_check_state, associated_qreg_name=associated_qreg_name, associated_qubit=associated_qubit, relative_qubit_index_in_quantum_register=relative_qubit_index_in_qreg: ( self._handle_expected_output_state_qubit_value_checkbox_state_change( - associated_qreg_name, associated_qubit, relative_qubit_index_in_quantum_register + new_check_state, + associated_qreg_name, + associated_qubit, + relative_qubit_index_in_quantum_register, + update_associated_state_input_field=True, ) ) ) @@ -1051,13 +1079,13 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam if qreg_name == associated_qreg_name and not qubit_values_groupbox.isVisible(): qubit_values_groupbox.setVisible(True) - qubit_values_toggle_button.setText("Toggle qubit values edit") + qubit_values_toggle_button.setText(TOGGLE_OUTPUT_STATE_QUBIT_VALUES_EDIT) qreg_input_state_input_field.setEnabled(False) qreg_expected_output_state_input_field.setEnabled(False) expected_output_state_value_toggle_button.setEnabled(False) else: qubit_values_groupbox.setVisible(False) - qubit_values_toggle_button.setText("Edit qubit values") + qubit_values_toggle_button.setText(EDIT_OUTPUT_STATE_QUBIT_VALUES) expected_output_state_value_toggle_button.setEnabled(True) qreg_input_state_input_field.setEnabled(True) qreg_expected_output_state_input_field.setEnabled(qreg_expected_output_state_input_field.text() != "") # noqa: PLC1901 @@ -1248,8 +1276,83 @@ def _handle_input_or_output_state_text_change( and (self.edited_simulation_run_model.expected_output_state is not None or not is_editing_input_state) ) + if are_stringified_qreg_contents_valid: + edited_qreg_layout: QuantumRegisterLayout | None = next( + filter(lambda qreg_layout: qreg_layout.qreg_name == associated_qreg_name, self.qreg_layouts), None + ) + if edited_qreg_layout is None: + self.failed_due_to_internal_error = True + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Could not find layout of quantum register!", + message_box_content=f"Failed to find layout of edited quantum register '{associated_qreg_name}'.\nUnsaved changed will be lost and edit dialog will be closed!", + is_cancellable=False, + log_contents=True, + ) + self.reject() + return + + optional_effected_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_effected_qreg_qubit_values_groupbox], + f"Failed to find required group box for edited quantum register '{associated_qreg_name}' QtWidgets during handling of input/output state edit!", + ): + return + + cast("QtWidgets.QGroupBox", optional_effected_qreg_qubit_values_groupbox) + + first_qubit_of_edited_qreg: Final[int] = edited_qreg_layout.first_qubit_of_qreg + n_qubits_of_edited_qreg: Final[int] = edited_qreg_layout.qreg_size + for qubit_of_edited_qreg in range( + first_qubit_of_edited_qreg, first_qubit_of_edited_qreg + n_qubits_of_edited_qreg + ): + relative_qubit_idx_in_qreg: int = qubit_of_edited_qreg - first_qubit_of_edited_qreg + new_checkbox_state: QtCore.Qt.CheckState = ( + QtCore.Qt.CheckState.Checked + if curr_edited_input_text_field.text()[relative_qubit_idx_in_qreg] == "1" + else QtCore.Qt.CheckState.Unchecked + ) + if is_editing_input_state: + self._handle_input_state_qubit_value_checkbox_state_change( + new_checkbox_state, + associated_qreg_name, + qubit_of_edited_qreg, + relative_qubit_idx_in_qreg, + update_associated_state_input_field=False, + ) + else: + self._handle_expected_output_state_qubit_value_checkbox_state_change( + new_checkbox_state, + associated_qreg_name, + qubit_of_edited_qreg, + relative_qubit_idx_in_qreg, + update_associated_state_input_field=False, + ) + for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name + optional_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_qreg_qubit_values_groupbox], + f"Failed to find required group box for not edited quantum register '{qreg_name}' QtWidgets during handling of input/output state edit!", + ): + return + + qreg_qubit_values_groupbox = cast("QtWidgets.QGroupBox", optional_qreg_qubit_values_groupbox) + if qreg_name != associated_qreg_name: optional_not_edited_input_state_text_field: QtWidgets.QLineEdit | None = ( self.simulation_run_wrapper_box.findChild( @@ -1295,45 +1398,31 @@ def _handle_input_or_output_state_text_change( "QtWidgets.QPushButton", optional_not_edited_qreg_qubit_values_edit_toggle_button ) - not_edited_input_state_text_field.setEnabled(are_stringified_qreg_contents_valid) + should_state_controls_be_visible: bool = ( + are_stringified_qreg_contents_valid and not qreg_qubit_values_groupbox.isVisible() + ) + not_edited_input_state_text_field.setEnabled(should_state_controls_be_visible) not_edited_output_state_text_field.setEnabled( - are_stringified_qreg_contents_valid + should_state_controls_be_visible and self.edited_simulation_run_model.expected_output_state is not None ) - not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(are_stringified_qreg_contents_valid) - - optional_not_edited_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = ( - self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, - QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, - ) - ) + not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(should_state_controls_be_visible) - if not self._assert_all_required_widgets_found_or_close_dialog( - [optional_not_edited_qreg_qubit_values_groupbox], - f"Failed to find required group box for not edited quantum register '{qreg_name}' QtWidgets during handling of input/output state edit!", - ): - return - - not_edited_qreg_qubit_values_groupbox = cast( - "QtWidgets.QGroupBox", optional_not_edited_qreg_qubit_values_groupbox - ) - if not not_edited_qreg_qubit_values_groupbox.isVisible(): + if not qreg_qubit_values_groupbox.isVisible(): continue first_qubit_of_qreg: int = qreg_layout.first_qubit_of_qreg n_qubits_of_qreg: int = qreg_layout.qreg_size for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + n_qubits_of_qreg): optional_not_edited_input_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( - not_edited_qreg_qubit_values_groupbox.findChild( + qreg_qubit_values_groupbox.findChild( QtWidgets.QCheckBox, INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) optional_not_edited_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( - not_edited_qreg_qubit_values_groupbox.findChild( + qreg_qubit_values_groupbox.findChild( QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) ) diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 968f59c6..64fd7bf1 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -15,22 +15,13 @@ from mqt import syrec +from ..logger_utils import log_error_to_console + if TYPE_CHECKING: from collections.abc import Iterable # Some debugging tips: https://www.eso.org/~eltmgr/ECS/documents-latest/CUT/sphinx_doc/latest/docs/500_gui_development.html#gdb # First custom item data role usable according to: https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum - -# TODO: Why does the mypy checker report the error "no-any-return" when processing the python function: -# def _get_vertical_text_width(options: QtWidgets.QStyleOptionsViewItem, font_size: int) -> int: -# return QtGui.QFontMetrics(QtGui.QFont(options.font.family(), font_size, options.font.weight())).height() -# -# The most common reason is that mypy does not have type information for the QtGui or QtWidgets modules. If you haven't installed the type stubs for your Qt bindings, mypy treats all calls to those libraries as returning Any. -# When you call .height(), mypy sees it as Any. Returning Any from a function marked as -> int triggers the no-any-return warning because mypy cannot verify that the value is actually an integer. -# Solution: -# Install the appropriate type stubs for your framework: -# - For PyQt6: pip install PyQt6-stubs -# - For PySide6: pip install shiboken6 (Type information is usually bundled, but ensure your environment is configured correctly). SIMULATION_RUN_IO_STATE_QT_ROLE: Final[int] = QtCore.Qt.ItemDataRole.UserRole QUANTUM_REGISTER_LAYOUT_QT_ROLE: Final[int] = SIMULATION_RUN_IO_STATE_QT_ROLE + 1 LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE: Final[int] = QUANTUM_REGISTER_LAYOUT_QT_ROLE + 1 @@ -58,15 +49,22 @@ def __init__( self, input_state: syrec.n_bit_values_container, expected_output_state: syrec.n_bit_values_container | None = None, + actual_output_state: syrec.n_bit_values_container | None = None, create_new_n_bit_values_container_instances: bool = False, ): if expected_output_state is not None and input_state.size() != expected_output_state.size(): msg = f"Expected output state size (n_qubits = {expected_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) + raise ValueError(msg) + if actual_output_state is not None and input_state.size() != actual_output_state.size(): + msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) if not create_new_n_bit_values_container_instances: self.input_state = input_state self.expected_output_state = expected_output_state + self.actual_output_state = actual_output_state else: self.input_state = syrec.n_bit_values_container(input_state.size()) for qubit in range(input_state.size()): @@ -75,6 +73,10 @@ def __init__( self.expected_output_state = syrec.n_bit_values_container(expected_output_state.size()) for qubit in range(expected_output_state.size()): self.expected_output_state.set(qubit, expected_output_state.test(qubit)) + if actual_output_state is not None: + self.actual_output_state = syrec.n_bit_values_container(actual_output_state.size()) + for qubit in range(actual_output_state.size()): + self.actual_output_state.set(qubit, actual_output_state.test(qubit)) def initialize_expected_output_state_as_copy_of_input_state(self) -> None: if self.expected_output_state is not None: @@ -90,28 +92,27 @@ def reset_result_of_execution(self) -> None: self.execution_runtime_in_ms = None def set_result_of_simulation_execution( - self, actual_output_state: syrec.n_bit_values_container, execution_runtime_in_ms: float + self, + actual_output_state: syrec.n_bit_values_container, + do_expected_and_actual_output_states_match: bool, + execution_runtime_in_ms: float, ) -> None: if actual_output_state.size() != self.input_state.size(): msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match input state size (n_qubits = {self.input_state.size()})" - raise ValueError(msg) - if self.expected_output_state is None: - msg = "Tried to set actual output state when expected output state was not set!" - raise ValueError(msg) - if self.expected_output_state.size() != actual_output_state.size(): - msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match expected output state size (n_qubits = {self.expected_output_state.size()})" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) if execution_runtime_in_ms < 0: msg = f"Invalid execution runtime value {execution_runtime_in_ms}" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) if self.actual_output_state is None: - self.actual_output_state = actual_output_state - else: self.actual_output_state = syrec.n_bit_values_container(self.input_state.size()) - for i in range(self.expected_output_state.size()): - self.actual_output_state.set(actual_output_state.test(i)) + for i in range(self.input_state.size()): + self.actual_output_state.set(i, actual_output_state.test(i)) + + self.do_expected_and_actual_outputs_match = do_expected_and_actual_output_states_match self.execution_runtime_in_ms = execution_runtime_in_ms def update_input_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: @@ -125,6 +126,32 @@ def update_expected_output_state_qubit_value(self, qubit: int, new_qubit_value: self.expected_output_state, qubit, new_qubit_value ) + def update_user_editable_data( + self, + edited_input_state: syrec.n_bit_values_container, + edited_expected_output_state: syrec.n_bit_values_container | None, + ) -> None: + if self.input_state.size() != edited_input_state.size(): + msg = f"Updated input state size state size (n_qubits = {edited_input_state.size()}) did not match current input state size (n_qubits = {self.input_state.size()})" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) + raise ValueError(msg) + + if edited_expected_output_state is not None and edited_expected_output_state.size() != self.input_state.size(): + msg = f"Expected output state size (n_qubits = {edited_expected_output_state.size()}) did not match input state size (n_qubits = {self.input_state.size()})" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) + raise ValueError(msg) + + for i in range(self.input_state.size()): + self.input_state.set(i, edited_input_state.test(i)) + + if edited_expected_output_state is None: + self.expected_output_state = None + else: + if self.expected_output_state is None: + self.expected_output_state = syrec.n_bit_values_container(self.input_state.size()) + for i in range(self.input_state.size()): + self.expected_output_state.set(i, edited_expected_output_state.test(i)) + @staticmethod def do_output_states_match( expected_output_state: syrec.n_bit_values_container | None, actual_output_state: syrec.n_bit_values_container @@ -134,6 +161,7 @@ def do_output_states_match( if expected_output_state.size() != actual_output_state.size(): msg = f"Expected output state to have {expected_output_state.size()} qubits but actual output state contained {actual_output_state.size()} qubits!" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) return all( @@ -262,7 +290,6 @@ def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: if self.is_model_index_valid(index): self.simulation_run_models.pop(index.row()) self.endRemoveRows() - # self.layoutChanged.emit() return True self.endRemoveRows() @@ -273,6 +300,14 @@ def delete_all_simulation_run_models(self) -> None: self.simulation_run_models.clear() self.endResetModel() + def reset_prev_simulation_run_execution_results(self) -> None: + if self.rowCount(QtCore.QModelIndex()) == 0: + return + + for sim_run_model in self.simulation_run_models: + sim_run_model.reset_result_of_execution() + self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(len(self.simulation_run_models) - 1, 0)) + def add_all_possible_simulation_run_models(self) -> bool: if self.rowCount(QtCore.QModelIndex()) > 0: return False @@ -299,44 +334,31 @@ def update_edited_simulation_run_model( ) -> None: if not self.is_model_index_valid(index): msg = "Invalid model index!" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) - # TODO: Further validation - - self.simulation_run_models[index.row()] = updated_simulation_run_data + self.simulation_run_models[index.row()].update_user_editable_data( + updated_simulation_run_data.input_state, updated_simulation_run_data.expected_output_state + ) self.dataChanged.emit(index, index) # TODO: Check that no duplicate input or expected output_state is added - # TODO: Add custom error messages if validation fails def update_model_using_simulation_run_result( self, index: QtCore.QModelIndex, actual_output_state: syrec.n_bit_values_container, - do_expected_and_actual_outputs_match: bool | None, + do_expected_and_actual_output_states_match: bool | None, execution_runtime_in_ms: float, ) -> None: if not self.is_model_index_valid(index): msg = "Invalid model index!" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) - to_be_updated_simulation_run_model: SimulationRunModel = self.simulation_run_models[index.row()] - # TODO: Should we validate that the current expected output state is equal to the input state - # if updated_simulation_run_model.expected_output_state is not None and updated_simulation_run_model.expected_output_state.size() != to_be_updated_simulation_run_model.input_state.size(): - # msg = "Input state sizes did not match" - # raise ValueError(msg) - - if ( - actual_output_state is not None - and actual_output_state.size() != to_be_updated_simulation_run_model.input_state.size() - ): - msg = "Input state sizes did not match" - raise ValueError(msg) - - self.simulation_run_models[index.row()].actual_output_state = actual_output_state - self.simulation_run_models[ - index.row() - ].do_expected_and_actual_outputs_match = do_expected_and_actual_outputs_match - self.simulation_run_models[index.row()].execution_runtime_in_ms = execution_runtime_in_ms + self.simulation_run_models[index.row()] + self.simulation_run_models[index.row()].set_result_of_simulation_execution( + actual_output_state, do_expected_and_actual_output_states_match, execution_runtime_in_ms + ) self.dataChanged.emit(index, index) def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index 70ffd7e2..3fb15307 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -541,6 +541,7 @@ def _draw_qreg_contents( qreg_contents_prefix, qreg_prefix_text_rect, font_size, + text_alignment=QtCore.Qt.AlignmentFlag.AlignRight, text_color=QtCore.Qt.GlobalColor.gray, ) @@ -551,5 +552,9 @@ def _draw_qreg_contents( qreg_contents_text_height, ) SimulationRunOverviewStyledItemDelegate._draw_elided_text( - painter, stringified_qreg_contents, qreg_contents_text_rect, font_size + painter, + stringified_qreg_contents, + qreg_contents_text_rect, + font_size, + text_alignment=QtCore.Qt.AlignmentFlag.AlignLeft, ) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 5263d4c2..d032aaea 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -50,6 +50,8 @@ def __init__( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: + curr_sim_run_num: int = 0 + try: SimulationRunWorker._validate_parameters(self.expected_input_state_size, self.batch_size) self_raised_error_msg: str | None = "" @@ -64,14 +66,23 @@ def start_simulations(self) -> None: batch_start_timestamp: float = 0 batch_timestamps: BatchTimestamps | None = None n_sim_runs_to_execute: Final[int] = self.shared_simulation_runs_model.rowCount(QtCore.QModelIndex()) + self.shared_simulation_runs_model.reset_prev_simulation_run_execution_results() batch_idx: int = 0 - curr_sim_run_num: int = 0 do_output_states_match: bool | None = None - while not self.is_cancellation_requested() and curr_sim_run_num < n_sim_runs_to_execute: + found_first_output_state_mismatch: bool = False + while ( + not self.is_cancellation_requested() + and curr_sim_run_num < n_sim_runs_to_execute + and (not self.should_stop_at_first_output_state_mismatch or not found_first_output_state_mismatch) + ): batch_start_timestamp = SimulationRunWorker._get_timestamp() for _ in range(self.batch_size): - if self.is_cancellation_requested() or curr_sim_run_num == n_sim_runs_to_execute: + if ( + self.is_cancellation_requested() + or curr_sim_run_num == n_sim_runs_to_execute + or (self.should_stop_at_first_output_state_mismatch and found_first_output_state_mismatch) + ): break curr_sim_run_model: SimulationRunModel = SimulationRunWorker._fetch_sim_model_or_throw( @@ -92,13 +103,11 @@ def start_simulations(self) -> None: do_output_states_match, ) - if ( + found_first_output_state_mismatch = ( self.should_stop_at_first_output_state_mismatch and do_output_states_match is not None and not do_output_states_match - ): - self.set_cancellation_requested_flag(True) - + ) curr_sim_run_num += 1 batch_idx += 1 if batch_idx > 0 and batch_idx != self.batch_size: From 2f56a450be409a62c4baf18ecb989893d87e6468 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Mon, 26 Jan 2026 19:49:53 +0100 Subject: [PATCH 48/88] Improvided error reporting on expected batch data type missmatch --- .../quantum_circuit_simulation_dialog.py | 2 +- .../all_input_states_generator_dialog.py | 11 ++-- .../dialogs/simulation_run_dialog.py | 11 ++-- .../dialogs/simulation_run_editor_dialog.py | 5 +- .../simulation_run_json_export_dialog.py | 49 +++++++++++------ .../simulation_run_json_import_dialog.py | 11 ++-- .../workers/cancellable_base_worker.py | 54 +++++++++++++++++-- .../simulation_run_json_export_worker.py | 53 +++++++++++------- 8 files changed, 129 insertions(+), 67 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 3c4d6356..c786beed 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -198,7 +198,7 @@ def initialize_simulation_runs_tab_widget( save_simulation_runs_to_file_button.clicked.connect(self.handle_sim_run_save_to_file_btn_click) save_simulation_runs_to_file_button.setEnabled(False) save_simulation_runs_to_file_button.setToolTip( - "Save the defined simulation runs to a .json file (only input state and expected output qubit values are exported)" + "Save the defined simulation runs to a .json file (only the input and expected output qubit values of simulation runs in which both input and output qubit values are known are exported)" ) simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index f512e13d..a5eec5b9 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -97,14 +97,9 @@ def _handle_generated_input_state_batch( if self.stop_processing_recv_batches: return - if not AllInputStatesGeneratorWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunModel): - show_optionally_cancellable_notification( - message_box_type=MessageBoxType.INFO, - message_box_parent=self, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be a list of SimulationRunModels but was actually {type(batch_data)}. Skipping batch!", - is_cancellable=False, - ) + if not AllInputStatesGeneratorWorker.is_batch_data_list_of_expected_type( + batch_data, SimulationRunModel, parent_widget_for_error_notification=self + ): if self.worker is not None: self.worker.ack_batch_processed() return diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 064a2952..0b6dc747 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -201,14 +201,9 @@ def _handle_simulation_run_execution_batch_done( if self.stop_processing_recv_batches: return - if not SimulationRunWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunResult): - show_optionally_cancellable_notification( - message_box_type=MessageBoxType.INFO, - message_box_parent=self, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be a list of SimulationRunResults but was actually {type(batch_data)}. Skipping batch!", - is_cancellable=False, - ) + if not SimulationRunWorker.is_batch_data_list_of_expected_type( + batch_data, SimulationRunResult, parent_widget_for_error_notification=self + ): if self.worker is not None: self.worker.ack_batch_processed() return diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index a1677ea9..19281456 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -644,10 +644,9 @@ def _create_in_or_out_state_edit_field( optional_qreg_qubit_values, qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size ) ) - else: - in_or_out_state_edit_field.setEnabled(False) - in_or_out_state_edit_field.setPlaceholderText("-") + in_or_out_state_edit_field.setEnabled(optional_qreg_qubit_values is not None) + in_or_out_state_edit_field.setPlaceholderText(unknown_qreg_contents_placeholder) in_or_out_state_edit_field.setCursorPosition(0) in_or_out_state_edit_field.setAlignment(QtCore.Qt.AlignmentFlag.AlignJustify) in_or_out_state_edit_field.setValidator( diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index 4e7ba440..8c37d532 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final, cast from PyQt6 import QtCore, QtWidgets @@ -19,28 +19,37 @@ from PyQt6 import QtGui from ..simulation_run_model import SimulationRunModel - from ..workers.simulation_run_json_export_worker import SimulationRunJsonExportWorker from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification -from ..workers.simulation_run_json_export_worker import SimulationRunJsonExportWorker +from ..workers.simulation_run_json_export_worker import ExportedBatchData, SimulationRunJsonExportWorker from .base_progress_dialog import BaseProgressDialog +EXPORTED_SIM_RUNS_DATA_LABEL: Final[str] = ( + "In total {n_exported_sim_runs:d} simulation runs where exported with {n_skipped_sim_runs:d} simulation runs being skipped" +) + class SimulationRunJsonExportDialog(BaseProgressDialog[SimulationRunJsonExportWorker]): def __init__(self, parent: QtWidgets.QWidget): super().__init__( parent, dialog_title="Exporting simulation runs...", - optional_progress_bar_text_format="Exported simulation run %v of %m", + optional_progress_bar_text_format="Processed simulation run %v of %m", create_default_layout=False, ) - self.num_exported_simulation_runs: int = 0 + self.num_processed_sim_runs: int = 0 + self.total_num_exported_sim_runs: int = 0 + self.total_num_skipped_sim_runs: int = 0 self.dialog_button_box.accepted.connect(self.accept) self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) self.export_location_info_lbl = QtWidgets.QLabel("") self.export_location_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.exported_sim_runs_data_lbl = QtWidgets.QLabel("") + self.exported_sim_runs_data_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.exported_sim_runs_data_lbl.setStyleSheet("QLabel { color : gray; }") + layout = QtWidgets.QVBoxLayout() layout.addWidget(self.title_lbl) layout.addWidget(self.export_location_info_lbl) @@ -49,6 +58,7 @@ def __init__(self, parent: QtWidgets.QWidget): layout.addStretch() layout.addWidget(self.progress_bar) layout.addWidget(self.total_runtime_info_text_lbl) + layout.addWidget(self.exported_sim_runs_data_lbl) layout.addWidget(self.dialog_button_box) self.setLayout(layout) @@ -108,25 +118,30 @@ def _handle_export_failure(self, err: Exception) -> None: @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] def _handle_batch_exported(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: - if not isinstance(batch_data, int): - # TODO: Better logging of mismatched type/s in list - show_optionally_cancellable_notification( - message_box_type=MessageBoxType.INFO, - message_box_parent=self, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be of type {type(int)} but was actually {type(batch_data)}! This should not happen.", - is_cancellable=False, - ) + if not SimulationRunJsonExportWorker.is_batch_data_of_type( + batch_data, ExportedBatchData, parent_widget_for_error_notification=self + ): if self.worker is not None: self.worker.ack_batch_processed() return - self._update_progress_text_with_batch_info(batch_data, batch_generation_duration_in_seconds) + casted_batch_data: Final[ExportedBatchData] = cast("ExportedBatchData", batch_data) + self.progress_info_text_lbl.setText( + f"Batch completed! Exported {casted_batch_data.exported_sim_runs} and skipping {casted_batch_data.skipped_sim_runs} simulation runs. Runtime [in seconds]: {batch_generation_duration_in_seconds}" + ) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_exported_simulation_runs += batch_data + self.num_processed_sim_runs += casted_batch_data.exported_sim_runs + casted_batch_data.skipped_sim_runs if self.progress_bar is not None: - self.progress_bar.setValue(self.num_exported_simulation_runs) + self.progress_bar.setValue(self.num_processed_sim_runs) + + self.total_num_exported_sim_runs += casted_batch_data.exported_sim_runs + self.total_num_skipped_sim_runs += casted_batch_data.skipped_sim_runs + self.exported_sim_runs_data_lbl.setText( + EXPORTED_SIM_RUNS_DATA_LABEL.format( + n_exported_sim_runs=self.total_num_exported_sim_runs, n_skipped_sim_runs=self.total_num_skipped_sim_runs + ) + ) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_export_completion(self, was_cancellation_requested: bool) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 493dc500..e82cdb20 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -134,14 +134,9 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f if self.stop_processing_recv_batches: return - if not SimulationRunJsonImportWorker.are_list_of_batch_items_of_type(batch_data, SimulationRunModel): - show_optionally_cancellable_notification( - message_box_type=MessageBoxType.INFO, - message_box_parent=self, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be a list of SimulationRunModels but was actually {type(batch_data)}. Skipping batch!", - is_cancellable=False, - ) + if not SimulationRunJsonImportWorker.is_batch_data_list_of_expected_type( + batch_data, SimulationRunModel, parent_widget_for_error_notification=self + ): if self.worker is not None: self.worker.ack_batch_processed() return diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py index 41b14518..fc0eb027 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py @@ -10,10 +10,15 @@ import time from dataclasses import dataclass -from typing import Any, Final, TypeVar +from typing import TYPE_CHECKING, Any, Final, TypeVar from PyQt6 import QtCore +from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification + +if TYPE_CHECKING: + from PyQt6 import QtWidgets + T = TypeVar("T") @@ -78,10 +83,51 @@ def set_cancellation_requested_flag(self, flag_value: bool) -> None: self.cancellation_flag_mutex.unlock() @staticmethod - def are_list_of_batch_items_of_type(batch_data: Any, expected_batch_element_type: type[T]) -> bool: - return isinstance(batch_data, list) and all( - isinstance(batch_item, expected_batch_element_type) for batch_item in batch_data + def is_batch_data_list_of_expected_type( + batch_data: Any, expected_batch_element_type: type[T], parent_widget_for_error_notification: QtWidgets.QWidget + ) -> bool: + if not isinstance(batch_data, list): + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=parent_widget_for_error_notification, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be a list of {expected_batch_element_type} but was actually {type(batch_data)}! This should not happen.", + is_cancellable=False, + ) + return False + + mismatched_elem_type: type | None = next( + filter(lambda elem_type: elem_type != expected_batch_element_type, (type(elem) for elem in batch_data)), + None, + ) + if mismatched_elem_type is None: + # All elements of list match expected element type or list was empty + return True + + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=parent_widget_for_error_notification, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be a list of {expected_batch_element_type} but was actually a list that contained an element of type {mismatched_elem_type}! This should not happen.", + is_cancellable=False, + ) + return False + + @staticmethod + def is_batch_data_of_type( + batch_data: Any, expected_batch_type: type[T], parent_widget_for_error_notification: QtWidgets.QWidget + ) -> bool: + if isinstance(batch_data, expected_batch_type): + return True + + show_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=parent_widget_for_error_notification, + message_box_title="Cannot handle batch data", + message_box_content=f"Expected batch data to be of type {expected_batch_type} but was actually of type {type(batch_data)}! This should not happen.", + is_cancellable=False, ) + return False @staticmethod def _get_timestamp() -> float: diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 301da150..96d46832 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Final from PyQt6 import QtCore @@ -24,6 +25,12 @@ from .cancellable_base_worker import BatchTimestamps +@dataclass(frozen=True) +class ExportedBatchData: + exported_sim_runs: int + skipped_sim_runs: int + + class SimulationRunJsonExportWorker(CancellableBaseWorker): def __init__( self, path_to_json_file: Path, simulation_runs_to_export: Iterable[SimulationRunModel], export_batch_size: int @@ -34,7 +41,6 @@ def __init__( self.simulation_runs_to_export: Iterable[SimulationRunModel] = simulation_runs_to_export self.export_batch_size: Final[int] = export_batch_size - # TODO: Pretty printing @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_export(self) -> None: if self.export_batch_size < 1: @@ -43,8 +49,9 @@ def start_export(self) -> None: n_generated_batches: int = 0 try: batch_idx: int = 0 + n_skipped_sim_runs_in_batch: int = 0 + n_exported_sim_runs_in_batch: int = 0 with self.path_to_json_file.open("w", encoding="ascii") as file: - # file.write("{\n\t\"simulationRuns\": [\n") file.write('{"simulationRuns":[') batch_start_timestamp: float = SimulationRunJsonExportWorker._get_timestamp() batch_timestamps: BatchTimestamps | None = None @@ -52,13 +59,15 @@ def start_export(self) -> None: if self.is_cancellation_requested(): break - # if batch_idx > 0: - # file.write(",\n") - # file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json, indent=2)) - - if batch_idx > 0 or (batch_idx == 0 and n_generated_batches > 0): - file.write(",") - file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json)) + if sim_run.expected_output_state is None: + n_skipped_sim_runs_in_batch += 1 + else: + if n_exported_sim_runs_in_batch > 0 or ( + n_exported_sim_runs_in_batch == 0 and n_generated_batches > 0 + ): + file.write(",") + file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json)) + n_exported_sim_runs_in_batch += 1 batch_idx += 1 if batch_idx == self.export_batch_size: @@ -68,10 +77,14 @@ def start_export(self) -> None: ) ) batch_start_timestamp = batch_timestamps.end - self.batchCompleted.emit(batch_timestamps.duration, self.export_batch_size) + self.batchCompleted.emit( + batch_timestamps.duration, + ExportedBatchData(n_exported_sim_runs_in_batch, n_skipped_sim_runs_in_batch), + ) batch_idx = 0 + n_skipped_sim_runs_in_batch = 0 + n_exported_sim_runs_in_batch = 0 n_generated_batches += 1 - # file.write("\n\t]\n}") # An error during during the serialization of the simulation runs to their .json representation will cause the content of the # exported to .json file to be invalid .json due to the simulation runs JSON array as well as the top level JSON object missing # their closing symbol. @@ -83,7 +96,10 @@ def start_export(self) -> None: batch_start_timestamp ) ) - self.batchCompleted.emit(batch_timestamps.duration, batch_idx) + self.batchCompleted.emit( + batch_timestamps.duration, + ExportedBatchData(batch_idx - n_skipped_sim_runs_in_batch, n_skipped_sim_runs_in_batch), + ) self.finished.emit(self.cancellation_requested) except Exception as error: error_msg: Final[str] = ( @@ -94,9 +110,10 @@ def start_export(self) -> None: @staticmethod def serialize_to_json(obj: Any) -> object: - if isinstance(obj, SimulationRunModel): - if obj.expected_output_state is None: - return {"in": str(obj.input_state)} - return {"in": str(obj.input_state), "out": str(obj.expected_output_state)} - msg = f"Cannot serialize object of {type(obj)}" - raise TypeError(msg) + if not isinstance(obj, SimulationRunModel): + msg = f"Cannot serialize object of {type(obj)}" + raise TypeError(msg) + + if obj.expected_output_state is None: + return {"in": str(obj.input_state)} + return {"in": str(obj.input_state), "out": str(obj.expected_output_state)} From 0835ca677c98e8d1b7a785bac4c12e8c7074e199 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Mon, 26 Jan 2026 21:57:49 +0100 Subject: [PATCH 49/88] Unified checks for required QtWidgets in all dialogs and renamed optionally_cancellable_notification function to make meaning of returned value clearer --- python/mqt/syrec/message_box_utils.py | 20 +- .../quantum_circuit_simulation_dialog.py | 397 +++++++++++------- .../all_input_states_generator_dialog.py | 29 +- .../dialogs/base_progress_dialog.py | 4 +- .../dialogs/simulation_run_dialog.py | 8 +- .../dialogs/simulation_run_editor_dialog.py | 61 +-- .../simulation_run_json_export_dialog.py | 6 +- .../simulation_run_json_import_dialog.py | 8 +- .../simulation_view/simulation_run_model.py | 2 - .../workers/cancellable_base_worker.py | 8 +- python/mqt/syrec/syrec_editor.py | 4 +- python/mqt/syrec/widget_check_utils.py | 49 +++ 12 files changed, 380 insertions(+), 216 deletions(-) create mode 100644 python/mqt/syrec/widget_check_utils.py diff --git a/python/mqt/syrec/message_box_utils.py b/python/mqt/syrec/message_box_utils.py index b040173c..48cd8e51 100644 --- a/python/mqt/syrec/message_box_utils.py +++ b/python/mqt/syrec/message_box_utils.py @@ -23,7 +23,7 @@ class MessageBoxType(Enum): ERROR = 3 -def show_optionally_cancellable_notification( +def show_and_request_ok_in_optionally_cancellable_notification( message_box_type: MessageBoxType, message_box_parent: QtWidgets.QWidget, message_box_title: str, @@ -47,9 +47,7 @@ def show_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_box_cancellation_was_clicked( - message_box_type, is_cancellable, clicked_message_box_button - ) + return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) case MessageBoxType.INFO: if log_contents: log_info_to_console( @@ -64,9 +62,7 @@ def show_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_box_cancellation_was_clicked( - message_box_type, is_cancellable, clicked_message_box_button - ) + return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) case MessageBoxType.WARNING: if log_contents: log_warning_to_console( @@ -81,9 +77,7 @@ def show_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_box_cancellation_was_clicked( - message_box_type, is_cancellable, clicked_message_box_button - ) + return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) case MessageBoxType.ERROR: if log_contents: log_error_to_console( @@ -98,9 +92,7 @@ def show_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_box_cancellation_was_clicked( - message_box_type, is_cancellable, clicked_message_box_button - ) + return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) case _: # Added guard to handle new message box types assert_never(message_box_type) @@ -131,7 +123,7 @@ def _get_default_button_for_message_box_type( return QtWidgets.QMessageBox.StandardButton.Cancel if is_cancellable else QtWidgets.QMessageBox.StandardButton.Ok -def _check_whether_message_box_cancellation_was_clicked( +def _check_whether_message_ok_was_clicked( message_box_type: MessageBoxType, is_cancellable: bool, clicked_message_box_button: QtWidgets.QMessageBox.StandardButton, diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index c786beed..6c97077b 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -10,13 +10,13 @@ import sys from pathlib import Path -from typing import Final +from typing import Final, cast from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec -from .message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from .message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from .simulation_view.dialogs.all_input_states_generator_dialog import AllInputStatesGeneratorDialog from .simulation_view.dialogs.base_progress_dialog import BaseProgressDialog from .simulation_view.dialogs.simulation_run_dialog import SimulationRunDialog @@ -31,6 +31,7 @@ from .simulation_view.styled_item_delegates.simulation_run_overview_styled_item_delegate import ( SimulationRunOverviewStyledItemDelegate, ) +from .widget_check_utils import assert_all_required_widgets_found_or_close_dialog LOADED_FROM_FILE_INPUT_FIELD_NAME: Final[str] = "load_from_file_input_field" IMPORT_FROM_FILE_BUTTON_NAME: Final[str] = "import_from_file_btn" @@ -104,7 +105,7 @@ def __init__( self.setSizeGripEnabled(True) def show_save_changes_reminder(self) -> None: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.INFO, message_box_parent=self, message_box_title="Remember to save your changes", @@ -232,32 +233,54 @@ def initialize_simulation_runs_tab_widget( # END: Create simulation runs execution Qt elements return tab_wrapper_widget - # TODO: After edit of simulation run is finished via click on save button will cause the run simulations buttons to only be executed after selecting/deselecting the element def handle_simulation_run_selection_change( self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection ) -> None: if selected.isEmpty() == deselected.isEmpty(): return - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget], + error_dialog_content="Failed to locate current active tab widget during simulation run selection change", + ): return is_list_item_selected: bool = not selected.isEmpty() and deselected.isEmpty() + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) - add_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + optional_add_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME ) - edit_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + optional_edit_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( QtWidgets.QPushButton, EDIT_SIM_RUN_BTN_NAME ) - delete_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + optional_delete_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( QtWidgets.QPushButton, DELETE_SIM_RUN_BTN_NAME ) - if add_simulation_run_btn is None or edit_simulation_run_btn is None or delete_simulation_run_btn is None: + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[ + optional_add_simulation_run_btn, + optional_edit_simulation_run_btn, + optional_delete_simulation_run_btn, + ], + error_dialog_content="Failed to locate simulation run control buttons during simulation run selection change", + ): return + add_simulation_run_btn: Final[QtWidgets.QWidget] = cast( + "QtWidgets.QPushButton", optional_add_simulation_run_btn + ) + edit_simulation_run_btn: Final[QtWidgets.QWidget] = cast( + "QtWidgets.QPushButton", optional_edit_simulation_run_btn + ) + delete_simulation_run_btn: Final[QtWidgets.QWidget] = cast( + "QtWidgets.QPushButton", optional_delete_simulation_run_btn + ) + add_simulation_run_btn.setEnabled(not is_list_item_selected) edit_simulation_run_btn.setEnabled(is_list_item_selected) delete_simulation_run_btn.setEnabled(is_list_item_selected) @@ -275,32 +298,51 @@ def handle_simulation_run_add_btn_click(self) -> None: ): return - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget], + error_dialog_content="Failed to locate current active tab widget during simulation run add button click", + ): return - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, True - ) + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(curr_active_tab_widget, True) - simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( + optional_simulation_runs_list_view: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME ) - if simulation_runs_list_view is None: + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_simulation_runs_list_view], + error_dialog_content="Failed to locate simulation run list view during simulation run add button click", + ): return + simulation_runs_list_view: Final[QtWidgets.QListView] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view + ) simulation_runs_list_view.scrollToBottom() def handle_simulation_run_edit_btn_click(self) -> None: - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_simulation_runs_list_view: QtWidgets.QListView | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget, optional_simulation_runs_list_view], + error_dialog_content="Failed to locate required QtWidgets during simulation run edit button click", + ): return - simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( - QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME + cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + simulation_runs_list_view: Final[QtWidgets.QWidget] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view ) - if simulation_runs_list_view is None: - return reference_sim_run_model: SimulationRunModel = simulation_runs_list_view.currentIndex().data( SIMULATION_RUN_IO_STATE_QT_ROLE @@ -309,11 +351,13 @@ def handle_simulation_run_edit_btn_click(self) -> None: reference_sim_run_model.expected_output_state is not None and reference_sim_run_model.input_state.size() != reference_sim_run_model.expected_output_state.size() ): - QtWidgets.QMessageBox.critical( - self, - "Initial simulation run model validation error", - f"Expected reference simulation runs input state size (n={reference_sim_run_model.input_state.size()}) to match expected output states size (n={reference_sim_run_model.expected_output_state.size()})", - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Initial simulation run model validation error", + message_box_content=f"Expected reference simulation runs input state size (n={reference_sim_run_model.input_state.size()}) to match expected output states size (n={reference_sim_run_model.expected_output_state.size()})", + is_cancellable=False, + log_contents=False, ) return @@ -343,22 +387,28 @@ def handle_simulation_run_editor_dialog_close(self, result: int) -> None: self.simulation_run_editor_dialog.edited_simulation_run_model, ) except ValueError as err: - pressed_message_box_button: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.critical( - self, - "Simulation run model update error!", - f"Update of simulation run model {self.simulation_run_editor_dialog.simulation_run_model_index.row()} failed due to an error!\nReason: {err}", - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Simulation run model update error!", + message_box_content=f"Update of simulation run model {self.simulation_run_editor_dialog.simulation_run_model_index.row()} failed due to an error!\nReason: {err}", + is_cancellable=False, + log_contents=False, ) - - if pressed_message_box_button == QtWidgets.QMessageBox.StandardButton: - pass finally: self.simulation_run_editor_dialog = None @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_sim_run_save_to_file_btn_click(self) -> None: if self.simulation_run_export_to_file_dialog is not None: - # TODO: Error logging + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Simulation run export dialog initialization error!", + message_box_content="Expected no simulation run export dialog instance to exist.", + is_cancellable=False, + log_contents=True, + ) return filename, _ = QtWidgets.QFileDialog.getOpenFileName( @@ -383,7 +433,14 @@ def handle_sim_run_export_to_file_dialog_close(self, _: int) -> None: def handle_open_and_start_all_input_states_generator_dialog(self, input_state_size: int) -> None: if self.all_input_states_generator_dialog is not None: - # TODO: Error logging? + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Input states generator dialog initialization error!", + message_box_content="Expected no input states generator dialog instance to exist.", + is_cancellable=False, + log_contents=True, + ) return self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog(self) @@ -394,24 +451,38 @@ def handle_open_and_start_all_input_states_generator_dialog(self, input_state_si def handle_input_states_generator_dialog_close(self, result: int) -> None: self.all_input_states_generator_dialog = None - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget], + error_dialog_content="Failed to locate active tab widget in input states generator dialog close handler", + ): return - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted ) def handle_simulation_run_delete_btn_click(self) -> None: - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_simulation_runs_list_view: QtWidgets.QListView | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget, optional_simulation_runs_list_view], + error_dialog_content="Failed to locate required QtWidgets during simulation run delete button click", + ): return - simulation_runs_list_view: QtWidgets.QListView | None = curr_active_tab_widget.findChild( - QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + simulation_runs_list_view: Final[QtWidgets.QWidget] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view ) - if simulation_runs_list_view is None: - return if not self.simulation_runs_model.delete_simulation_run_model(simulation_runs_list_view.currentIndex()): return @@ -420,9 +491,7 @@ def handle_simulation_run_delete_btn_click(self) -> None: # and the subsequent update of the backing model of the QListView selection will switch to the element the index # of the previously selected element thus the simulation run execution controls should not be enabled after an element # is deleted - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, False - ) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(curr_active_tab_widget, False) def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayout: controls_layout = QtWidgets.QHBoxLayout() @@ -494,7 +563,6 @@ def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayou controls_layout.addStretch() return controls_layout - # TODO: Check that number of generate simulation runs does not exceed sys.maxsize def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: int) -> None: if switched_to_tab_index == -1: return @@ -502,61 +570,58 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i if switched_to_tab_index == self.prev_active_simulation_runs_tab_idx: return - prev_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + optional_prev_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( self.prev_active_simulation_runs_tab_idx ) - if prev_active_tab_widget is None: - return - - to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + optional_to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( switched_to_tab_index ) - if to_be_switched_to_tab_widget is None: - self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_prev_active_tab_widget, optional_to_be_switched_to_tab_widget], + error_dialog_content="Failed to locate previous/current active tab widget during simulation run tab change handler", + ): + if optional_to_be_switched_to_tab_widget is None: + self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return - if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: - pressed_message_box_button_in_tab_switch_warning: QtWidgets.QMessageBox.StandardButton = ( - QtWidgets.QMessageBox.warning( - self, - "Existing simulation runs detected!", - "Switching tabs will delete all existing simulation runs. Do you want to continue?", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - ) + prev_active_tab_widget: Final[QtWidgets.QLabel] = cast("QtWidgets.QWidget", optional_prev_active_tab_widget) + to_be_switched_to_tab_widget: Final[QtWidgets.QLabel] = cast( + "QtWidgets.QWidget", optional_to_be_switched_to_tab_widget + ) - if pressed_message_box_button_in_tab_switch_warning == QtWidgets.QMessageBox.StandardButton.Cancel: + if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: + if not show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=self, + message_box_title="Existing simulation runs detected!", + message_box_content="Switching tabs will delete all existing simulation runs. Do you want to continue?", + is_cancellable=True, + log_contents=False, + ): self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return self.simulation_runs_model.delete_all_simulation_run_models() - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - to_be_switched_to_tab_widget, False - ) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(to_be_switched_to_tab_widget, False) if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: n_input_state_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits - pressed_message_box_button_in_all_sim_run_generation_warning: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( - self, - "Generating all possible input state combinations!", - f"Are you sure that you want to generate {n_input_state_combinations} simulation runs, one for each input state combination?", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - - if ( - pressed_message_box_button_in_all_sim_run_generation_warning - == QtWidgets.QMessageBox.StandardButton.Cancel + if not show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=self, + message_box_title="Generating all possible input state combinations!", + message_box_content=f"Are you sure that you want to generate {n_input_state_combinations} simulation runs, one for each input state combination?", + is_cancellable=True, + log_contents=False, ): self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return + self.handle_open_and_start_all_input_states_generator_dialog( self.annotatable_quantum_computation.num_data_qubits ) - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - prev_active_tab_widget, False - ) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) self.prev_active_simulation_runs_tab_idx = switched_to_tab_index def handle_run_all_simulation_runs_button_click(self) -> None: @@ -567,25 +632,13 @@ def handle_run_all_simulation_runs_stop_at_first_failure_button_click(self) -> N def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_mismatch: bool) -> None: if self.simulation_run_dialog is not None: - # TODO: Error logging? - return - - # TODO: Should this validation be performed in the dialog itself? Can this condition even be met? - num_simulation_runs: Final[int] = self.simulation_runs_model.rowCount(QtCore.QModelIndex()) - if num_simulation_runs >= sys.maxsize: - QtWidgets.QMessageBox.critical( - self, - "Number of simulation runs not supported!", - f"The maximum number of simulation runs is limited to {sys.maxsize} while you tried to execute {num_simulation_runs}!", - buttons=QtWidgets.QMessageBox.StandardButton.Ok, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - - curr_tab_widget: QtWidgets.QWidget = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() - ) - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_tab_widget, False + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Simulation run dialog initialization error!", + message_box_content="Expected no simulation run dialog instance to exist.", + is_cancellable=False, + log_contents=True, ) return @@ -596,7 +649,6 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma self.annotatable_quantum_computation, self.simulation_runs_model, stop_at_first_output_state_mismatch ) - # TODO: Toggle state after edits in simulation runs were performed? def handle_simulation_runs_dialog_close(self, _: int) -> None: self.simulation_run_dialog = None @@ -606,54 +658,79 @@ def open_import_file_selector(self) -> None: self, "Select a file to import simulation runs from", str(Path.home()), "Json files (*.json)" ) - active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + if not filename: + return + + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( self.simulation_runs_tab_widget.currentIndex() ) - selected_filename_lbl: QtWidgets.QWidget | None = ( - active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) - if active_tab_widget is not None + optional_selected_filename_lbl: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) + if optional_curr_active_tab_widget is not None else None ) - load_from_file_btn: QtWidgets.QWidget | None = ( - active_tab_widget.findChild(QtWidgets.QPushButton, IMPORT_FROM_FILE_BUTTON_NAME) - if active_tab_widget is not None + optional_load_from_file_btn: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QPushButton, IMPORT_FROM_FILE_BUTTON_NAME) + if optional_curr_active_tab_widget is not None else None ) - if active_tab_widget is None or selected_filename_lbl is None or load_from_file_btn is None: - return - if not filename: + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[ + optional_curr_active_tab_widget, + optional_selected_filename_lbl, + optional_load_from_file_btn, + ], + error_dialog_content="Failed to locate required QtWidgets in open import file handle", + ): return + selected_filename_lbl: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_selected_filename_lbl) + load_from_file_btn: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_load_from_file_btn) + selected_filename_lbl.setText(filename) load_from_file_btn.setEnabled(True) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def open_import_from_file_dialog(self) -> None: if self.simulation_run_import_from_file_dialog is not None: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Simulation run import dialog initialization error!", + message_box_content="Expected no simulation run import dialog instance to exist.", + is_cancellable=False, + log_contents=True, + ) return - active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( self.simulation_runs_tab_widget.currentIndex() ) - - selected_filename_lbl: QtWidgets.QWidget | None = ( - active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) - if active_tab_widget is not None + optional_selected_filename_lbl: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) + if optional_curr_active_tab_widget is not None else None ) - if active_tab_widget is None or selected_filename_lbl is None: + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget, optional_selected_filename_lbl], + error_dialog_content="Failed to locate required QtWidgets on import simulation runs from file click", + ): return + selected_filename_lbl: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_selected_filename_lbl) if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: - pressed_btn_in_confirm_dialog: QtWidgets.QMessageBox.StandardButton = QtWidgets.QMessageBox.warning( - self, - "Existing simulation runs detected", - "Importing from a file will delete any existing simulation runs. Do you want to continue?", - buttons=QtWidgets.QMessageBox.StandardButton.Ok | QtWidgets.QMessageBox.StandardButton.Cancel, - defaultButton=QtWidgets.QMessageBox.StandardButton.Ok, - ) - if pressed_btn_in_confirm_dialog == QtWidgets.QMessageBox.StandardButton.Cancel: + if not show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=self, + message_box_title="Existing simulation runs detected", + message_box_content="Importing from a file will delete any existing simulation runs. Do you want to continue?", + is_cancellable=True, + log_contents=False, + ): return self.simulation_runs_model.delete_all_simulation_run_models() @@ -669,49 +746,69 @@ def open_import_from_file_dialog(self) -> None: @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_import_from_file_dialog_close(self, result: int) -> None: self.simulation_run_import_from_file_dialog = None - curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() - if curr_active_tab_widget is None: - # TODO: Error logging + + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget], + error_dialog_content="Failed to locate active tab widget in import simulation runs from file dialog close handler", + ): return - QuantumCircuitSimulationDialog.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget: Final[QtWidgets.QLabel] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted ) - load_from_file_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( - QtWidgets.QPushButton, IMPORT_FROM_FILE_BUTTON_NAME - ) - add_sim_run_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( + optional_add_sim_run_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME ) - if load_from_file_btn is None or add_sim_run_btn is None: - # TODO: Error logging + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_add_sim_run_btn], + error_dialog_content="Failed to locate required QtWidgets in import simulation runs from file dialog close handler", + ): return + add_sim_run_btn: Final[QtWidgets.QLabel] = cast("QtWidgets.QPushButton", optional_add_sim_run_btn) add_sim_run_btn.setEnabled(result == QtWidgets.QDialog.DialogCode.Accepted) - @staticmethod def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - tab_widget: QtWidgets.QWidget, should_controls_be_enabled: bool + self, tab_widget: QtWidgets.QWidget, should_controls_be_enabled: bool ) -> None: - run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( + optional_run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_NAME ) - run_simulation_runs_stop_at_first_failure_btn: QtWidgets.QPushButton | None = tab_widget.findChild( + optional_run_simulation_runs_stop_at_first_failure_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME ) - save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = tab_widget.findChild( + optional_save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, SAVE_SIM_RUNS_TO_FILE_BTN_NAME ) - if ( - run_simulation_runs_btn is None - or run_simulation_runs_stop_at_first_failure_btn is None - or save_simulation_runs_to_file_btn is None + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[ + optional_run_simulation_runs_btn, + optional_run_simulation_runs_stop_at_first_failure_btn, + optional_save_simulation_runs_to_file_btn, + ], + error_dialog_content="Failed to locate required QtWidgets during change of enabled state of simulation run execution controls", ): return + run_simulation_runs_btn: Final[QtWidgets.QLabel] = cast( + "QtWidgets.QPushButton", optional_run_simulation_runs_btn + ) + run_simulation_runs_stop_at_first_failure_btn: Final[QtWidgets.QLabel] = cast( + "QtWidgets.QPushButton", optional_run_simulation_runs_stop_at_first_failure_btn + ) + save_simulation_runs_to_file_btn: Final[QtWidgets.QLabel] = cast( + "QtWidgets.QPushButton", optional_save_simulation_runs_to_file_btn + ) + run_simulation_runs_btn.setEnabled(should_controls_be_enabled) run_simulation_runs_stop_at_first_failure_btn.setEnabled(should_controls_be_enabled) - # TODO: Button should only be enabled if all simulation runs have their expected output state set? save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index a5eec5b9..35f84d89 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -8,6 +8,7 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Final from PyQt6 import QtCore @@ -18,7 +19,7 @@ from ..simulation_run_model import QtSimulationRunModel from ...logger_utils import log_error_to_console, log_info_to_console -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..simulation_run_model import SimulationRunModel from ..workers.all_input_states_generator_worker import AllInputStatesGeneratorWorker from .base_progress_dialog import BaseProgressDialog @@ -42,13 +43,27 @@ def start_generation( ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model self.title_lbl.setText(f"Generating simulation runs with batch size {batch_size}!") - # TODO: Validation that maximum value can actually be stored in progress bar maximum (should validation be performed in dialog or by caller?) + + # Since the operands of the exponentiation operator are casted to floats, the result (a float) could exceed the maximum storable value in an integer so we need to check this error case since + # otherwise setting the maximum value of the QtWidgets.QProgressBar would raise an exception/no matching overload will be found since said function expects an integer parameter. + n_expected_sim_runs_to_generate: Final[float] = 2**expected_input_state_size + if n_expected_sim_runs_to_generate >= sys.maxsize: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Number of to be generated simulation runs not supported!", + message_box_content=f"The number of to be generated simulation runs (n={n_expected_sim_runs_to_generate}) exceeded the maximum supported value of {sys.maxsize} for the given expected input state size {expected_input_state_size}!", + is_cancellable=False, + ) + self.reject() + return + if self.progress_bar is not None: self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(2**expected_input_state_size) + self.progress_bar.setMaximum(int(n_expected_sim_runs_to_generate)) self.progress_bar.setValue(0) else: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Required widget not found", @@ -150,7 +165,7 @@ def _handle_input_state_generation_cancel_button_click(self) -> bool: if self.worker is None: return True - if show_optionally_cancellable_notification( + if show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Cancellation of generation of input states requested!", @@ -175,7 +190,7 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: try: self.shared_simulation_runs_model.delete_all_simulation_run_models() except Exception: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Internal error!", @@ -183,7 +198,7 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: is_cancellable=False, ) else: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Internal state error!", diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 9aae00df..7d28550b 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -13,7 +13,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from ...logger_utils import log_error_to_console, log_info_to_console -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..workers.cancellable_base_worker import CancellableBaseWorker T = TypeVar("T", bound=CancellableBaseWorker) @@ -209,7 +209,7 @@ def _change_dialog_button_enable_state( if dialog_button is not None: dialog_button.setEnabled(should_button_be_enabled) else: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=btn_not_found_notification_parent, message_box_title="Internal error", diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 0b6dc747..ba1081dc 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -20,7 +20,7 @@ from ..simulation_run_model import QtSimulationRunModel from ...logger_utils import log_error_to_console, log_info_to_console -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..styled_item_delegates.simulation_run_execution_styled_item_delegate import ( SimulationRunExecutionStyledItemDelegate, ) @@ -93,7 +93,7 @@ def start_simulations( expected_input_state_size: Final[int] = self.annotatable_quantum_computation.num_data_qubits if batch_size <= 0 or expected_input_state_size <= 0: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Invalid input parameters detected", @@ -112,7 +112,7 @@ def start_simulations( self.progress_bar.setMaximum(expected_total_num_simulation_runs) self.progress_bar.setValue(0) else: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Required widget not found", @@ -181,7 +181,7 @@ def _handle_simulation_runs_cancel_button_click(self) -> bool: if self.worker is None: return True - if show_optionally_cancellable_notification( + if show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Cancellation of simulation runs requested!", diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 19281456..b67ef047 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -18,7 +18,8 @@ from mqt import syrec from ...logger_utils import log_error_to_console -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification +from ...widget_check_utils import assert_all_required_widgets_found_or_close_dialog from ..simulation_run_model import ( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE, QUANTUM_REGISTER_LAYOUT_QT_ROLE, @@ -327,7 +328,7 @@ def __init__( def reject(self) -> None: # Ask for confirmation before closing dialog - if self.failed_due_to_internal_error or show_optionally_cancellable_notification( + if self.failed_due_to_internal_error or show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Confirm close", @@ -343,7 +344,7 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 return # Ask for confirmation before closing dialog - if show_optionally_cancellable_notification( + if show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Confirm close", @@ -474,7 +475,7 @@ def _handle_input_state_qubit_value_checkbox_state_change( ) if not self.edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Failed to updated qubit value", @@ -545,7 +546,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( associated_qubit, updated_qubit_value ): - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Failed to updated qubit value", @@ -1281,7 +1282,7 @@ def _handle_input_or_output_state_text_change( ) if edited_qreg_layout is None: self.failed_due_to_internal_error = True - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Could not find layout of quantum register!", @@ -1487,26 +1488,38 @@ def _stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: b def _assert_all_required_widgets_found_or_close_dialog( self, required_widgets: Iterable[QtWidgets.QWidget], error_dialog_content: str ) -> bool: - if all(widget is not None for widget in required_widgets): + if assert_all_required_widgets_found_or_close_dialog( + self, + required_widgets, + error_dialog_content, + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ): return True self.failed_due_to_internal_error = True - show_optionally_cancellable_notification( - message_box_type=MessageBoxType.ERROR, - message_box_parent=self, - message_box_title="Not all required Qt widgets found!", - message_box_content=f"{error_dialog_content}\nUnsaved changed will be lost and edit dialog will be closed!", - is_cancellable=False, - log_contents=False, - ) - - stringified_found_widgets_object_names: Final[str] = "Object names of found widgets: " + ( - ",".join([widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets)]) - ) - # We want to log the caller of this function as the origin of the error instead of this function itself. - log_error_to_console( - f"{error_dialog_content}\n{stringified_found_widgets_object_names}", - num_additionally_skipped_stack_frames_starting_from_caller_function=1, - ) self.reject() return False + + # if all(widget is not None for widget in required_widgets): + # return True + + # self.failed_due_to_internal_error = True + # show_and_request_ok_in_optionally_cancellable_notification( + # message_box_type=MessageBoxType.ERROR, + # message_box_parent=self, + # message_box_title="Not all required Qt widgets found!", + # message_box_content=f"{error_dialog_content}\nUnsaved changed will be lost and edit dialog will be closed!", + # is_cancellable=False, + # log_contents=False, + # ) + + # stringified_found_widgets_object_names: Final[str] = "Object names of found widgets: " + ( + # ",".join([widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets)]) + # ) + # # We want to log the caller of this function as the origin of the error instead of this function itself. + # log_error_to_console( + # f"{error_dialog_content}\n{stringified_found_widgets_object_names}", + # num_additionally_skipped_stack_frames_starting_from_caller_function=1, + # ) + # self.reject() + # return False diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index 8c37d532..d3dd7c85 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -20,7 +20,7 @@ from ..simulation_run_model import SimulationRunModel from ...logger_utils import log_info_to_console -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..workers.simulation_run_json_export_worker import ExportedBatchData, SimulationRunJsonExportWorker from .base_progress_dialog import BaseProgressDialog @@ -77,7 +77,7 @@ def start_export( self.progress_bar.setMaximum(num_sim_runs_to_export) self.progress_bar.setValue(0) else: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Required widget not found", @@ -163,7 +163,7 @@ def _handle_export_to_file_cancel_button_click(self) -> bool: if self.worker is None: return True - if show_optionally_cancellable_notification( + if show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Cancellation of export to json file!", diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index e82cdb20..fd461498 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -20,7 +20,7 @@ from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel from ...logger_utils import log_error_to_console, log_info_to_console -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..simulation_run_model import SimulationRunModel from ..workers.simulation_run_json_import_worker import SimulationRunJsonImportWorker from .base_progress_dialog import BaseProgressDialog @@ -183,7 +183,7 @@ def _handle_import_from_file_cancel_button_click(self) -> bool: if self.worker is None: return True - if show_optionally_cancellable_notification( + if show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Cancellation of import from json file!", @@ -205,7 +205,7 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: try: self.shared_simulation_runs_model.delete_all_simulation_run_models() except Exception: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Internal error!", @@ -213,7 +213,7 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: is_cancellable=False, ) else: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Internal state error!", diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 64fd7bf1..82a2c6a0 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -266,7 +266,6 @@ def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: def get_all_simulation_run_models(self) -> Iterable[SimulationRunModel]: yield from self.simulation_run_models - # TODO: Check for duplicates? def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> bool: n_simulation_runs: int = len(self.simulation_run_models) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) @@ -342,7 +341,6 @@ def update_edited_simulation_run_model( ) self.dataChanged.emit(index, index) - # TODO: Check that no duplicate input or expected output_state is added def update_model_using_simulation_run_result( self, index: QtCore.QModelIndex, diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py index fc0eb027..d1facf6d 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py @@ -14,7 +14,7 @@ from PyQt6 import QtCore -from ...message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification if TYPE_CHECKING: from PyQt6 import QtWidgets @@ -87,7 +87,7 @@ def is_batch_data_list_of_expected_type( batch_data: Any, expected_batch_element_type: type[T], parent_widget_for_error_notification: QtWidgets.QWidget ) -> bool: if not isinstance(batch_data, list): - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=parent_widget_for_error_notification, message_box_title="Cannot handle batch data", @@ -104,7 +104,7 @@ def is_batch_data_list_of_expected_type( # All elements of list match expected element type or list was empty return True - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=parent_widget_for_error_notification, message_box_title="Cannot handle batch data", @@ -120,7 +120,7 @@ def is_batch_data_of_type( if isinstance(batch_data, expected_batch_type): return True - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=parent_widget_for_error_notification, message_box_title="Cannot handle batch data", diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index b324ff0d..3fb786f0 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -21,7 +21,7 @@ from mqt import syrec from .logger_utils import configure_default_console_logger -from .message_box_utils import MessageBoxType, show_optionally_cancellable_notification +from .message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification # We are using a relative import here: https://stackoverflow.com/questions/43728431/relative-imports-modulenotfounderror-no-module-named-x from .quantum_circuit_simulation_dialog import QuantumCircuitSimulationDialog @@ -1357,7 +1357,7 @@ def update_circuit_view_and_qubit_information( self.qubits_information_lookup.set_lookup_information(annotatable_quantum_computation) if self.quantum_circuit_sim_runs_dialog is not None: - show_optionally_cancellable_notification( + show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Internal error", diff --git a/python/mqt/syrec/widget_check_utils.py b/python/mqt/syrec/widget_check_utils.py new file mode 100644 index 00000000..96991ff5 --- /dev/null +++ b/python/mqt/syrec/widget_check_utils.py @@ -0,0 +1,49 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +from typing import TYPE_CHECKING, Final + +from .logger_utils import log_error_to_console +from .message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification + +if TYPE_CHECKING: + from collections.abc import Iterable + + from PyQt6 import QtWidgets + + +def assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget: QtWidgets.QWidget, + required_widgets: Iterable[QtWidgets.QWidget], + error_dialog_content: str, + num_additionally_skipped_stack_frames_starting_from_caller_function: int = 0, +) -> bool: + if all(widget is not None for widget in required_widgets): + return True + + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=error_notification_parent_widget, + message_box_title="Not all required Qt widgets found!", + message_box_content=f"{error_dialog_content}\nUnsaved changed will be lost and edit dialog will be closed!", + is_cancellable=False, + log_contents=False, + ) + + stringified_found_widgets_object_names: Final[str] = "Object names of found widgets: " + ( + ",".join([widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets)]) + ) + # We want to log the caller of this function as the origin of the error instead of this function itself. + log_error_to_console( + f"{error_dialog_content}\n{stringified_found_widgets_object_names}", + num_additionally_skipped_stack_frames_starting_from_caller_function=1 + + num_additionally_skipped_stack_frames_starting_from_caller_function, + ) + return False From 0f966ffb783d452b4a900fab51ddf0c8ba9c9283 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Mon, 26 Jan 2026 23:34:36 +0100 Subject: [PATCH 50/88] Added reject() implementation to handle correctly reuse cancellation functionality when ESC key is pressed in derived classes of base_progress_dialog --- .../all_input_states_generator_dialog.py | 5 ++++ .../dialogs/simulation_run_dialog.py | 5 ++++ .../simulation_run_json_export_dialog.py | 5 ++++ .../simulation_run_json_import_dialog.py | 5 ++++ .../simulation_view/simulation_run_model.py | 23 +++++++++++-------- 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 35f84d89..38e17767 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -101,6 +101,11 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 else: event.ignore() + # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + def reject(self) -> None: + if self._handle_input_state_generation_cancel_button_click(): + super().reject() + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] def _handle_input_state_generator_failure(self, err: Exception) -> None: self._handle_non_recoverable_error(err) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index ba1081dc..7994c4f2 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -145,6 +145,11 @@ def start_simulations( self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) + # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + def reject(self) -> None: + if self._handle_simulation_runs_cancel_button_click(): + super().reject() + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing if self._handle_simulation_runs_cancel_button_click(): diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index d3dd7c85..3f1433e5 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -102,6 +102,11 @@ def start_export( self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) + # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + def reject(self) -> None: + if self._handle_export_to_file_cancel_button_click(): + super().reject() + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing if self._handle_export_to_file_cancel_button_click(): diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index fd461498..9fb0b254 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -115,6 +115,11 @@ def start_generation( self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) + # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + def reject(self) -> None: + if self._handle_import_from_file_cancel_button_click(): + super().reject() + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing if self._handle_import_from_file_cancel_button_click(): diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 82a2c6a0..db30f0bf 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -68,15 +68,15 @@ def __init__( else: self.input_state = syrec.n_bit_values_container(input_state.size()) for qubit in range(input_state.size()): - self.input_state.set(qubit, input_state.test(qubit)) + self.input_state.set(qubit, input_state.test(qubit)) # type: ignore[arg-type] if expected_output_state is not None: self.expected_output_state = syrec.n_bit_values_container(expected_output_state.size()) for qubit in range(expected_output_state.size()): - self.expected_output_state.set(qubit, expected_output_state.test(qubit)) + self.expected_output_state.set(qubit, expected_output_state.test(qubit)) # type: ignore[arg-type] if actual_output_state is not None: self.actual_output_state = syrec.n_bit_values_container(actual_output_state.size()) for qubit in range(actual_output_state.size()): - self.actual_output_state.set(qubit, actual_output_state.test(qubit)) + self.actual_output_state.set(qubit, actual_output_state.test(qubit)) # type: ignore[arg-type] def initialize_expected_output_state_as_copy_of_input_state(self) -> None: if self.expected_output_state is not None: @@ -84,7 +84,7 @@ def initialize_expected_output_state_as_copy_of_input_state(self) -> None: self.expected_output_state = syrec.n_bit_values_container(self.input_state.size()) for i in range(self.expected_output_state.size()): - self.expected_output_state.set(i, self.input_state.test(i)) + self.expected_output_state.set(i, self.input_state.test(i)) # type: ignore[arg-type] def reset_result_of_execution(self) -> None: self.actual_output_state = None @@ -109,8 +109,8 @@ def set_result_of_simulation_execution( if self.actual_output_state is None: self.actual_output_state = syrec.n_bit_values_container(self.input_state.size()) - for i in range(self.input_state.size()): - self.actual_output_state.set(i, actual_output_state.test(i)) + for i in range(self.actual_output_state.size()): + self.actual_output_state.set(i, actual_output_state.test(i)) # type: ignore[arg-type] self.do_expected_and_actual_outputs_match = do_expected_and_actual_output_states_match self.execution_runtime_in_ms = execution_runtime_in_ms @@ -142,15 +142,15 @@ def update_user_editable_data( raise ValueError(msg) for i in range(self.input_state.size()): - self.input_state.set(i, edited_input_state.test(i)) + self.input_state.set(i, edited_input_state.test(i)) # type: ignore[arg-type] if edited_expected_output_state is None: self.expected_output_state = None else: if self.expected_output_state is None: self.expected_output_state = syrec.n_bit_values_container(self.input_state.size()) - for i in range(self.input_state.size()): - self.expected_output_state.set(i, edited_expected_output_state.test(i)) + for i in range(self.expected_output_state.size()): + self.expected_output_state.set(i, edited_expected_output_state.test(i)) # type: ignore[arg-type] @staticmethod def do_output_states_match( @@ -218,8 +218,11 @@ def _record_quantum_register_layouts( ) -> list[QuantumRegisterLayout]: quantum_register_layouts: list[QuantumRegisterLayout] = [] for qreg in annotatable_quantum_computation.qregs.values(): + internal_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( + qreg.start, syrec.qubit_label_type.internal + ) if qreg.size == 0 or QtSimulationRunModel._does_qubit_label_start_with_internal_qubit_label_prefix( - annotatable_quantum_computation.get_qubit_label(qreg.start, syrec.qubit_label_type.internal) + internal_qubit_label if internal_qubit_label is not None else "" ): continue From 6dcbdb9b674a1e35d2cb2dc2033bceeb595c96a3 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 28 Jan 2026 20:53:35 +0100 Subject: [PATCH 51/88] Associated SyReC program is now also serialized into exported JSON data. Main simulation run dialog now also requires confirmation to close. Added ignore for missing imports for ijson library. Simulation run dialog is now only opened when 'sim' button in editor is pressed instead of being opened automatically on successful synthesis --- pyproject.toml | 2 +- .../quantum_circuit_simulation_dialog.py | 28 ++- .../dialogs/simulation_run_editor_dialog.py | 73 +++++-- .../simulation_run_json_export_dialog.py | 5 +- .../simulation_run_json_export_worker.py | 16 +- .../simulation_run_json_import_worker.py | 3 +- python/mqt/syrec/syrec_editor.py | 187 +++--------------- 7 files changed, 129 insertions(+), 185 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de44bfef..d7c48a58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,7 +154,7 @@ explicit_package_bases = true warn_unreachable = true [[tool.mypy.overrides]] -module = ["mqt.syrec.pysyrec.*", "PyQt6.*"] +module = ["mqt.syrec.pysyrec.*", "PyQt6.*", "ijson.*"] ignore_missing_imports = true diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 6c97077b..ea45f303 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -46,13 +46,16 @@ IMPORT_FROM_FILE_NO_FILE_SELECTED_PLACEHOLDER_TEXT: Final[str] = "" -# TODO: Should a confirmation be requested when the dialog is closed and simulation runs exist? class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] def __init__( - self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtWidgets.QWidget + self, + associated_stringified_syrec_program: str, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + parent: QtWidgets.QWidget, ) -> None: super().__init__() self.parent = parent + self.associated_stringified_syrec_program = associated_stringified_syrec_program self.annotatable_quantum_computation = annotatable_quantum_computation self.some_sim_runs_tab_widget_name = "some_sim_runs_tab" self.all_sim_runs_tab_widget_name = "all_sim_runs_tab" @@ -78,7 +81,6 @@ def __init__( self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self.simulation_run_dialog: SimulationRunDialog | None = None - # TODO: Default background of tabwidget is white on windows (https://forum.qt.io/topic/82262/default-background-color-of-qtabwidget-and-qwidget-qgroupbox/4) self.prev_active_simulation_runs_tab_idx: int = 0 self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self.simulation_runs_tab_widget.currentChanged.connect(self.handle_simulation_runs_tab_widget_tab_changed) @@ -233,6 +235,25 @@ def initialize_simulation_runs_tab_widget( # END: Create simulation runs execution Qt elements return tab_wrapper_widget + def show_close_confirmation_dialog_and_return_boolean_user_choice(self) -> bool: + return show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.INFO, + message_box_parent=self, + message_box_title="Confirm dialog close", + message_box_content="Do you want to close the simulation run dialog, any unsaved simulation runs will be lost?", + is_cancellable=True, + log_contents=False, + ) + + # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + def reject(self) -> None: + if self.show_close_confirmation_dialog_and_return_boolean_user_choice(): + super().reject() + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + # Ask for confirmation before closing + self.accept() if self.show_close_confirmation_dialog_and_return_boolean_user_choice() else event.ignore() + def handle_simulation_run_selection_change( self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection ) -> None: @@ -422,6 +443,7 @@ def handle_sim_run_save_to_file_btn_click(self) -> None: self.simulation_run_export_to_file_dialog.finished.connect(self.handle_sim_run_export_to_file_dialog_close) self.simulation_run_export_to_file_dialog.start_export( Path(filename), + self.associated_stringified_syrec_program, self.simulation_runs_model.get_all_simulation_run_models(), self.simulation_runs_model.rowCount(QtCore.QModelIndex()), ) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index b67ef047..560b191d 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -100,6 +100,7 @@ class QRegContentsLabelAndCheckbox: QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME: Final[str] = "output_state_value_toggle" QREG_SEARCH_INPUT_FIELD_NAME: Final[str] = "qreg_name_search_input_field" +QREG_SEARCH_TRIGGER_BUTTON_NAME: Final[str] = "qreg_name_trigger_btn" QREG_VALUES_VALIDATION_ERROR_LABEL_NAME: Final[str] = "qreg_values_validation_err_lbl" STRINGIFIED_QUBIT_VALUE_FORMAT: Final[str] = "(Value: {stringified_qubit_value:s})" @@ -156,7 +157,6 @@ def __init__( "Simulation run #" + str(simulation_run_model_index.row()) ) - # TODO: How to render n-dimensional variables self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 self.edit_of_qubit_values_enabled: bool = False @@ -428,7 +428,6 @@ def _handle_quantum_register_name_search(self) -> None: qreg_edit_qubit_values_toggle_button = cast( "QtWidgets.QPushButton", optional_qreg_edit_qubit_values_toggle_button ) - should_control_be_visible: bool = qreg_name_search_input_field.text() is None or qreg_name.startswith( qreg_name_search_input_field.text() ) @@ -584,7 +583,7 @@ def _create_qreg_search_controls(self) -> QtWidgets.QLayout: qreg_name_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) qreg_search_input_field.setCompleter(qreg_name_search_completer) - qreg_search_trigger_button = QtWidgets.QPushButton("Search") + qreg_search_trigger_button = QtWidgets.QPushButton("Search", objectName=QREG_SEARCH_TRIGGER_BUTTON_NAME) qreg_search_trigger_button.clicked.connect(self._handle_quantum_register_name_search) qreg_search_controls_layout.addWidget(qreg_search_label) @@ -999,9 +998,14 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ "QtWidgets.QCheckBox", optional_actual_output_state_qubit_checkbox ) - does_qubit_label_match_search_text: bool = self.annotatable_quantum_computation.get_qubit_label( + matched_with_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal - ).startswith(qubit_search_input_field.text()) + ) + does_qubit_label_match_search_text: bool = ( + matched_with_qubit_label.startswith(qubit_search_input_field.text()) + if matched_with_qubit_label is not None + else False + ) qubit_value_label.setVisible(does_qubit_label_match_search_text) input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) expected_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) @@ -1010,6 +1014,33 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ actual_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: + is_any_qubit_values_groupbox_collapsed: bool = False + optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, + QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + optional_qreg_search_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME + ) + + optional_qreg_search_trigger_btn: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, QREG_SEARCH_TRIGGER_BUTTON_NAME + ) + + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_expected_output_state_value_toggle_button, + optional_qreg_search_input_field, + optional_qreg_search_trigger_btn, + ], + f"Failed to find expected output state init/reset button during handling of edit qubit values of output state toggle button of quantum register {associated_qreg_name}!", + ): + return + for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( @@ -1034,13 +1065,6 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( - self.simulation_run_wrapper_box.findChild( - QtWidgets.QPushButton, - QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME.format(qreg_name=associated_qreg_name), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, - ) - ) optional_qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( optional_qubit_values_groupbox.findChild( QtWidgets.QLineEdit, @@ -1057,7 +1081,6 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam optional_qreg_expected_output_state_input_field, optional_qubit_values_groupbox, optional_qubit_values_toggle_button, - optional_expected_output_state_value_toggle_button, optional_qubit_values_groupbox_qubit_search_field, ], f"Failed to find all required QtWidgets for quantum register '{qreg_name}' during handling of edit toggle of output state!", @@ -1070,33 +1093,39 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam ) qubit_values_groupbox = cast("QtWidgets.QCheckBox", optional_qubit_values_groupbox) qubit_values_toggle_button = cast("QtWidgets.QPushButton", optional_qubit_values_toggle_button) - expected_output_state_value_toggle_button = cast( - "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button - ) + qubit_values_groupbox_qubit_search_field = cast( "QtWidgets.QLineEdit", optional_qubit_values_groupbox_qubit_search_field ) - if qreg_name == associated_qreg_name and not qubit_values_groupbox.isVisible(): + is_any_qubit_values_groupbox_collapsed = True qubit_values_groupbox.setVisible(True) qubit_values_toggle_button.setText(TOGGLE_OUTPUT_STATE_QUBIT_VALUES_EDIT) qreg_input_state_input_field.setEnabled(False) qreg_expected_output_state_input_field.setEnabled(False) - expected_output_state_value_toggle_button.setEnabled(False) else: qubit_values_groupbox.setVisible(False) qubit_values_toggle_button.setText(EDIT_OUTPUT_STATE_QUBIT_VALUES) - expected_output_state_value_toggle_button.setEnabled(True) qreg_input_state_input_field.setEnabled(True) qreg_expected_output_state_input_field.setEnabled(qreg_expected_output_state_input_field.text() != "") # noqa: PLC1901 qubit_values_groupbox_qubit_search_field.setText("") self._handle_qubit_search_trigger_button_click(associated_qreg_name) + expected_output_state_value_toggle_button = cast( + "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button + ) + qreg_search_input_field = cast("QtWidgets.QLineEdit", optional_qreg_search_input_field) + qreg_search_trigger_btn = cast("QtWidgets.QPushButtonLineEdit", optional_qreg_search_trigger_btn) + + expected_output_state_value_toggle_button.setEnabled(not is_any_qubit_values_groupbox_collapsed) + qreg_search_input_field.setEnabled(not is_any_qubit_values_groupbox_collapsed) + qreg_search_trigger_btn.setEnabled(not is_any_qubit_values_groupbox_collapsed) + def _handle_init_expected_output_state_button_click(self, associated_qreg_name: str) -> None: optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, - QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME.format(qreg_name=associated_qreg_name), + QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) @@ -1146,9 +1175,11 @@ def _handle_init_expected_output_state_button_click(self, associated_qreg_name: qreg_output_state_input_field.setText("") qreg_output_state_input_field.setEnabled(False) else: + # The value of the should_reset_output_state flag is dependent on the value of the expected output state member variable of the associated simulation run + # and since no reset is requested, i.e. we initialized the expected output state member variable which in turn means it is not None at this point. qreg_output_state_input_field.setText( SimulationRunEditorDialog._stringify_some_qubits_of_n_bit_values_container( - self.edited_simulation_run_model.expected_output_state, + self.edited_simulation_run_model.expected_output_state, # type: ignore[arg-type] qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, ) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index 3f1433e5..e9d1bc7e 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -65,6 +65,7 @@ def __init__(self, parent: QtWidgets.QWidget): def start_export( self, export_location: Path, + associated_stringified_syrec_program: str, sim_runs_to_export: Iterable[SimulationRunModel], num_sim_runs_to_export: int, batch_size: int = 500, @@ -86,7 +87,9 @@ def start_export( ) # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker = SimulationRunJsonExportWorker(export_location, sim_runs_to_export, batch_size) + self.worker = SimulationRunJsonExportWorker( + export_location, associated_stringified_syrec_program, sim_runs_to_export, batch_size + ) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) self.worker.batchCompleted.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 96d46832..7657585b 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +import re from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Final @@ -33,10 +34,15 @@ class ExportedBatchData: class SimulationRunJsonExportWorker(CancellableBaseWorker): def __init__( - self, path_to_json_file: Path, simulation_runs_to_export: Iterable[SimulationRunModel], export_batch_size: int + self, + path_to_json_file: Path, + associated_stringified_syrec_program: str, + simulation_runs_to_export: Iterable[SimulationRunModel], + export_batch_size: int, ): super().__init__(do_batches_require_ack=False) + self.associated_stringified_syrec_program = associated_stringified_syrec_program self.path_to_json_file: Final[Path] = path_to_json_file self.simulation_runs_to_export: Iterable[SimulationRunModel] = simulation_runs_to_export self.export_batch_size: Final[int] = export_batch_size @@ -52,7 +58,9 @@ def start_export(self) -> None: n_skipped_sim_runs_in_batch: int = 0 n_exported_sim_runs_in_batch: int = 0 with self.path_to_json_file.open("w", encoding="ascii") as file: - file.write('{"simulationRuns":[') + file.write( + f'{{"inputCircuit":"{SimulationRunJsonExportWorker.convert_to_single_line_string(self.associated_stringified_syrec_program)}", "simulationRuns":[' + ) batch_start_timestamp: float = SimulationRunJsonExportWorker._get_timestamp() batch_timestamps: BatchTimestamps | None = None for sim_run in self.simulation_runs_to_export: @@ -117,3 +125,7 @@ def serialize_to_json(obj: Any) -> object: if obj.expected_output_state is None: return {"in": str(obj.input_state)} return {"in": str(obj.input_state), "out": str(obj.expected_output_state)} + + @staticmethod + def convert_to_single_line_string(stringified_syrec_program: str) -> str: + return re.sub(r"\s+", " ", stringified_syrec_program) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index e7294cbc..d85e9e08 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -11,9 +11,8 @@ import time from typing import TYPE_CHECKING, Any, Final -# TODO: Correctly configure third-party package for mypy # The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) -import ijson.backends.yajl2_c as ijson # type: ignore[import-not-found] +import ijson.backends.yajl2_c as ijson from PyQt6 import QtCore from mqt import syrec diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 3fb786f0..d9457041 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -276,7 +276,6 @@ def wheelEvent(self, event): # noqa: N802 class SyReCEditor(QtWidgets.QWidget): # type: ignore[misc] - widget: CodeEditor | None = None annotatable_quantum_computation: syrec.annotatable_quantum_computation | None = None build_successful: Callable[[syrec.annotatable_quantum_computation], None] | None = None build_failed: Callable[[str], None] | None = None @@ -291,6 +290,11 @@ def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__() self.parent = parent + self.code_editor_widget: CodeEditor = CodeEditor(self.parent) + self.code_editor_widget.setFont(QtGui.QFont("Monospace", 10, QtGui.QFont.Weight.Normal)) + self.code_editor_widget.highlighter = SyReCHighlighter(self.code_editor_widget.document()) + self.quantum_circuit_sim_runs_dialog: QuantumCircuitSimulationDialog | None = None + self.setup_actions() self.filename: str @@ -344,6 +348,12 @@ def setup_actions(self) -> None: ) self.configurable_parser_and_synthesis_options_update_button.clicked.connect(self.update_configurable_options) + def setText(self, text): # noqa: N802 + self.code_editor_widget.setPlainText(text) + + def getText(self) -> str: # noqa: N802 + return self.code_editor_widget.toPlainText() # type: ignore[no-any-return] + def update_configurable_options(self) -> None: update_configurable_options_modal = ConfigurableOptionsUpdateDialog( self, self.configurable_parser_and_synthesis_options @@ -390,8 +400,8 @@ def open_file(self) -> None: options=QtWidgets.QFileDialog.Option.ReadOnly, ) - if len(selected_file_name) > 0 and self.widget is not None: - self.widget.load(selected_file_name) + if len(selected_file_name) > 0 and self.code_editor_widget is not None: + self.code_editor_widget.load(selected_file_name) if self.before_build is not None: self.before_build() @@ -486,126 +496,30 @@ def stat(self) -> None: msg.setStandardButtons(QtWidgets.QMessageBox.StandardButton.Ok) msg.exec() + def handle_quantum_circuit_sim_runs_dialog_finished(self, _: int) -> None: + self.quantum_circuit_sim_runs_dialog = None + def sim(self) -> None: if self.annotatable_quantum_computation is None: msg = "Set annotatable_quantum_computation before calling sim()" raise RuntimeError(msg) - bit1_mask = 0 - - no_of_bits = self.annotatable_quantum_computation.num_qubits - all_inputs_bit_mask = 2**self.annotatable_quantum_computation.num_data_qubits - 1 - input_list = [all_inputs_bit_mask & x for x in range(2**self.annotatable_quantum_computation.num_data_qubits)] - - n_ancilla_qubits = self.annotatable_quantum_computation.num_ancilla_qubits - n_data_qubits = self.annotatable_quantum_computation.num_data_qubits - ancilla_qubit_values = [False] * n_ancilla_qubits - - # Ancilla qubits are assumed to be defined immediately after the data qubits in the quantum computation thus the first ancillary qubit has the index n_data_qubits + 1 - ancillary_qubit_index = self.annotatable_quantum_computation.num_data_qubits - ancilla_qubit_indices = set() - ancilla_qubit_indices.update([ancillary_qubit_index + i for i in range(n_ancilla_qubits)]) - - if n_ancilla_qubits > 0: - for quantum_operation_index in range(self.annotatable_quantum_computation.num_ops): - quantum_operation = self.annotatable_quantum_computation[quantum_operation_index] - - # We assume that the value of the ancillary qubits is set at the start of the quantum computation with the help of X gates operating only on the ancillary qubits - # The initial state of the ancilla is assumed to be set if any of the following conditions is not met - if ( - quantum_operation.type_ != OpType.x - or len(quantum_operation.controls) > 0 - or len(quantum_operation.targets) != 1 - or quantum_operation.targets[0] not in ancilla_qubit_indices - ): - break - - # There should only be one X gate per ancillary qubit (if its initial state should be 1 instead of the default state of 0) but for now we allow multiple - ancilla_qubit_values[quantum_operation.targets[0] - ancillary_qubit_index] = not ancilla_qubit_values[ - quantum_operation.targets[0] - ancillary_qubit_index - ] - - for i in range(no_of_bits): - if ( - self.annotatable_quantum_computation.is_circuit_qubit_ancillary(i) is True - and ancilla_qubit_values[i - n_data_qubits] - ): - bit1_mask += 2**i - - input_list_len = len(input_list) - - combination_inp = [] - combination_out = [] - - final_inp = [] - final_out = [] - - for i in input_list: - my_inp_bitset = syrec.n_bit_values_container(no_of_bits, i) - my_out_bitset = syrec.n_bit_values_container(no_of_bits) - syrec.simple_simulation(my_out_bitset, self.annotatable_quantum_computation, my_inp_bitset) - - inp_bitset_with_ancillaes_set = syrec.n_bit_values_container(no_of_bits, i + bit1_mask) - combination_inp.append(str(inp_bitset_with_ancillaes_set)) - combination_out.append(str(my_out_bitset)) - - sorted_ind = sorted(range(len(combination_inp)), key=lambda k: int(combination_inp[k], 2)) - - for i in sorted_ind: - final_inp.append(combination_inp[i]) - final_out.append(combination_out[i]) - - # Initiate table - self.table.clear() - self.table.setRowCount(0) - self.table.setColumnCount(0) - self.table.setRowCount(input_list_len + 2) - self.table.setColumnCount(2 * no_of_bits) - - self.table.setSpan(0, 0, 1, no_of_bits) - header1 = QtWidgets.QTableWidgetItem("INPUTS") - header1.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.table.setItem(0, 0, header1) - - self.table.setSpan(0, no_of_bits, 1, no_of_bits) - header2 = QtWidgets.QTableWidgetItem("OUTPUTS") - header2.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.table.setItem(0, no_of_bits, header2) - - self.table.horizontalHeader().setVisible(False) - self.table.verticalHeader().setVisible(False) - - for i in range(no_of_bits): - # One could display the user declared qubit label for the qubits of the local variables of a module but since these variable identifiers could be identical to ones from a different module, we display the internal qubit label instead. - io_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( - i, syrec.qubit_label_type.internal + if self.quantum_circuit_sim_runs_dialog is not None: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Internal error", + message_box_content="Single use simulation run dialog was not correctly reset, expected no instance to exist!", + is_cancellable=False, ) - # Fetching the matching label for a qubit of the annotatable quantum computation should not fail but in case it does, assume a default qubit label . - # We still display the column in any case because otherwise the user would be shown a different number of qubits than the number of qubits that actual exist in the annotatable quantum computation. - if io_qubit_label is None: - io_qubit_label = "" - - input_signal = QtWidgets.QTableWidgetItem(io_qubit_label) - input_signal.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.table.setItem(1, i, QtWidgets.QTableWidgetItem(input_signal)) - - output_signal = QtWidgets.QTableWidgetItem(io_qubit_label) - output_signal.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.table.setItem(1, i + no_of_bits, QtWidgets.QTableWidgetItem(output_signal)) - - for i in range(input_list_len): - for j in range(no_of_bits): - input_cell = QtWidgets.QTableWidgetItem(final_inp[i][j]) - input_cell.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.table.setItem(i + 2, j, QtWidgets.QTableWidgetItem(input_cell)) - - output_cell = QtWidgets.QTableWidgetItem(final_out[i][j]) - output_cell.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.table.setItem(i + 2, j + no_of_bits, QtWidgets.QTableWidgetItem(output_cell)) + return - self.table.horizontalHeader().setStretchLastSection(True) - self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Stretch) - self.show() + self.quantum_circuit_sim_runs_dialog = QuantumCircuitSimulationDialog( + self.getText(), self.annotatable_quantum_computation, parent=self + ) + self.quantum_circuit_sim_runs_dialog.finished.connect(self.handle_quantum_circuit_sim_runs_dialog_finished) + self.quantum_circuit_sim_runs_dialog.open() + self.quantum_circuit_sim_runs_dialog.show_save_changes_reminder() class SyReCHighlighter(QtGui.QSyntaxHighlighter): # type: ignore[misc] @@ -660,21 +574,6 @@ def highlightBlock(self, text): # noqa: N802 match = expression.match(text, offset=index + length) -class QtSyReCEditor(SyReCEditor): - def __init__(self, parent: Any | None = None) -> None: - SyReCEditor.__init__(self, parent) - - self.widget: CodeEditor = CodeEditor(parent) - self.widget.setFont(QtGui.QFont("Monospace", 10, QtGui.QFont.Weight.Normal)) - self.widget.highlighter = SyReCHighlighter(self.widget.document()) - - def setText(self, text): # noqa: N802 - self.widget.setPlainText(text) - - def getText(self): # noqa: N802 - return self.widget.toPlainText() - - class LineNumberArea(QtWidgets.QWidget): # type: ignore[misc] def __init__(self, editor: CodeEditor) -> None: QtWidgets.QWidget.__init__(self, editor) @@ -1279,8 +1178,6 @@ def save_settings(self) -> QtWidgets.QDialog.DialogCode: class MainWindow(QtWidgets.QMainWindow): # type: ignore[misc] def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: QtWidgets.QWidget.__init__(self, parent) - - self.quantum_circuit_sim_runs_dialog: QuantumCircuitSimulationDialog | None = None self.setWindowTitle("SyReC Editor") self.setup_widgets() @@ -1289,7 +1186,7 @@ def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: self.setup_toolbar() def setup_widgets(self) -> None: - self.editor = QtSyReCEditor(self) + self.editor = SyReCEditor(self) self.viewer = CircuitView(parent=self) self.qubits_information_lookup = CircuitQubitsInformationLookup(parent=self) @@ -1301,7 +1198,7 @@ def setup_widgets(self) -> None: variable_info_search_circuit_view_splitter.setStretchFactor(1, 10) splitter = QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical, self) - splitter.addWidget(self.editor.widget) + splitter.addWidget(self.editor.code_editor_widget) splitter.addWidget(variable_info_search_circuit_view_splitter) self.setCentralWidget(splitter) @@ -1356,26 +1253,6 @@ def update_circuit_view_and_qubit_information( self.viewer.load(annotatable_quantum_computation) self.qubits_information_lookup.set_lookup_information(annotatable_quantum_computation) - if self.quantum_circuit_sim_runs_dialog is not None: - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.ERROR, - message_box_parent=self, - message_box_title="Internal error", - message_box_content="Single use simulation run dialog was not correctly reset, expected no instance to exist!", - is_cancellable=False, - ) - return - - self.quantum_circuit_sim_runs_dialog = QuantumCircuitSimulationDialog( - annotatable_quantum_computation, parent=self - ) - self.quantum_circuit_sim_runs_dialog.finished.connect(self.handle_quantum_circuit_sim_runs_dialog_finished) - self.quantum_circuit_sim_runs_dialog.open() - self.quantum_circuit_sim_runs_dialog.show_save_changes_reminder() - - def handle_quantum_circuit_sim_runs_dialog_finished(self, _: int) -> None: - self.quantum_circuit_sim_runs_dialog = None - def clear_error_log_and_circuit_view(self) -> None: self.logWidget.clear() self.viewer.clear() From e2f9fb4d8b5cd1ccbc83ceaf4859db3ed6ba233b Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 28 Jan 2026 21:43:15 +0100 Subject: [PATCH 52/88] Fixed enabled state of simulation run modification buttons when cancelling generation of all input states --- .../quantum_circuit_simulation_dialog.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index ea45f303..a7cead6d 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -254,6 +254,7 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 # Ask for confirmation before closing self.accept() if self.show_close_confirmation_dialog_and_return_boolean_user_choice() else event.ignore() + @QtCore.pyqtSlot(QtCore.QItemSelection, QtCore.QItemSelection) # type: ignore[untyped-decorator] def handle_simulation_run_selection_change( self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection ) -> None: @@ -310,6 +311,7 @@ def handle_simulation_run_selection_change( not is_list_item_selected and (self.simulation_runs_model.rowCount(QtCore.QModelIndex()) < sys.maxsize), ) + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_add_btn_click(self) -> None: if not self.simulation_runs_model.add_simulation_run_model( SimulationRunModel( @@ -345,6 +347,7 @@ def handle_simulation_run_add_btn_click(self) -> None: ) simulation_runs_list_view.scrollToBottom() + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_edit_btn_click(self) -> None: optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() optional_simulation_runs_list_view: QtWidgets.QListView | None = ( @@ -397,6 +400,7 @@ def handle_simulation_run_edit_btn_click(self) -> None: self.simulation_run_editor_dialog.finished.connect(self.handle_simulation_run_editor_dialog_close) self.simulation_run_editor_dialog.show() + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_run_editor_dialog_close(self, result: int) -> None: # This should not happen but is checked nevertheless if self.simulation_run_editor_dialog is None or result == QtWidgets.QDialog.DialogCode.Rejected: @@ -453,6 +457,7 @@ def handle_sim_run_save_to_file_btn_click(self) -> None: def handle_sim_run_export_to_file_dialog_close(self, _: int) -> None: self.simulation_run_export_to_file_dialog = None + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_open_and_start_all_input_states_generator_dialog(self, input_state_size: int) -> None: if self.all_input_states_generator_dialog is not None: show_and_request_ok_in_optionally_cancellable_notification( @@ -470,6 +475,7 @@ def handle_open_and_start_all_input_states_generator_dialog(self, input_state_si self.all_input_states_generator_dialog.show() self.all_input_states_generator_dialog.start_generation(self.simulation_runs_model, input_state_size) + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_input_states_generator_dialog_close(self, result: int) -> None: self.all_input_states_generator_dialog = None @@ -486,6 +492,7 @@ def handle_input_states_generator_dialog_close(self, result: int) -> None: curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted ) + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_delete_btn_click(self) -> None: optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() optional_simulation_runs_list_view: QtWidgets.QListView | None = ( @@ -585,6 +592,7 @@ def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayou controls_layout.addStretch() return controls_layout + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: int) -> None: if switched_to_tab_index == -1: return @@ -638,6 +646,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i log_contents=False, ): self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) + self.set_default_simulation_run_modification_buttons_enabled_state() return self.handle_open_and_start_all_input_states_generator_dialog( @@ -646,9 +655,11 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) self.prev_active_simulation_runs_tab_idx = switched_to_tab_index + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_run_all_simulation_runs_button_click(self) -> None: self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=False) + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_run_all_simulation_runs_stop_at_first_failure_button_click(self) -> None: self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=True) @@ -671,6 +682,7 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma self.annotatable_quantum_computation, self.simulation_runs_model, stop_at_first_output_state_mismatch ) + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_runs_dialog_close(self, _: int) -> None: self.simulation_run_dialog = None @@ -834,3 +846,46 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( run_simulation_runs_btn.setEnabled(should_controls_be_enabled) run_simulation_runs_stop_at_first_failure_btn.setEnabled(should_controls_be_enabled) save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) + + def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + + optional_add_sim_run_btn: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + optional_edit_sim_run_btn: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QPushButton, EDIT_SIM_RUN_BTN_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + optional_delete_sim_run_btn: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QPushButton, DELETE_SIM_RUN_BTN_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[ + optional_curr_active_tab_widget, + optional_add_sim_run_btn, + optional_edit_sim_run_btn, + optional_delete_sim_run_btn, + ], + error_dialog_content="Failed to locate required QtWidgets during switch back to previous tab widget", + ): + return + + add_sim_run_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_add_sim_run_btn) + edit_sim_run_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_edit_sim_run_btn) + delete_sim_run_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_delete_sim_run_btn) + + add_sim_run_btn.setEnabled(True) + edit_sim_run_btn.setEnabled(False) + delete_sim_run_btn.setEnabled(False) From c217ec7478297a08cc6a8f9ea3a1987e7727da89 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 28 Jan 2026 22:53:32 +0100 Subject: [PATCH 53/88] Added optional typing-extensions project dependency --- pyproject.toml | 3 ++- python/mqt/syrec/message_box_utils.py | 7 ++++++- .../dialogs/simulation_run_editor_dialog.py | 7 ++++++- uv.lock | 4 +++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d7c48a58..9217591d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,8 @@ requires-python = ">=3.10" dependencies = [ "mqt.core~=3.4.0", "PyQt6>=6.8", - "ijson~=3.4.0" + "ijson>=3.4.0", + "typing-extensions>=4.15.0; python_version < '3.11'" ] dynamic = ["version"] diff --git a/python/mqt/syrec/message_box_utils.py b/python/mqt/syrec/message_box_utils.py index 48cd8e51..9ae66689 100644 --- a/python/mqt/syrec/message_box_utils.py +++ b/python/mqt/syrec/message_box_utils.py @@ -8,10 +8,15 @@ from __future__ import annotations +import sys from enum import Enum from PyQt6 import QtWidgets -from typing_extensions import assert_never + +if sys.version_info >= (3, 11): + from typing import assert_never +else: + from typing_extensions import assert_never from .logger_utils import log_debug_to_console, log_error_to_console, log_info_to_console, log_warning_to_console diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 560b191d..ad636b4c 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -8,12 +8,17 @@ from __future__ import annotations +import sys from dataclasses import dataclass from enum import Enum from typing import TYPE_CHECKING, Final, cast from PyQt6 import QtCore, QtGui, QtWidgets -from typing_extensions import assert_never + +if sys.version_info >= (3, 11): + from typing import assert_never +else: + from typing_extensions import assert_never from mqt import syrec diff --git a/uv.lock b/uv.lock index 72bb5dc9..5e379fec 100644 --- a/uv.lock +++ b/uv.lock @@ -1169,6 +1169,7 @@ dependencies = [ { name = "ijson" }, { name = "mqt-core" }, { name = "pyqt6" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] [package.dev-dependencies] @@ -1217,9 +1218,10 @@ test = [ [package.metadata] requires-dist = [ - { name = "ijson", specifier = "~=3.4.0" }, + { name = "ijson", specifier = ">=3.4.0" }, { name = "mqt-core", specifier = "~=3.4.0" }, { name = "pyqt6", specifier = ">=6.8" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.15.0" }, ] [package.metadata.requires-dev] From 44bde58fb770c0fb8238bd6b52ea1adeda1a1a23 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Wed, 28 Jan 2026 23:58:37 +0100 Subject: [PATCH 54/88] Export to file now allows for file creation, fixed display of runtimes in different units (seconds and milliseconds) --- .../syrec/quantum_circuit_simulation_dialog.py | 2 +- .../dialogs/base_progress_dialog.py | 4 ++-- .../dialogs/simulation_run_dialog.py | 2 +- ...mulation_run_execution_styled_item_delegate.py | 15 +++++++-------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index a7cead6d..49cb868b 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -436,7 +436,7 @@ def handle_sim_run_save_to_file_btn_click(self) -> None: ) return - filename, _ = QtWidgets.QFileDialog.getOpenFileName( + filename, _ = QtWidgets.QFileDialog.getSaveFileName( self, "Select a file to export simulation runs to", str(Path.home()), "Json files (*.json)" ) diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 7d28550b..843b7e8f 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -22,7 +22,7 @@ "Total runtime [in seconds] (excluding model updates, internal waits): {total_runtime_in_seconds:f}" ) DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( - "Batch of {n_batch_elements:d} completed! Runtime [in seconds]: {batch_duration_in_seconds:f}" + "Batch of {n_batch_elements:d} completed! Runtime [in ms]: {batch_duration_in_ms:f}" ) SMALL_DIALOG_WIDTH: Final[int] = 600 @@ -125,7 +125,7 @@ def get_center_screen_position_for_size(dialog_size: QtCore.Size) -> QtCore.QPoi def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: self.progress_info_text_lbl.setText( DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT.format( - n_batch_elements=n_batch_elements, batch_duration_in_seconds=batch_duration_in_seconds + n_batch_elements=n_batch_elements, batch_duration_in_ms=batch_duration_in_seconds * 1000 ) ) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 7994c4f2..8a976af1 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -228,7 +228,7 @@ def _handle_simulation_run_execution_batch_done( self.shared_simulation_runs_model.index(to_be_updated_simulation_run_number), casted_batch_data[i].actual_output_state, casted_batch_data[i].do_expected_and_actual_outputs_match, - simulation_run_execution_duration_in_seconds / 1000 + simulation_run_execution_duration_in_seconds * 1000 if simulation_run_execution_duration_in_seconds > 0 else 0, ) diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index a3219342..93a67663 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -468,7 +468,9 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, aggregate_result_row_runtime_label_col_rect, RUNTIME_LABEL_TEXT, aggregate_result_row_runtime_value_col_rect, - str(associated_input_output_mapping.execution_runtime_in_ms), + SimulationRunExecutionStyledItemDelegate._truncate_and_stringify_simulation_runtime( + associated_input_output_mapping.execution_runtime_in_ms + ), value_col_text_color=QtCore.Qt.GlobalColor.gray if associated_input_output_mapping.execution_runtime_in_ms is None else QtCore.Qt.GlobalColor.black, @@ -553,13 +555,6 @@ def _draw_label_and_value( text_color=value_col_text_color, ) - # SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( - # painter, label_col_rect, 5, QtCore.Qt.GlobalColor.red, 0 - # ) - # SimulationRunExecutionStyledItemDelegate._paint_rect_edge_points( - # painter, value_col_rect, 5, QtCore.Qt.GlobalColor.blue, 0 - # ) - @staticmethod def _determine_color_for_outputs_match_result_text(do_outputs_match: bool | None) -> QtCore.Qt.GlobalColor: if do_outputs_match is None: @@ -573,3 +568,7 @@ def _stringify_outputs_match_result(do_outputs_match: bool | None) -> str: return OUTPUTS_MATCH_UNKNOWN_TEXT return OUTPUTS_MATCH_TEXT if do_outputs_match else OUTPUTS_MISMATCH_TEXT + + @staticmethod + def _truncate_and_stringify_simulation_runtime(simulation_runtime_in_ms: float | None) -> str: + return f"{simulation_runtime_in_ms:.7}" if simulation_runtime_in_ms is not None else "NONE" From e1184946151c374d251bf0608d2b07ebc44b3d0b Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 29 Jan 2026 15:09:56 +0100 Subject: [PATCH 55/88] Applied CodeRabbit code review suggestions --- pyproject.toml | 2 +- python/mqt/syrec/message_box_utils.py | 21 +- .../quantum_circuit_simulation_dialog.py | 3 +- .../all_input_states_generator_dialog.py | 6 +- .../dialogs/base_progress_dialog.py | 11 +- .../dialogs/simulation_run_dialog.py | 6 +- .../dialogs/simulation_run_editor_dialog.py | 229 ++++++++---------- .../simulation_run_json_export_dialog.py | 8 +- .../simulation_run_json_import_dialog.py | 10 +- .../simulation_view/simulation_run_model.py | 24 +- ...ase_simulation_run_styled_item_delegate.py | 19 +- ...tion_run_execution_styled_item_delegate.py | 26 +- ...ation_run_overview_styled_item_delegate.py | 13 +- .../all_input_states_generator_worker.py | 4 +- .../workers/cancellable_base_worker.py | 2 +- .../simulation_run_json_export_worker.py | 23 +- .../simulation_run_json_import_worker.py | 4 +- .../workers/simulation_run_worker.py | 4 +- python/mqt/syrec/widget_check_utils.py | 12 +- uv.lock | 2 +- 20 files changed, 222 insertions(+), 207 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9217591d..fe18573a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "mqt.core~=3.4.0", "PyQt6>=6.8", "ijson>=3.4.0", - "typing-extensions>=4.15.0; python_version < '3.11'" + "typing-extensions>=4.1.0; python_version < '3.11'" ] dynamic = ["version"] diff --git a/python/mqt/syrec/message_box_utils.py b/python/mqt/syrec/message_box_utils.py index 9ae66689..9cb8d876 100644 --- a/python/mqt/syrec/message_box_utils.py +++ b/python/mqt/syrec/message_box_utils.py @@ -52,7 +52,7 @@ def show_and_request_ok_in_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) + return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case MessageBoxType.INFO: if log_contents: log_info_to_console( @@ -67,7 +67,7 @@ def show_and_request_ok_in_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) + return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case MessageBoxType.WARNING: if log_contents: log_warning_to_console( @@ -82,7 +82,7 @@ def show_and_request_ok_in_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) + return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case MessageBoxType.ERROR: if log_contents: log_error_to_console( @@ -97,7 +97,7 @@ def show_and_request_ok_in_optionally_cancellable_notification( buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), ) - return _check_whether_message_ok_was_clicked(message_box_type, is_cancellable, clicked_message_box_button) + return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case _: # Added guard to handle new message box types assert_never(message_box_type) @@ -130,9 +130,14 @@ def _get_default_button_for_message_box_type( def _check_whether_message_ok_was_clicked( message_box_type: MessageBoxType, - is_cancellable: bool, - clicked_message_box_button: QtWidgets.QMessageBox.StandardButton, + clicked_message_box_button: QtWidgets.QMessageBox.StandardButton | None, ) -> bool: + # Pressing the ESC key in a QMessageBox can return None is no escape button can be determined or was configured (see https://doc.qt.io/qt-6/qmessagebox.html#default-and-escape-keys) if message_box_type == MessageBoxType.QUESTION: - return clicked_message_box_button == QtWidgets.QMessageBox.StandardButton.Yes if is_cancellable else False - return clicked_message_box_button == QtWidgets.QMessageBox.StandardButton.Ok if is_cancellable else False + return ( + clicked_message_box_button is not None + and clicked_message_box_button == QtWidgets.QMessageBox.StandardButton.Yes + ) + return ( + clicked_message_box_button is not None and clicked_message_box_button == QtWidgets.QMessageBox.StandardButton.Ok + ) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 49cb868b..cc071856 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -53,8 +53,7 @@ def __init__( annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtWidgets.QWidget, ) -> None: - super().__init__() - self.parent = parent + super().__init__(parent) self.associated_stringified_syrec_program = associated_stringified_syrec_program self.annotatable_quantum_computation = annotatable_quantum_computation self.some_sim_runs_tab_widget_name = "some_sim_runs_tab" diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 38e17767..43bd44d5 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -97,7 +97,8 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 if not self.error_text_lbl.text(): self.accept() else: - self.reject() + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() else: event.ignore() @@ -156,6 +157,9 @@ def _handle_input_state_generator_finished(self, was_cancellation_requested: boo if self.progress_bar is not None: self.progress_bar.setVisible(False) + # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker + # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation + # was already requested, skip this operation. if not was_cancellation_requested: if self.worker is not None: self._request_worker_cancellation() diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 843b7e8f..ee1a60c3 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -30,6 +30,12 @@ class BaseProgressDialog(QtWidgets.QDialog, Generic[T]): # type: ignore[misc] + """Base class for progress dialogs with worker thread management. + + Note: Instances of this dialog are designed to be used only once. + Create a new instance for each operation rather than reusing the same dialog. + """ + def __init__( self, parent: QtWidgets.QWidget, @@ -46,6 +52,8 @@ def __init__( self.stop_processing_recv_batches: bool = False self.total_runtime_in_seconds: float = 0 + # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) self.setModal(True) self.setSizeGripEnabled(True) self.setWindowTitle(dialog_title) @@ -103,7 +111,8 @@ def __init__( layout.addWidget(self.progress_info_text_lbl) layout.addWidget(self.error_text_lbl) layout.addStretch() - layout.addWidget(self.progress_bar) + if optional_progress_bar_text_format is not None: + layout.addWidget(self.progress_bar) layout.addWidget(self.total_runtime_info_text_lbl) layout.addWidget(self.dialog_button_box) self.setLayout(layout) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 8a976af1..6324703c 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -156,7 +156,8 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 if not self.error_text_lbl.text(): self.accept() else: - self.reject() + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() else: event.ignore() @@ -168,6 +169,9 @@ def _handle_all_simulation_run_executions_done(self, was_cancellation_requested: if self.progress_bar is not None: self.progress_bar.setVisible(False) + # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker + # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation + # was already requested, skip this operation. if not was_cancellation_requested: if self.worker is not None: self._request_worker_cancellation() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index ad636b4c..82434cd4 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -68,7 +68,7 @@ def sizeHint(self) -> QtCore.QSize: # noqa: N802 fm = QtGui.QFontMetrics(self.font()) nominal = fm.boundingRect("W" * self.expected_max_num_characters).width() # use the offered width - preferred = min(nominal, self.width()) + preferred = max(nominal, self.width()) return QtCore.QSize(preferred, sh.height()) def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 @@ -912,111 +912,109 @@ def _create_qubit_controls_groupbox( return input_output_qubits_value_controls_groupbox def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: - for qreg_layout in self.qreg_layouts: - if qreg_layout.qreg_name != associated_quantum_register_name: - continue + associated_qreg_layout: Final[QuantumRegisterLayout | None] = next( + filter(lambda qreg_layout: qreg_layout.qreg_name == associated_quantum_register_name, self.qreg_layouts), + None, + ) + if associated_qreg_layout is None: + return - optional_qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, - QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, - ) + optional_qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) - if not self._assert_all_required_widgets_found_or_close_dialog( - [optional_qreg_qubits_groupbox], - f"Failed to find required qubits groupbox for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", - ): - return + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_qreg_qubits_groupbox], + f"Failed to find required qubits groupbox for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", + ): + return + + qreg_qubits_groupbox = cast("QtWidgets.QGroupBox", optional_qreg_qubits_groupbox) + optional_qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLineEdit, + QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) - qreg_qubits_groupbox = cast("QtWidgets.QGroupBox", optional_qreg_qubits_groupbox) - optional_qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( - QtWidgets.QLineEdit, - QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_qubit_search_input_field], + f"Failed to find required qubit label search input field for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", + ): + return + + qubit_search_input_field = cast("QtWidgets.QLineEdit", optional_qubit_search_input_field) + for qubit in range( + associated_qreg_layout.first_qubit_of_qreg, + associated_qreg_layout.first_qubit_of_qreg + associated_qreg_layout.qreg_size, + ): + optional_qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, + QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - + optional_input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, + INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + optional_expected_output_state_qubit_checkbox_label: QtWidgets.QLabel | None = ( + qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=qubit) + ) + ) + optional_expected_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) + optional_actual_output_state_qubit_checkbox_label: QtWidgets.QLabel | None = qreg_qubits_groupbox.findChild( + QtWidgets.QLabel, ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=qubit) + ) + optional_actual_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( + QtWidgets.QCheckBox, ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) if not self._assert_all_required_widgets_found_or_close_dialog( - [optional_qubit_search_input_field], - f"Failed to find required qubit label search input field for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", + [ + optional_qubit_value_label, + optional_input_state_qubit_checkbox, + optional_expected_output_state_qubit_checkbox_label, + optional_expected_output_state_qubit_checkbox, + optional_actual_output_state_qubit_checkbox_label, + optional_actual_output_state_qubit_checkbox, + ], + f"Failed to find required controls for qubits for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", ): return - qubit_search_input_field = cast("QtWidgets.QLineEdit", optional_qubit_search_input_field) - for qubit in range( - qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size - ): - optional_qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( - QtWidgets.QLabel, - QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, - ) - optional_input_state_qubit_checkbox: QtWidgets.QCheckBox | None = qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, - INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, - ) - optional_expected_output_state_qubit_checkbox_label: QtWidgets.QLabel | None = ( - qreg_qubits_groupbox.findChild( - QtWidgets.QLabel, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=qubit) - ) - ) - optional_expected_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( - qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) - ) - ) - optional_actual_output_state_qubit_checkbox_label: QtWidgets.QLabel | None = ( - qreg_qubits_groupbox.findChild( - QtWidgets.QLabel, ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_LABEL_NAME_FORMAT.format(qubit=qubit) - ) - ) - optional_actual_output_state_qubit_checkbox: QtWidgets.QCheckBox | None = ( - qreg_qubits_groupbox.findChild( - QtWidgets.QCheckBox, ACTUAL_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) - ) - ) - if not self._assert_all_required_widgets_found_or_close_dialog( - [ - optional_qubit_value_label, - optional_input_state_qubit_checkbox, - optional_expected_output_state_qubit_checkbox_label, - optional_expected_output_state_qubit_checkbox, - optional_actual_output_state_qubit_checkbox_label, - optional_actual_output_state_qubit_checkbox, - ], - f"Failed to find required controls for qubits for quantum register '{associated_quantum_register_name}' during handling of qubit label search!", - ): - return - - qubit_value_label = cast("QtWidgets.QLabel", optional_qubit_value_label) - input_state_qubit_checkbox = cast("QtWidgets.QCheckBox", optional_input_state_qubit_checkbox) - expected_output_state_qubit_checkbox_label = cast( - "QtWidgets.QLabel", optional_expected_output_state_qubit_checkbox_label - ) - expected_output_state_qubit_checkbox = cast( - "QtWidgets.QCheckBox", optional_expected_output_state_qubit_checkbox - ) - actual_output_state_qubit_checkbox_label = cast( - "QtWidgets.QLabel", optional_actual_output_state_qubit_checkbox_label - ) - actual_output_state_qubit_checkbox = cast( - "QtWidgets.QCheckBox", optional_actual_output_state_qubit_checkbox - ) + qubit_value_label = cast("QtWidgets.QLabel", optional_qubit_value_label) + input_state_qubit_checkbox = cast("QtWidgets.QCheckBox", optional_input_state_qubit_checkbox) + expected_output_state_qubit_checkbox_label = cast( + "QtWidgets.QLabel", optional_expected_output_state_qubit_checkbox_label + ) + expected_output_state_qubit_checkbox = cast( + "QtWidgets.QCheckBox", optional_expected_output_state_qubit_checkbox + ) + actual_output_state_qubit_checkbox_label = cast( + "QtWidgets.QLabel", optional_actual_output_state_qubit_checkbox_label + ) + actual_output_state_qubit_checkbox = cast( + "QtWidgets.QCheckBox", optional_actual_output_state_qubit_checkbox + ) - matched_with_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( - qubit, syrec.qubit_label_type.internal - ) - does_qubit_label_match_search_text: bool = ( - matched_with_qubit_label.startswith(qubit_search_input_field.text()) - if matched_with_qubit_label is not None - else False - ) - qubit_value_label.setVisible(does_qubit_label_match_search_text) - input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) - expected_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) - expected_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) - actual_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) - actual_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + matched_with_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( + qubit, syrec.qubit_label_type.internal + ) + does_qubit_label_match_search_text: bool = ( + matched_with_qubit_label.startswith(qubit_search_input_field.text()) + if matched_with_qubit_label is not None + else False + ) + qubit_value_label.setVisible(does_qubit_label_match_search_text) + input_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + expected_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) + expected_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + actual_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) + actual_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: is_any_qubit_values_groupbox_collapsed: bool = False @@ -1486,14 +1484,13 @@ def _handle_input_or_output_state_text_change( def _stringify_some_qubits_of_n_bit_values_container( n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int ) -> str: - if ( - first_qubit >= n_bit_values_container.size() - or first_qubit + (n_qubits - 1) >= n_bit_values_container.size() - ): - return "" - return "".join([ - "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, first_qubit + n_qubits) - ]) + last_qubit_of_qreg: Final[int] = first_qubit + (n_qubits - 1) + + if first_qubit <= last_qubit_of_qreg < n_bit_values_container.size(): + return "".join([ + "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, last_qubit_of_qreg + 1) + ]) + return "" @staticmethod def _get_internal_qubit_labels_for_qreg( @@ -1535,27 +1532,3 @@ def _assert_all_required_widgets_found_or_close_dialog( self.failed_due_to_internal_error = True self.reject() return False - - # if all(widget is not None for widget in required_widgets): - # return True - - # self.failed_due_to_internal_error = True - # show_and_request_ok_in_optionally_cancellable_notification( - # message_box_type=MessageBoxType.ERROR, - # message_box_parent=self, - # message_box_title="Not all required Qt widgets found!", - # message_box_content=f"{error_dialog_content}\nUnsaved changed will be lost and edit dialog will be closed!", - # is_cancellable=False, - # log_contents=False, - # ) - - # stringified_found_widgets_object_names: Final[str] = "Object names of found widgets: " + ( - # ",".join([widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets)]) - # ) - # # We want to log the caller of this function as the origin of the error instead of this function itself. - # log_error_to_console( - # f"{error_dialog_content}\n{stringified_found_widgets_object_names}", - # num_additionally_skipped_stack_frames_starting_from_caller_function=1, - # ) - # self.reject() - # return False diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index e9d1bc7e..f3fd4fdc 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -25,7 +25,7 @@ from .base_progress_dialog import BaseProgressDialog EXPORTED_SIM_RUNS_DATA_LABEL: Final[str] = ( - "In total {n_exported_sim_runs:d} simulation runs where exported with {n_skipped_sim_runs:d} simulation runs being skipped" + "In total {n_exported_sim_runs:d} simulation runs were exported with {n_skipped_sim_runs:d} simulation runs being skipped" ) @@ -116,7 +116,8 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 if not self.error_text_lbl.text(): self.accept() else: - self.reject() + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() else: event.ignore() @@ -159,6 +160,9 @@ def _handle_export_completion(self, was_cancellation_requested: bool) -> None: if self.progress_bar is not None: self.progress_bar.setVisible(False) + # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker + # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation + # was already requested, skip this operation. if not was_cancellation_requested: self._request_worker_cancellation() self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 9fb0b254..547f83e2 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -126,7 +126,8 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 if not self.error_text_lbl.text(): self.accept() else: - self.reject() + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() else: event.ignore() @@ -174,8 +175,11 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_import_completion(self, was_cancellation_requested: bool) -> None: self.progress_info_text_lbl.setText("Simulation run import finished!") - log_info_to_console("Simulation run export finished!") + log_info_to_console("Simulation run import finished!") + # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker + # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation + # was already requested, skip this operation. if not was_cancellation_requested: self._request_worker_cancellation() self._shutdown_worker_thread_and_await_completion() @@ -196,7 +200,7 @@ def _handle_import_from_file_cancel_button_click(self) -> bool: is_cancellable=True, log_contents=False, ): - log_info_to_console("Cancellation of simulation run export requested!") + log_info_to_console("Cancellation of simulation run import requested!") self._handle_non_recoverable_error(None) return True return False diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index db30f0bf..7daadc8a 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -262,7 +262,7 @@ def data(self, index: QtCore.QModelIndex, role: int) -> object: return None def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: - if index >= 0 and index < len(self.simulation_run_models): + if 0 <= index < len(self.simulation_run_models): return self.simulation_run_models[index] return None @@ -310,27 +310,6 @@ def reset_prev_simulation_run_execution_results(self) -> None: sim_run_model.reset_result_of_execution() self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(len(self.simulation_run_models) - 1, 0)) - def add_all_possible_simulation_run_models(self) -> bool: - if self.rowCount(QtCore.QModelIndex()) > 0: - return False - - self.beginInsertRows(QtCore.QModelIndex(), 0, 0) - for i in range(2**self.n_data_qubits): - binary_string_of_i = format(i, "b") - input_state = syrec.n_bit_values_container(self.n_data_qubits) - - n_qubits_to_process_in_binary_string: int = min(self.n_data_qubits, len(binary_string_of_i)) - qubit_idx_in_binary_string: int = n_qubits_to_process_in_binary_string - 1 - for qubit in range(n_qubits_to_process_in_binary_string): - qubit_value: bool = binary_string_of_i[qubit_idx_in_binary_string] == "1" - input_state.set(qubit, qubit_value) - qubit_idx_in_binary_string -= 1 - - output_state: syrec.n_bit_values_container | None = None - self.simulation_run_models.append(SimulationRunModel(input_state, output_state)) - self.endInsertRows() - return True - def update_edited_simulation_run_model( self, index: QtCore.QModelIndex, updated_simulation_run_data: SimulationRunModel ) -> None: @@ -356,7 +335,6 @@ def update_model_using_simulation_run_result( log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) - self.simulation_run_models[index.row()] self.simulation_run_models[index.row()].set_result_of_simulation_execution( actual_output_state, do_expected_and_actual_output_states_match, execution_runtime_in_ms ) diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py index d4049ee6..99b38ade 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -72,14 +72,13 @@ def _get_pixel_height_of_text(font_used_to_draw_text: QtGui.QFont, expected_font def _stringify_some_qubits_of_n_bit_values_container( n_bit_values_container: syrec.n_bit_values_container, first_qubit: int, n_qubits: int ) -> str: - last_qubit_of_qreg: int = first_qubit + (n_qubits - 1) + last_qubit_of_qreg: Final[int] = first_qubit + (n_qubits - 1) - if first_qubit >= n_bit_values_container.size() or last_qubit_of_qreg >= n_bit_values_container.size(): - return "" - - return "".join([ - "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, last_qubit_of_qreg + 1) - ]) + if first_qubit <= last_qubit_of_qreg < n_bit_values_container.size(): + return "".join([ + "1" if n_bit_values_container.test(i) else "0" for i in range(first_qubit, last_qubit_of_qreg + 1) + ]) + return DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT @staticmethod def _get_estimated_quantum_register_contents_column_width( @@ -113,7 +112,11 @@ def _get_estimated_quantum_register_contents_column_width( def _get_column_width_scaled_by_ratio_to_total_available_width( required_column_width: int, total_required_width: int, total_available_width: int ) -> int: - return int(float(required_column_width / total_required_width) * total_available_width) + return ( + int(float(required_column_width / total_required_width) * total_available_width) + if total_required_width > 0 + else 0 + ) @staticmethod def _scale_column_widths_based_on_ratio_to_total_available_width( diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index 93a67663..89745a82 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -8,8 +8,14 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore, QtGui, QtWidgets from ..simulation_run_model import ( @@ -61,7 +67,7 @@ def __init__(self, parent=None): super().__init__(parent) @staticmethod - def _get_required_width_for_labels_column(option: QtWidgets.QStyleItemOptionViewItem, font_size: int) -> int: + def _get_required_width_for_labels_column(option: QtWidgets.QStyleOptionViewItem, font_size: int) -> int: return ( QREG_CONTENT_X_SPACING + max( @@ -195,8 +201,8 @@ def _get_required_size_for_content( ) return QtCore.QSize(required_total_card_width, required_total_card_height) - @staticmethod - def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 + @override + def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: required_content_size: Final[QtCore.QSize] = ( SimulationRunExecutionStyledItemDelegate._get_required_size_for_content(option, index) ) @@ -205,6 +211,7 @@ def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) min(required_content_size.width(), available_content_rect.width()), required_content_size.height() ) + @override def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: return @@ -214,7 +221,6 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, return associated_input_output_mapping: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) - SimulationRunExecutionStyledItemDelegate._get_required_size_for_content(option, index) available_rect_for_content: QtCore.QRect = option.rect.adjusted( CARD_CONTENT_PADDING, CARD_CONTENT_PADDING, @@ -426,8 +432,6 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, ) row_idx += 1 - painter.drawLine(base_row_i_label_col_rect.bottomLeft(), base_row_i_value_col_rect.bottomRight()) - y_offset_from_card_header_to_aggregate_result_row: int = ( qreg_contents_height_without_spacing + AGGREGATE_RESULT_TOP_Y_MARGIN ) @@ -440,6 +444,16 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, aggregate_result_row_outputs_match_value_col_rect: QtCore.QRect = base_value_column_rect.adjusted( 0, y_offset_from_card_header_to_aggregate_result_row, 0, y_offset_from_card_header_to_aggregate_result_row ) + + delimiter_line_start_pos: Final[QtCore.QPoint] = QtCore.QPoint( + aggregate_result_row_outputs_match_label_col_rect.topLeft().x(), + aggregate_result_row_outputs_match_label_col_rect.topLeft().y() - AGGREGATE_RESULT_TOP_Y_MARGIN, + ) + delimiter_line_end_pos: Final[QtCore.QPoint] = QtCore.QPoint( + aggregate_result_row_outputs_match_value_col_rect.bottomRight().x(), delimiter_line_start_pos.y() + ) + painter.drawLine(delimiter_line_start_pos, delimiter_line_end_pos) + SimulationRunExecutionStyledItemDelegate._draw_label_and_value( painter, aggregate_result_row_outputs_match_label_col_rect, diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index 3fb15307..a63b139e 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -8,8 +8,14 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore, QtGui, QtWidgets from ..simulation_run_model import ( @@ -61,7 +67,6 @@ def _get_required_qreg_name_and_layout_column_width( if not index.isValid(): return 0 - index.data(LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE) largest_quantum_register_size: int = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) largest_first_qubit_of_quantum_registers: int = index.data(LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE) @@ -165,8 +170,8 @@ def _get_required_size_for_content( ) return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) - @staticmethod - def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: # noqa: N802 + @override + def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize: required_content_size: Final[QtCore.QSize] = ( SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) ) @@ -175,6 +180,7 @@ def sizeHint(option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) min(required_content_size.width(), available_content_rect.width()), required_content_size.height() ) + @override def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> None: if not index.isValid() or option.rect.width() == 0: return @@ -182,7 +188,6 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, associated_sim_run_model: SimulationRunModel = index.data(SIMULATION_RUN_IO_STATE_QT_ROLE) largest_qreg_size: Final[int] = index.data(LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE) - SimulationRunOverviewStyledItemDelegate._get_required_size_for_content(option, index) available_rect_for_content: QtCore.QRect = option.rect.adjusted( CARD_CONTENT_PADDING, CARD_CONTENT_PADDING, diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index 67382d92..cd64346f 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -79,7 +79,7 @@ def start_generation(self) -> None: for i in range(len(batch_data)): batch_data[i] = None batch_idx = 0 - self.finished.emit(self.cancellation_requested) + self.finished.emit(self.is_cancellation_requested()) except Exception as error: self_raised_error_msg = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" log_error_to_console(self_raised_error_msg) @@ -87,7 +87,7 @@ def start_generation(self) -> None: @staticmethod def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: - if expected_input_state_size < 0: + if expected_input_state_size < 1: msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" raise ValueError(msg) diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py index d1facf6d..c65da730 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py @@ -97,7 +97,7 @@ def is_batch_data_list_of_expected_type( return False mismatched_elem_type: type | None = next( - filter(lambda elem_type: elem_type != expected_batch_element_type, (type(elem) for elem in batch_data)), + (type(elem) for elem in batch_data if not isinstance(elem, expected_batch_element_type)), None, ) if mismatched_elem_type is None: diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 7657585b..12cee38d 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -49,15 +49,14 @@ def __init__( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_export(self) -> None: - if self.export_batch_size < 1: - return - - n_generated_batches: int = 0 try: + SimulationRunJsonExportWorker._validate_parameters(self.export_batch_size) + + n_generated_batches: int = 0 batch_idx: int = 0 n_skipped_sim_runs_in_batch: int = 0 n_exported_sim_runs_in_batch: int = 0 - with self.path_to_json_file.open("w", encoding="ascii") as file: + with self.path_to_json_file.open("w") as file: file.write( f'{{"inputCircuit":"{SimulationRunJsonExportWorker.convert_to_single_line_string(self.associated_stringified_syrec_program)}", "simulationRuns":[' ) @@ -108,7 +107,7 @@ def start_export(self) -> None: batch_timestamps.duration, ExportedBatchData(batch_idx - n_skipped_sim_runs_in_batch, n_skipped_sim_runs_in_batch), ) - self.finished.emit(self.cancellation_requested) + self.finished.emit(self.is_cancellation_requested()) except Exception as error: error_msg: Final[str] = ( f"Error in simulaton run export worker (exported .json file could be incomplete)! Reason: {type(error)=}, {error=}" @@ -116,6 +115,12 @@ def start_export(self) -> None: log_error_to_console(error_msg) self.failed.emit(error) + @staticmethod + def _validate_parameters(batch_size: int) -> None: + if batch_size < 1: + msg = f"Batch size must be larger than 0 but was actually {batch_size}" + raise ValueError(msg) + @staticmethod def serialize_to_json(obj: Any) -> object: if not isinstance(obj, SimulationRunModel): @@ -123,9 +128,11 @@ def serialize_to_json(obj: Any) -> object: raise TypeError(msg) if obj.expected_output_state is None: - return {"in": str(obj.input_state)} + msg = "Cannot serialize simulation run with unknown expected output state" + raise TypeError(msg) + return {"in": str(obj.input_state), "out": str(obj.expected_output_state)} @staticmethod def convert_to_single_line_string(stringified_syrec_program: str) -> str: - return re.sub(r"\s+", " ", stringified_syrec_program) + return json.dumps(re.sub(r"\s+", " ", stringified_syrec_program)) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index d85e9e08..2a314164 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -110,7 +110,7 @@ def start_import(self) -> None: ) ) self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) - self.finished.emit(self.cancellation_requested) + self.finished.emit(self.is_cancellation_requested()) except Exception as error: self_raised_error_msg = f"Error in simulaton run import worker! Reason: {type(error)=}, {error=}" log_error_to_console(self_raised_error_msg) @@ -118,7 +118,7 @@ def start_import(self) -> None: @staticmethod def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: - if expected_input_state_size < 0: + if expected_input_state_size < 1: msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" raise ValueError(msg) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index d032aaea..5892977e 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -127,7 +127,7 @@ def start_simulations(self) -> None: for i in range(len(batch_data)): batch_data[i] = None batch_idx = 0 - self.finished.emit(self.cancellation_requested) + self.finished.emit(self.is_cancellation_requested()) except Exception as error: self_raised_error_msg = f"Error in simulation run execution worker (curr. simulation run idx: {curr_sim_run_num}), reason: {type(error)=}, {error=}" log_error_to_console(self_raised_error_msg) @@ -136,7 +136,7 @@ def start_simulations(self) -> None: @staticmethod def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: if expected_input_state_size < 1: - msg = f"Expected state size must be larger than 0 but was actually {expected_input_state_size}" + msg = f"Expected state size must be a positive integer but was actually {expected_input_state_size}" raise ValueError(msg) if batch_size < 1: diff --git a/python/mqt/syrec/widget_check_utils.py b/python/mqt/syrec/widget_check_utils.py index 96991ff5..5bcd0384 100644 --- a/python/mqt/syrec/widget_check_utils.py +++ b/python/mqt/syrec/widget_check_utils.py @@ -25,20 +25,26 @@ def assert_all_required_widgets_found_or_close_dialog( error_dialog_content: str, num_additionally_skipped_stack_frames_starting_from_caller_function: int = 0, ) -> bool: - if all(widget is not None for widget in required_widgets): + # Iterables may be one-shot iterables which will be consumed by the all predicate which would prevent + # the correct logging of the iterable if the predicate is not fulfilled since said iterable would already + # be consumed at that point. + required_widgets_materialized: Final[list[QtWidgets.QWidget]] = list(required_widgets) + if all(widget is not None for widget in required_widgets_materialized): return True show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=error_notification_parent_widget, message_box_title="Not all required Qt widgets found!", - message_box_content=f"{error_dialog_content}\nUnsaved changed will be lost and edit dialog will be closed!", + message_box_content=f"{error_dialog_content}\nUnsaved changes will be lost and edit dialog will be closed!", is_cancellable=False, log_contents=False, ) stringified_found_widgets_object_names: Final[str] = "Object names of found widgets: " + ( - ",".join([widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets)]) + ",".join([ + widget.objectName() for widget in filter(lambda widget: widget is not None, required_widgets_materialized) + ]) ) # We want to log the caller of this function as the origin of the error instead of this function itself. log_error_to_console( diff --git a/uv.lock b/uv.lock index 5e379fec..e53105b7 100644 --- a/uv.lock +++ b/uv.lock @@ -1221,7 +1221,7 @@ requires-dist = [ { name = "ijson", specifier = ">=3.4.0" }, { name = "mqt-core", specifier = "~=3.4.0" }, { name = "pyqt6", specifier = ">=6.8" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.15.0" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.1.0" }, ] [package.metadata.requires-dev] From 385224026089979fda4af50a9941b61123cccc39 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 29 Jan 2026 16:20:30 +0100 Subject: [PATCH 56/88] Exporting stringified SyReC programs is only supported in quantum circuit simulation dialog if the stringified program did not contain line or block comments --- .../quantum_circuit_simulation_dialog.py | 30 +++++++++++++++++-- python/mqt/syrec/syrec_editor.py | 1 + 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index cc071856..5b0f54b5 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -8,6 +8,7 @@ from __future__ import annotations +import re import sys from pathlib import Path from typing import Final, cast @@ -47,6 +48,10 @@ class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] + # We would like to reuse the regex in multiple one-time use dialog instances. Additionally, the regex is used only once in these instances so declaring the + # regex as an instance variable seems wasteful. Whether a compiled or non-compiled regex should be used would have to be benchmarked. + __syrec_program_comment_regex: re.Pattern[str] = re.compile("|".join(map(re.escape, [r"//", r"/*"]))) + def __init__( self, associated_stringified_syrec_program: str, @@ -54,7 +59,13 @@ def __init__( parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) - self.associated_stringified_syrec_program = associated_stringified_syrec_program + self.did_syrec_program_contain_comments: Final[bool] = ( + self.__syrec_program_comment_regex.search(associated_stringified_syrec_program) is not None + ) + self.associated_stringified_syrec_program: str = ( + associated_stringified_syrec_program if not self.did_syrec_program_contain_comments else "" + ) + self.annotatable_quantum_computation = annotatable_quantum_computation self.some_sim_runs_tab_widget_name = "some_sim_runs_tab" self.all_sim_runs_tab_widget_name = "all_sim_runs_tab" @@ -115,6 +126,19 @@ def show_save_changes_reminder(self) -> None: log_contents=False, ) + def show_optional_comments_in_syrec_program_not_supported_notification(self) -> None: + if not self.did_syrec_program_contain_comments: + return + + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=self, + message_box_title="Synthesized SyReC program contained line or block comments!", + message_box_content="SyReC programs containing line or block comments cannot be serialized to JSON since this would generate invalid JSON. Export to JSON functionality is disabled.", + is_cancellable=False, + log_contents=False, + ) + def initialize_simulation_runs_tab_widget( self, shared_simulation_runs_model: QtSimulationRunModel, @@ -844,7 +868,9 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( run_simulation_runs_btn.setEnabled(should_controls_be_enabled) run_simulation_runs_stop_at_first_failure_btn.setEnabled(should_controls_be_enabled) - save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) + save_simulation_runs_to_file_btn.setEnabled( + should_controls_be_enabled and not self.did_syrec_program_contain_comments + ) def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index d9457041..a4bb6544 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -520,6 +520,7 @@ def sim(self) -> None: self.quantum_circuit_sim_runs_dialog.finished.connect(self.handle_quantum_circuit_sim_runs_dialog_finished) self.quantum_circuit_sim_runs_dialog.open() self.quantum_circuit_sim_runs_dialog.show_save_changes_reminder() + self.quantum_circuit_sim_runs_dialog.show_optional_comments_in_syrec_program_not_supported_notification() class SyReCHighlighter(QtGui.QSyntaxHighlighter): # type: ignore[misc] From d4b8bb047aa662b593b6f979e65fa330b6071d06 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 29 Jan 2026 23:07:53 +0100 Subject: [PATCH 57/88] Further CodeRabbit code review suggestions --- .../quantum_circuit_simulation_dialog.py | 19 +++++++------- .../all_input_states_generator_dialog.py | 25 ++++++++++--------- .../dialogs/base_progress_dialog.py | 18 ++++++++++--- .../dialogs/simulation_run_dialog.py | 15 ++++++++++- .../dialogs/simulation_run_editor_dialog.py | 11 ++++++-- .../simulation_run_json_export_dialog.py | 15 ++++++++++- .../simulation_run_json_import_dialog.py | 10 +++++++- .../simulation_view/simulation_run_model.py | 14 +++++------ ...tion_run_execution_styled_item_delegate.py | 12 ++++----- .../simulation_run_json_export_worker.py | 2 +- 10 files changed, 97 insertions(+), 44 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 5b0f54b5..aa846098 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -281,6 +281,8 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 def handle_simulation_run_selection_change( self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection ) -> None: + # We want to only update the simulation run execution controls in case that a selected simulation run was deselected without selecting a new simulation run or vice versa. + # In all other cases leave the enabled state of the simulation run controls the same by simply returning from this function. if selected.isEmpty() == deselected.isEmpty(): return @@ -386,7 +388,6 @@ def handle_simulation_run_edit_btn_click(self) -> None: ): return - cast("QtWidgets.QWidget", optional_curr_active_tab_widget) simulation_runs_list_view: Final[QtWidgets.QWidget] = cast( "QtWidgets.QListView", optional_simulation_runs_list_view ) @@ -638,8 +639,8 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) return - prev_active_tab_widget: Final[QtWidgets.QLabel] = cast("QtWidgets.QWidget", optional_prev_active_tab_widget) - to_be_switched_to_tab_widget: Final[QtWidgets.QLabel] = cast( + prev_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_prev_active_tab_widget) + to_be_switched_to_tab_widget: Final[QtWidgets.QWidget] = cast( "QtWidgets.QWidget", optional_to_be_switched_to_tab_widget ) @@ -744,7 +745,7 @@ def open_import_file_selector(self) -> None: return selected_filename_lbl: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_selected_filename_lbl) - load_from_file_btn: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_load_from_file_btn) + load_from_file_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_load_from_file_btn) selected_filename_lbl.setText(filename) load_from_file_btn.setEnabled(True) @@ -814,7 +815,7 @@ def handle_import_from_file_dialog_close(self, result: int) -> None: ): return - curr_active_tab_widget: Final[QtWidgets.QLabel] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted ) @@ -829,7 +830,7 @@ def handle_import_from_file_dialog_close(self, result: int) -> None: ): return - add_sim_run_btn: Final[QtWidgets.QLabel] = cast("QtWidgets.QPushButton", optional_add_sim_run_btn) + add_sim_run_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_add_sim_run_btn) add_sim_run_btn.setEnabled(result == QtWidgets.QDialog.DialogCode.Accepted) def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( @@ -856,13 +857,13 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( ): return - run_simulation_runs_btn: Final[QtWidgets.QLabel] = cast( + run_simulation_runs_btn: Final[QtWidgets.QPushButton] = cast( "QtWidgets.QPushButton", optional_run_simulation_runs_btn ) - run_simulation_runs_stop_at_first_failure_btn: Final[QtWidgets.QLabel] = cast( + run_simulation_runs_stop_at_first_failure_btn: Final[QtWidgets.QPushButton] = cast( "QtWidgets.QPushButton", optional_run_simulation_runs_stop_at_first_failure_btn ) - save_simulation_runs_to_file_btn: Final[QtWidgets.QLabel] = cast( + save_simulation_runs_to_file_btn: Final[QtWidgets.QPushButton] = cast( "QtWidgets.QPushButton", optional_save_simulation_runs_to_file_btn ) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 43bd44d5..cf5613ae 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -11,6 +11,11 @@ import sys from typing import TYPE_CHECKING, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore if TYPE_CHECKING: @@ -47,18 +52,12 @@ def start_generation( # Since the operands of the exponentiation operator are casted to floats, the result (a float) could exceed the maximum storable value in an integer so we need to check this error case since # otherwise setting the maximum value of the QtWidgets.QProgressBar would raise an exception/no matching overload will be found since said function expects an integer parameter. n_expected_sim_runs_to_generate: Final[float] = 2**expected_input_state_size - if n_expected_sim_runs_to_generate >= sys.maxsize: - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.ERROR, - message_box_parent=self, - message_box_title="Number of to be generated simulation runs not supported!", - message_box_content=f"The number of to be generated simulation runs (n={n_expected_sim_runs_to_generate}) exceeded the maximum supported value of {sys.maxsize} for the given expected input state size {expected_input_state_size}!", - is_cancellable=False, - ) - self.reject() - return - if self.progress_bar is not None: + if not self._can_value_can_be_used_as_progress_bar_max_value(int(n_expected_sim_runs_to_generate)): + # We do not ask for confirmation to close the dialog since we faulted before the input state generation started. + super().reject() + return + self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(int(n_expected_sim_runs_to_generate)) self.progress_bar.setValue(0) @@ -91,7 +90,8 @@ def start_generation( self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_input_state_generation_cancel_button_click(): if not self.error_text_lbl.text(): @@ -103,6 +103,7 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 event.ignore() # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + @override def reject(self) -> None: if self._handle_input_state_generation_cancel_button_click(): super().reject() diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index ee1a60c3..bd47082d 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -93,7 +93,6 @@ def __init__( # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format # self.progress_bar.setFormat("Generated %v out of %m input states") self.progress_bar.setFormat(optional_progress_bar_text_format) - self.progress_bar.setFormat(optional_progress_bar_text_format) self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.total_runtime_info_text_lbl = QtWidgets.QLabel() @@ -118,14 +117,14 @@ def __init__( self.setLayout(layout) @staticmethod - def get_default_big_dialog_size() -> QtCore.Size: + def get_default_big_dialog_size() -> QtCore.QSize: return QtCore.QSize( int(QtGui.QGuiApplication.primaryScreen().availableSize().width() / 1.5), int(QtGui.QGuiApplication.primaryScreen().availableSize().height() / 1.5), ) @staticmethod - def get_center_screen_position_for_size(dialog_size: QtCore.Size) -> QtCore.QPoint: + def get_center_screen_position_for_size(dialog_size: QtCore.QSize) -> QtCore.QPoint: return QtCore.QPoint( (QtGui.QGuiApplication.primaryScreen().availableSize().width() // 2) - (dialog_size.width() // 2), (QtGui.QGuiApplication.primaryScreen().availableSize().height() // 2) - (dialog_size.height() // 2), @@ -233,3 +232,16 @@ def _reset_workers(self) -> None: @staticmethod def _stringify_error(error: Exception) -> str: return f"Error during long running worker operation! Reason: {type(error)=}, {error=}" + + def _can_value_can_be_used_as_progress_bar_max_value(self, value: int) -> bool: + max_allowed_value: Final[int] = (1 << 31) - 1 + if value > max_allowed_value: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Number not supported as maximum value of progress bar!", + message_box_content=f"Attempted to use value {value} as maximum value of progress bar that was larger than the maximum supported value of {max_allowed_value}!", + is_cancellable=False, + ) + return False + return True diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 6324703c..98aa755d 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -8,8 +8,14 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Final, cast +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore, QtGui, QtWidgets if TYPE_CHECKING: @@ -108,6 +114,11 @@ def start_simulations( f"Executing {expected_total_num_simulation_runs} simulation runs with batch size {batch_size}!" ) if self.progress_bar is not None: + if not self._can_value_can_be_used_as_progress_bar_max_value(expected_input_state_size): + # We do not ask for confirmation to close the dialog since we faulted before the simulation run execution started. + super().reject() + return + self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(expected_total_num_simulation_runs) self.progress_bar.setValue(0) @@ -146,11 +157,13 @@ def start_simulations( self._change_dialog_cancel_button_enable_state(True) # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + @override def reject(self) -> None: if self._handle_simulation_runs_cancel_button_click(): super().reject() - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_simulation_runs_cancel_button_click(): if not self.error_text_lbl.text(): diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 82434cd4..5075c9f5 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -15,6 +15,11 @@ from PyQt6 import QtCore, QtGui, QtWidgets +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + if sys.version_info >= (3, 11): from typing import assert_never else: @@ -331,6 +336,7 @@ def __init__( main_layout.addWidget(self.dialog_button_box) self.setLayout(main_layout) + @override def reject(self) -> None: # Ask for confirmation before closing dialog if self.failed_due_to_internal_error or show_and_request_ok_in_optionally_cancellable_notification( @@ -343,7 +349,8 @@ def reject(self) -> None: ): super().reject() - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: if self.failed_due_to_internal_error: self.reject() return @@ -773,7 +780,7 @@ def _create_search_controls_for_qubits_of_qreg( qubit_search_completer = QtWidgets.QCompleter( SimulationRunEditorDialog._get_internal_qubit_labels_for_qreg( - self.annotatable_quantum_computation, first_qreg_qubit, last_qreg_qubit + self.annotatable_quantum_computation, first_qreg_qubit, last_qreg_qubit - first_qreg_qubit ) ) qubit_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index f3fd4fdc..95e779f5 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -8,8 +8,14 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Final, cast +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore, QtWidgets if TYPE_CHECKING: @@ -74,6 +80,11 @@ def start_export( self.export_location_info_lbl.setText(f"Export destination: {export_location!s}") if self.progress_bar is not None: + if not self._can_value_can_be_used_as_progress_bar_max_value(num_sim_runs_to_export): + # We do not ask for confirmation to close the dialog since we faulted before the export started. + super().reject() + return + self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(num_sim_runs_to_export) self.progress_bar.setValue(0) @@ -106,11 +117,13 @@ def start_export( self._change_dialog_cancel_button_enable_state(True) # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + @override def reject(self) -> None: if self._handle_export_to_file_cancel_button_click(): super().reject() - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_export_to_file_cancel_button_click(): if not self.error_text_lbl.text(): diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 547f83e2..91cfb869 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -8,8 +8,14 @@ from __future__ import annotations +import sys from typing import TYPE_CHECKING, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore, QtWidgets if TYPE_CHECKING: @@ -116,11 +122,13 @@ def start_generation( self._change_dialog_cancel_button_enable_state(True) # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + @override def reject(self) -> None: if self._handle_import_from_file_cancel_button_click(): super().reject() - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_import_from_file_cancel_button_click(): if not self.error_text_lbl.text(): diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 7daadc8a..3aabbbfc 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -94,7 +94,7 @@ def reset_result_of_execution(self) -> None: def set_result_of_simulation_execution( self, actual_output_state: syrec.n_bit_values_container, - do_expected_and_actual_output_states_match: bool, + do_expected_and_actual_output_states_match: bool | None, execution_runtime_in_ms: float, ) -> None: if actual_output_state.size() != self.input_state.size(): @@ -287,15 +287,13 @@ def add_simulation_run_models(self, to_be_added_simulation_run_models: list[Simu self.endInsertRows() def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: - self.beginRemoveRows(QtCore.QModelIndex(), index.row(), index.row()) - - if self.is_model_index_valid(index): - self.simulation_run_models.pop(index.row()) - self.endRemoveRows() - return True + if not index.isValid(): + return False + self.beginRemoveRows(QtCore.QModelIndex(), index.row(), index.row()) + self.simulation_run_models.pop(index.row()) self.endRemoveRows() - return False + return True def delete_all_simulation_run_models(self) -> None: self.beginResetModel() diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index 89745a82..482ed303 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -245,20 +245,20 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, available_header_width=required_text_width_for_header_for_largest_sim_run_number, ) - group_box_content_font_size: int = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + group_box_content_line_height: int = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( option.font, CARD_CONTENT_FONT_SIZE ) - group_box_content_line_height_with_spacing: Final[int] = group_box_content_font_size + QREG_CONTENT_Y_SPACING + group_box_content_line_height_with_spacing: Final[int] = group_box_content_line_height + QREG_CONTENT_Y_SPACING available_content_width: Final[int] = available_rect_for_content.width() required_label_column_width: Final[int] = ( SimulationRunExecutionStyledItemDelegate._get_required_width_for_labels_column( - option, group_box_content_font_size + option, CARD_CONTENT_FONT_SIZE ) ) required_values_column_width: Final[int] = ( SimulationRunExecutionStyledItemDelegate._get_required_width_for_qreg_contents_and_outputs_match_result( - option, index, group_box_content_font_size + option, index, CARD_CONTENT_FONT_SIZE ) ) @@ -338,7 +338,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, base_row_i_value_col_rect.topLeft().x(), base_row_i_value_col_rect.topLeft().y(), scaled_width_for_longest_qreg_name, - group_box_content_font_size, + group_box_content_line_height, ) SimulationRunExecutionStyledItemDelegate._draw_elided_text( painter, @@ -353,7 +353,7 @@ def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, qreg_name_rect.topRight().x(), qreg_name_rect.topRight().y(), scaled_width_for_largest_qreg_layout_info_text, - group_box_content_font_size, + group_box_content_line_height, ) SimulationRunExecutionStyledItemDelegate._draw_elided_text( painter, diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 12cee38d..078baa4a 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -135,4 +135,4 @@ def serialize_to_json(obj: Any) -> object: @staticmethod def convert_to_single_line_string(stringified_syrec_program: str) -> str: - return json.dumps(re.sub(r"\s+", " ", stringified_syrec_program)) + return re.sub(r"\s+", " ", stringified_syrec_program) From 75198a141af10c02bed1978b0365679d3036ddc1 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 29 Jan 2026 23:18:43 +0100 Subject: [PATCH 58/88] Added pyqtSlot() decorator to simulation run editor dialog --- .../dialogs/simulation_run_editor_dialog.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 5075c9f5..cd1200c1 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -368,6 +368,7 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: else: event.ignore() + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_quantum_register_name_search(self) -> None: for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name @@ -452,6 +453,7 @@ def _handle_quantum_register_name_search(self) -> None: qreg_actual_output_state_widget.setVisible(should_control_be_visible) qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) + @QtCore.pyqtSlot(QtCore.CheckState, str, int, int, bool) # type: ignore[untyped-decorator] def _handle_input_state_qubit_value_checkbox_state_change( self, new_checkbox_state: QtCore.CheckState, @@ -511,6 +513,7 @@ def _handle_input_state_qubit_value_checkbox_state_change( else: associated_qubit_value_checkbox.setCheckState(new_checkbox_state) + @QtCore.pyqtSlot(QtCore.CheckState, str, int, int, bool) # type: ignore[untyped-decorator] def _handle_expected_output_state_qubit_value_checkbox_state_change( self, new_checkbox_state: QtCore.CheckState, @@ -918,6 +921,7 @@ def _create_qubit_controls_groupbox( input_output_qubits_value_controls_groupbox.setLayout(input_output_qubits_value_controls_groupbox_layout) return input_output_qubits_value_controls_groupbox + @QtCore.pyqtSlot(str) # type: ignore[untyped-decorator] def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: associated_qreg_layout: Final[QuantumRegisterLayout | None] = next( filter(lambda qreg_layout: qreg_layout.qreg_name == associated_quantum_register_name, self.qreg_layouts), @@ -1023,6 +1027,7 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ actual_output_state_qubit_checkbox_label.setVisible(does_qubit_label_match_search_text) actual_output_state_qubit_checkbox.setVisible(does_qubit_label_match_search_text) + @QtCore.pyqtSlot(str) # type: ignore[untyped-decorator] def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: is_any_qubit_values_groupbox_collapsed: bool = False optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( @@ -1131,6 +1136,7 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam qreg_search_input_field.setEnabled(not is_any_qubit_values_groupbox_collapsed) qreg_search_trigger_btn.setEnabled(not is_any_qubit_values_groupbox_collapsed) + @QtCore.pyqtSlot(str) # type: ignore[untyped-decorator] def _handle_init_expected_output_state_button_click(self, associated_qreg_name: str) -> None: optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( @@ -1227,6 +1233,7 @@ def _handle_init_expected_output_state_button_click(self, associated_qreg_name: ) associated_qubit_value_checkbox.setEnabled(not should_reset_output_state) + @QtCore.pyqtSlot(str, int, bool) # type: ignore[untyped-decorator] def _handle_input_or_output_state_text_change( self, associated_qreg_name: str, expected_qreg_size: int, is_editing_input_state: bool ) -> None: From 3eb25c3ea22906f378cbd241263886af717b7138 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Thu, 29 Jan 2026 23:32:45 +0100 Subject: [PATCH 59/88] Fixed decorator signature missmatch during expected output state missmatch in simulation run editor dialog. Added delete on close to simulation run editor and quantum circuit simulation dialog --- .../syrec/quantum_circuit_simulation_dialog.py | 2 ++ .../dialogs/simulation_run_editor_dialog.py | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index aa846098..b840f99a 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -73,6 +73,8 @@ def __init__( self.title = "Define simulation runs for quantum computation" self.setWindowTitle(self.title) + # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) self.setModal(True) dialog_size: Final[QtCore.QSize] = BaseProgressDialog.get_default_big_dialog_size() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index cd1200c1..d6da70e7 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -149,6 +149,8 @@ def __init__( self.edited_simulation_run_model.actual_output_state ) + # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) self.setModal(True) self.setSizeGripEnabled(True) self.setWindowTitle("Edit qubit values of quantum registers for simulation run") @@ -453,10 +455,10 @@ def _handle_quantum_register_name_search(self) -> None: qreg_actual_output_state_widget.setVisible(should_control_be_visible) qreg_edit_qubit_values_toggle_button.setVisible(should_control_be_visible) - @QtCore.pyqtSlot(QtCore.CheckState, str, int, int, bool) # type: ignore[untyped-decorator] + @QtCore.pyqtSlot(QtCore.Qt.CheckState, str, int, int, bool) # type: ignore[untyped-decorator] def _handle_input_state_qubit_value_checkbox_state_change( self, - new_checkbox_state: QtCore.CheckState, + new_checkbox_state: QtCore.Qt.CheckState, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int, @@ -513,10 +515,10 @@ def _handle_input_state_qubit_value_checkbox_state_change( else: associated_qubit_value_checkbox.setCheckState(new_checkbox_state) - @QtCore.pyqtSlot(QtCore.CheckState, str, int, int, bool) # type: ignore[untyped-decorator] + @QtCore.pyqtSlot(QtCore.Qt.CheckState, str, int, int, bool) # type: ignore[untyped-decorator] def _handle_expected_output_state_qubit_value_checkbox_state_change( self, - new_checkbox_state: QtCore.CheckState, + new_checkbox_state: QtCore.Qt.CheckState, associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int, @@ -1136,8 +1138,8 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam qreg_search_input_field.setEnabled(not is_any_qubit_values_groupbox_collapsed) qreg_search_trigger_btn.setEnabled(not is_any_qubit_values_groupbox_collapsed) - @QtCore.pyqtSlot(str) # type: ignore[untyped-decorator] - def _handle_init_expected_output_state_button_click(self, associated_qreg_name: str) -> None: + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_init_expected_output_state_button_click(self) -> None: optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( self.simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, @@ -1148,7 +1150,7 @@ def _handle_init_expected_output_state_button_click(self, associated_qreg_name: if not self._assert_all_required_widgets_found_or_close_dialog( [optional_expected_output_state_value_toggle_button], - f"Failed to find all required QtWidgets for quantum register '{associated_qreg_name}' during handling of initialization/clearing of output state!", + "Failed to find all required QtWidgets for init/clear operation of expected output state!", ): return From 380d3b0aff7990b6569b4c5274e183de660f91df Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 30 Jan 2026 00:02:09 +0100 Subject: [PATCH 60/88] Removed unneeded regex in simulation run editor dialog --- .../simulation_view/dialogs/simulation_run_editor_dialog.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index d6da70e7..9d02d978 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -64,7 +64,7 @@ def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = super().__init__(parent) self.expected_max_num_characters = expected_max_num_characters self.setMaxLength(expected_max_num_characters) - self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) # Make the widget greedy: whenever the layout offers more # than the nominal width, grab it. @@ -204,9 +204,6 @@ def __init__( output_column_label, 1, 2, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) - n_bit_values_container_contents_validator_regular_expr = QtCore.QRegularExpression(R"^[0-1]*$") - QtGui.QRegularExpressionValidator(n_bit_values_container_contents_validator_regular_expr, self) - qreg_controls_grid_row: int = 2 for qreg_layout in self.qreg_layouts: qreg_name: str = qreg_layout.qreg_name From 5a8fc840e4524d2c2f985b13b21ab87c6a63eafe Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 30 Jan 2026 00:34:22 +0100 Subject: [PATCH 61/88] Added missing return type to __init__ constructors of newly added classes of PR --- .../dialogs/all_input_states_generator_dialog.py | 2 +- .../syrec/simulation_view/dialogs/base_progress_dialog.py | 2 +- .../syrec/simulation_view/dialogs/simulation_run_dialog.py | 6 +++--- .../simulation_view/dialogs/simulation_run_editor_dialog.py | 2 +- .../dialogs/simulation_run_json_export_dialog.py | 2 +- .../dialogs/simulation_run_json_import_dialog.py | 2 +- python/mqt/syrec/simulation_view/simulation_run_model.py | 4 ++-- .../simulation_run_execution_styled_item_delegate.py | 2 +- .../simulation_run_overview_styled_item_delegate.py | 2 +- .../workers/all_input_states_generator_worker.py | 2 +- .../simulation_view/workers/cancellable_base_worker.py | 2 +- .../workers/simulation_run_json_export_worker.py | 2 +- .../workers/simulation_run_json_import_worker.py | 2 +- .../syrec/simulation_view/workers/simulation_run_worker.py | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index cf5613ae..509dd1d4 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -31,7 +31,7 @@ class AllInputStatesGeneratorDialog(BaseProgressDialog[AllInputStatesGeneratorWorker]): - def __init__(self, parent: QtWidgets.QWidget): + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__( parent, dialog_title="Generating simulation runs...", diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index bd47082d..5996b15f 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -44,7 +44,7 @@ def __init__( create_default_layout: bool = True, user_provided_dialog_size: QtCore.QSize | None = None, center_dialog: bool = True, - ): + ) -> None: super().__init__(parent) self.worker_thread: QtCore.QThread | None = None diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 98aa755d..012670ec 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -35,7 +35,7 @@ class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): - def __init__(self, parent: QtWidgets.QWidget): + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__( parent, dialog_title="Executing simulation runs...", @@ -58,7 +58,7 @@ def __init__(self, parent: QtWidgets.QWidget): simulation_runs_list_layout = QtWidgets.QHBoxLayout() self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() - self.simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) # type: ignore[no-untyped-call] + self.simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) self.simulation_runs_list_view.setUniformItemSizes(True) self.simulation_runs_list_view.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust) self.simulation_runs_list_view.setAutoFillBackground(False) @@ -114,7 +114,7 @@ def start_simulations( f"Executing {expected_total_num_simulation_runs} simulation runs with batch size {batch_size}!" ) if self.progress_bar is not None: - if not self._can_value_can_be_used_as_progress_bar_max_value(expected_input_state_size): + if not self._can_value_can_be_used_as_progress_bar_max_value(expected_total_num_simulation_runs): # We do not ask for confirmation to close the dialog since we faulted before the simulation run execution started. super().reject() return diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 9d02d978..fdc2f68b 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -128,7 +128,7 @@ def __init__( simulation_run_model_index: QtCore.QModelIndex, copy_of_reference_edit_sim_run_model: SimulationRunModel, parent: QtWidgets.QWidget, - ): + ) -> None: super().__init__(parent) self.failed_due_to_internal_error: bool = False self.simulation_run_model_index: QtCore.QModelIndex = simulation_run_model_index diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index 95e779f5..d5fbf127 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -36,7 +36,7 @@ class SimulationRunJsonExportDialog(BaseProgressDialog[SimulationRunJsonExportWorker]): - def __init__(self, parent: QtWidgets.QWidget): + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__( parent, dialog_title="Exporting simulation runs...", diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 91cfb869..f46cd130 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -33,7 +33,7 @@ class SimulationRunJsonImportDialog(BaseProgressDialog[SimulationRunJsonImportWorker]): - def __init__(self, parent: QtWidgets.QWidget): + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__( parent, dialog_title="Importing simulation runs...", diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 3aabbbfc..9c435d33 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -51,7 +51,7 @@ def __init__( expected_output_state: syrec.n_bit_values_container | None = None, actual_output_state: syrec.n_bit_values_container | None = None, create_new_n_bit_values_container_instances: bool = False, - ): + ) -> None: if expected_output_state is not None and input_state.size() != expected_output_state.size(): msg = f"Expected output state size (n_qubits = {expected_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) @@ -183,7 +183,7 @@ def _update_n_bit_values_container_qubit_value( class QtSimulationRunModel(QtCore.QAbstractListModel): # type: ignore[misc] def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtCore.QObject = None - ): + ) -> None: super().__init__(parent) self.n_data_qubits: int = annotatable_quantum_computation.num_data_qubits self.simulation_run_models: list[SimulationRunModel] = [] diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index 482ed303..9b118a5d 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -63,7 +63,7 @@ # Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html class SimulationRunExecutionStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] - def __init__(self, parent=None): + def __init__(self, parent: QtWidgets.QWidget = None) -> None: super().__init__(parent) @staticmethod diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index a63b139e..a31b64a7 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -57,7 +57,7 @@ # Progress bar delegate C++ example: https://doc.qt.io/qt-6/qtnetwork-torrent-example.html class SimulationRunOverviewStyledItemDelegate(BaseSimulationRunStyledItemDelegate, QtWidgets.QStyledItemDelegate): # type: ignore[misc] - def __init__(self, parent=None): + def __init__(self, parent: QtWidgets.QWidget = None) -> None: super().__init__(parent) @staticmethod diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index cd64346f..28f3b38d 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -24,7 +24,7 @@ class AllInputStatesGeneratorWorker(CancellableBaseWorker): - def __init__(self, expected_input_state_size: int, batch_size: int): + def __init__(self, expected_input_state_size: int, batch_size: int) -> None: super().__init__(do_batches_require_ack=True) self.expected_input_state_size: Final[int] = expected_input_state_size self.batch_size: Final[int] = batch_size diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py index c65da730..fdbb5ba7 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py @@ -43,7 +43,7 @@ class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] finished = QtCore.pyqtSignal(bool, name="finished") failed = QtCore.pyqtSignal(Exception, name="failed") - def __init__(self, do_batches_require_ack: bool): + def __init__(self, do_batches_require_ack: bool) -> None: super().__init__() self.cancellation_requested = False diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 078baa4a..748af08d 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -39,7 +39,7 @@ def __init__( associated_stringified_syrec_program: str, simulation_runs_to_export: Iterable[SimulationRunModel], export_batch_size: int, - ): + ) -> None: super().__init__(do_batches_require_ack=False) self.associated_stringified_syrec_program = associated_stringified_syrec_program diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 2a314164..c576d2a5 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -32,7 +32,7 @@ class SimulationRunJsonImportWorker(CancellableBaseWorker): - def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batch_size: int): + def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batch_size: int) -> None: super().__init__(do_batches_require_ack=True) self.path_to_json_file = path_to_json_file diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 5892977e..8fa33f81 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -40,7 +40,7 @@ def __init__( expected_input_state_size: int, batch_size: int, stop_at_first_output_state_mismatch: bool, - ): + ) -> None: super().__init__(do_batches_require_ack=True) self.batch_size = batch_size self.expected_input_state_size = expected_input_state_size From a1f384efd0e78a153cf9593356ec049cf1fec1b6 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 30 Jan 2026 01:59:48 +0100 Subject: [PATCH 62/88] Further CodeRabbit suggestions that unifies worker and worker thread cancellation across base progress dialog instances. Simulation run execution results are reset when the input state/potentially reset when the expected output state was edited in the simulation run editor --- .../quantum_circuit_simulation_dialog.py | 4 +-- .../all_input_states_generator_dialog.py | 12 +++---- .../dialogs/base_progress_dialog.py | 32 +++++++++++++------ .../dialogs/simulation_run_dialog.py | 12 +++---- .../dialogs/simulation_run_editor_dialog.py | 4 +-- .../simulation_run_json_export_dialog.py | 6 ++-- .../simulation_run_json_import_dialog.py | 10 +++--- .../simulation_view/simulation_run_model.py | 17 ++++++++-- ...ation_run_overview_styled_item_delegate.py | 2 +- .../simulation_run_json_import_worker.py | 2 +- .../workers/simulation_run_worker.py | 2 +- 11 files changed, 58 insertions(+), 45 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index b840f99a..d2b59d62 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -162,7 +162,7 @@ def initialize_simulation_runs_tab_widget( # BEGIN: Create simulation runs list view Qt elements simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView(objectName=SIMULATION_RUNS_LIST_VIEW_NAME) simulation_runs_list_view.setModel(shared_simulation_runs_model) - simulation_runs_list_view.setItemDelegate(SimulationRunOverviewStyledItemDelegate()) # type: ignore[no-untyped-call] + simulation_runs_list_view.setItemDelegate(SimulationRunOverviewStyledItemDelegate()) simulation_runs_list_view.setUniformItemSizes(True) simulation_runs_list_view.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust) simulation_runs_list_view.setAutoFillBackground(False) @@ -797,7 +797,7 @@ def open_import_from_file_dialog(self) -> None: self.simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog(self) self.simulation_run_import_from_file_dialog.finished.connect(self.handle_import_from_file_dialog_close) self.simulation_run_import_from_file_dialog.show() - self.simulation_run_import_from_file_dialog.start_generation( + self.simulation_run_import_from_file_dialog.start_import( Path(selected_filename_lbl.text()), self.simulation_runs_model, expected_input_state_size=self.annotatable_quantum_computation.num_data_qubits, diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 509dd1d4..17635f5b 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -162,10 +162,8 @@ def _handle_input_state_generator_finished(self, was_cancellation_requested: boo # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation # was already requested, skip this operation. if not was_cancellation_requested: - if self.worker is not None: - self._request_worker_cancellation() - if self.worker_thread is not None: - self._shutdown_worker_thread_and_await_completion() + self._request_worker_cancellation() + self._shutdown_worker_thread_and_await_completion() self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) @@ -216,7 +214,5 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: is_cancellable=False, ) - if self.worker is not None: - self._request_worker_cancellation() - if self.worker_thread is not None: - self._shutdown_worker_thread_and_await_completion() + self._request_worker_cancellation() + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 5996b15f..c857f753 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -118,16 +118,26 @@ def __init__( @staticmethod def get_default_big_dialog_size() -> QtCore.QSize: + # None could be returned when running the application in headless mode which should not happen but we cover this case nevertheless + optional_primary_screen: QtGui.QScreen | None = QtGui.QGuiApplication.primaryScreen() + if optional_primary_screen is None: + return QtCore.QSize(0, 0) + return QtCore.QSize( - int(QtGui.QGuiApplication.primaryScreen().availableSize().width() / 1.5), - int(QtGui.QGuiApplication.primaryScreen().availableSize().height() / 1.5), + int(optional_primary_screen.availableSize().width() / 1.5), + int(optional_primary_screen.availableSize().height() / 1.5), ) @staticmethod def get_center_screen_position_for_size(dialog_size: QtCore.QSize) -> QtCore.QPoint: + # None could be returned when running the application in headless mode which should not happen but we cover this case nevertheless + optional_primary_screen: QtGui.QScreen | None = QtGui.QGuiApplication.primaryScreen() + if optional_primary_screen is None: + return QtCore.QSize(0, 0) + return QtCore.QPoint( - (QtGui.QGuiApplication.primaryScreen().availableSize().width() // 2) - (dialog_size.width() // 2), - (QtGui.QGuiApplication.primaryScreen().availableSize().height() // 2) - (dialog_size.height() // 2), + (optional_primary_screen.availableSize().width() // 2) - (dialog_size.width() // 2), + (optional_primary_screen.availableSize().height() // 2) - (dialog_size.height() // 2), ) def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: @@ -165,14 +175,16 @@ def _shutdown_worker_thread_and_await_completion(self) -> None: self.progress_info_text_lbl.setText("Worker thread finished!") def _request_worker_cancellation(self) -> None: + if self.worker is None: + return + self.stop_processing_recv_batches = True self.progress_info_text_lbl.setText("Requesting cancellation of long running worker!") - if self.worker is not None: - log_info_to_console( - "Requesting cancellation of long running worker", - num_additionally_skipped_stack_frames_starting_from_caller_function=1, - ) - self.worker.request_cancellation() + log_info_to_console( + "Requesting cancellation of long running worker", + num_additionally_skipped_stack_frames_starting_from_caller_function=1, + ) + self.worker.request_cancellation() self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) def _change_dialog_cancel_button_enable_state(self, should_button_be_enabled: bool) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 012670ec..352b5d19 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -186,10 +186,8 @@ def _handle_all_simulation_run_executions_done(self, was_cancellation_requested: # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation # was already requested, skip this operation. if not was_cancellation_requested: - if self.worker is not None: - self._request_worker_cancellation() - if self.worker_thread is not None: - self._shutdown_worker_thread_and_await_completion() + self._request_worker_cancellation() + self._shutdown_worker_thread_and_await_completion() self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) @@ -272,7 +270,5 @@ def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) - if self.worker is not None: - self._request_worker_cancellation() - if self.worker_thread is not None: - self._shutdown_worker_thread_and_await_completion() + self._request_worker_cancellation() + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index fdc2f68b..c94be640 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -351,7 +351,7 @@ def reject(self) -> None: @override def closeEvent(self, event: QtGui.QCloseEvent) -> None: if self.failed_due_to_internal_error: - self.reject() + super().reject() return # Ask for confirmation before closing dialog @@ -363,7 +363,7 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: is_cancellable=True, log_contents=False, ): - self.accept() + super().reject() else: event.ignore() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index d5fbf127..cbace68c 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -206,7 +206,5 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) - if self.worker is not None: - self._request_worker_cancellation() - if self.worker_thread is not None: - self._shutdown_worker_thread_and_await_completion() + self._request_worker_cancellation() + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index f46cd130..679297cd 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -23,7 +23,7 @@ from PyQt6 import QtGui - from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel + from ..simulation_run_model import QtSimulationRunModel from ...logger_utils import log_error_to_console, log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification @@ -67,7 +67,7 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: layout.addWidget(self.dialog_button_box) self.setLayout(layout) - def start_generation( + def start_import( self, path_to_json_file: Path, shared_simulation_runs_model: QtSimulationRunModel, @@ -238,7 +238,5 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: is_cancellable=False, ) - if self.worker is not None: - self._request_worker_cancellation() - if self.worker_thread is not None: - self._shutdown_worker_thread_and_await_completion() + self._request_worker_cancellation() + self._shutdown_worker_thread_and_await_completion() diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 9c435d33..83858949 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -86,8 +86,10 @@ def initialize_expected_output_state_as_copy_of_input_state(self) -> None: for i in range(self.expected_output_state.size()): self.expected_output_state.set(i, self.input_state.test(i)) # type: ignore[arg-type] - def reset_result_of_execution(self) -> None: - self.actual_output_state = None + def reset_result_of_execution(self, reset_actual_output_state: bool = True) -> None: + if reset_actual_output_state: + self.actual_output_state = None + self.do_expected_and_actual_outputs_match = None self.execution_runtime_in_ms = None @@ -141,16 +143,27 @@ def update_user_editable_data( log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) + did_input_state_change: bool = False for i in range(self.input_state.size()): + did_input_state_change |= self.input_state.test(i) != edited_input_state.test(i) self.input_state.set(i, edited_input_state.test(i)) # type: ignore[arg-type] + # If the edited input state does not match the current input state of this instance then reset the previously determined simulation run execution results + # since they were based on the current input state + if did_input_state_change: + self.reset_result_of_execution() + if edited_expected_output_state is None: self.expected_output_state = None + # We do not need to reset the actual output state since its value depends only on the input state + self.reset_result_of_execution(reset_actual_output_state=False) else: if self.expected_output_state is None: self.expected_output_state = syrec.n_bit_values_container(self.input_state.size()) for i in range(self.expected_output_state.size()): self.expected_output_state.set(i, edited_expected_output_state.test(i)) # type: ignore[arg-type] + # We do not need to reset the actual output state since its value depends only on the input state + self.reset_result_of_execution(reset_actual_output_state=False) @staticmethod def do_output_states_match( diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index a31b64a7..c8e0d9e6 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -132,7 +132,7 @@ def _get_required_size_for_content( qreg_name_and_layout_info_column_width: Final[int] = ( SimulationRunOverviewStyledItemDelegate._get_required_qreg_name_and_layout_column_width( - option, index, CARD_TITLE_FONT_SIZE + option, index, CARD_CONTENT_FONT_SIZE ) ) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index c576d2a5..c0a33809 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -43,7 +43,7 @@ def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batc def start_import(self) -> None: try: SimulationRunJsonImportWorker._validate_parameters(self.expected_input_state_size, self.batch_size) - self_raised_error_msg: str | None = "" + self_raised_error_msg: str | None = None if self.wait_on_batch_processed_acknowledgement_condition is None: self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 8fa33f81..3f58199d 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -54,7 +54,7 @@ def start_simulations(self) -> None: try: SimulationRunWorker._validate_parameters(self.expected_input_state_size, self.batch_size) - self_raised_error_msg: str | None = "" + self_raised_error_msg: str | None = None if self.wait_on_batch_processed_acknowledgement_condition is None: self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" From 9b40e65bf83a7a4574a328ca51333f3bd657a1ad Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Fri, 30 Jan 2026 02:02:43 +0100 Subject: [PATCH 63/88] Added missing @override decorators to closeEvent and reject functions of quantum circuit simulation dialog --- python/mqt/syrec/quantum_circuit_simulation_dialog.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index d2b59d62..a23f7317 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -13,6 +13,11 @@ from pathlib import Path from typing import Final, cast +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec @@ -271,11 +276,13 @@ def show_close_confirmation_dialog_and_return_boolean_user_choice(self) -> bool: ) # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. + @override def reject(self) -> None: if self.show_close_confirmation_dialog_and_return_boolean_user_choice(): super().reject() - def closeEvent(self, event: QtGui.QCloseEvent) -> None: # noqa: N802 + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing self.accept() if self.show_close_confirmation_dialog_and_return_boolean_user_choice() else event.ignore() From e7880a89aa5cad01dd03e36139f32c942193f650 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sat, 31 Jan 2026 12:36:29 +0100 Subject: [PATCH 64/88] Removed redundant name parameter from signal declarations and smaller CodeRabbit suggestions --- .../simulation_view/dialogs/base_progress_dialog.py | 8 +++++--- .../dialogs/simulation_run_editor_dialog.py | 4 ++-- .../mqt/syrec/simulation_view/simulation_run_model.py | 10 +++++++++- .../simulation_view/workers/cancellable_base_worker.py | 6 +++--- .../workers/simulation_run_json_import_worker.py | 1 + python/mqt/syrec/syrec_editor.py | 4 ++-- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index c857f753..93d17aed 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -133,11 +133,13 @@ def get_center_screen_position_for_size(dialog_size: QtCore.QSize) -> QtCore.QPo # None could be returned when running the application in headless mode which should not happen but we cover this case nevertheless optional_primary_screen: QtGui.QScreen | None = QtGui.QGuiApplication.primaryScreen() if optional_primary_screen is None: - return QtCore.QSize(0, 0) + return QtCore.QPoint(0, 0) return QtCore.QPoint( - (optional_primary_screen.availableSize().width() // 2) - (dialog_size.width() // 2), - (optional_primary_screen.availableSize().height() // 2) - (dialog_size.height() // 2), + (optional_primary_screen.availableSize().width() // 2) + - ((dialog_size.width() // 2) if dialog_size.width() > 0 else 0), + (optional_primary_screen.availableSize().height() // 2) + - ((dialog_size.height() // 2) if dialog_size.height() > 0 else 0), ) def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index c94be640..84cb8f07 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -58,7 +58,7 @@ class QubitValueLabelAndCheckbox: class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] - focus_out = QtCore.pyqtSignal(name="focusOut") + focusOut = QtCore.pyqtSignal() # noqa: N815 def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = None): super().__init__(parent) @@ -78,7 +78,7 @@ def sizeHint(self) -> QtCore.QSize: # noqa: N802 def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 super().focusOutEvent(ev) - self.focus_out.emit() + self.focusOut.emit() @dataclass(frozen=True) diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 83858949..43138af7 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -8,9 +8,15 @@ from __future__ import annotations +import sys from dataclasses import dataclass from typing import TYPE_CHECKING, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore from mqt import syrec @@ -244,9 +250,11 @@ def _record_quantum_register_layouts( quantum_register_layouts.sort(key=lambda qreg_layout: qreg_layout.first_qubit_of_qreg) return quantum_register_layouts - def rowCount(self, parent: QtCore.QModelIndex) -> int: # noqa: N802 + @override + def rowCount(self, parent: QtCore.QModelIndex) -> int: return 0 if parent.isValid() else len(self.simulation_run_models) + @override def data(self, index: QtCore.QModelIndex, role: int) -> object: if not index.isValid(): return None diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py index fdbb5ba7..b3a6ea45 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py @@ -30,7 +30,7 @@ class BatchTimestamps: class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] - batch_completed = QtCore.pyqtSignal(float, object, name="batchCompleted") + batchCompleted = QtCore.pyqtSignal(float, object) # noqa: N815 # While the cancellation operation is assumed to request the cancellation of the internal worker # as well as the worker_thread, due to the Qt event loop the QThread.finished signal is received # after the finished signal of the worker, i.e. the order of events in case of a cancellation or error will be: @@ -40,8 +40,8 @@ class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] # assuming that the cancel/error handler will request the worker shutdown, when the finished slot of the worker # perform the shutdown of the worker. Thus we introduce an additional flag in the finished signal to perform a # conditional shutdown of the worker in the slot that is connected to the finished signal. - finished = QtCore.pyqtSignal(bool, name="finished") - failed = QtCore.pyqtSignal(Exception, name="failed") + finished = QtCore.pyqtSignal(bool) + failed = QtCore.pyqtSignal(Exception) def __init__(self, do_batches_require_ack: bool) -> None: super().__init__() diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index c0a33809..158df83a 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Final # The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) +# TODO: Try catch and fallback incase that import fails import ijson.backends.yajl2_c as ijson from PyQt6 import QtCore diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index a4bb6544..af671656 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -133,7 +133,7 @@ def __init__( class CircuitView(QtWidgets.QGraphicsView): # type: ignore[misc] - qubit_label_clicked = QtCore.pyqtSignal(str, name="qubitLabelClicked") + qubitLabelClicked = QtCore.pyqtSignal(str) # noqa: N815 def __init__( self, @@ -182,7 +182,7 @@ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: # + "\nto internal DTO. This should not happen!", ) elif destringified_qubit_label.associated_qubit not in self.non_ancillary_or_garbage_qubits_lookup: - self.qubit_label_clicked.emit(str(destringified_qubit_label)) + self.qubitLabelClicked.emit(str(destringified_qubit_label)) super().mousePressEvent(event) From 9259c47270862161d470cd85a42fe2a7ea641f61 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 1 Feb 2026 14:18:32 +0100 Subject: [PATCH 65/88] Added prototype refactoring of cancellable base worker to support producer consumer pattern when running in QThread and communicating with UI thread. Added prototype of simulation run worker rework --- .../dialogs/simulation_run_dialog.py | 225 +++++++++++++----- .../workers/cancellable_worker_variants.py | 123 ++++++++++ .../workers/simulation_run_worker_rework.py | 165 +++++++++++++ 3 files changed, 457 insertions(+), 56 deletions(-) create mode 100644 python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py create mode 100644 python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 352b5d19..c8eca873 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -8,8 +8,9 @@ from __future__ import annotations +import queue import sys -from typing import TYPE_CHECKING, Final, cast +from typing import TYPE_CHECKING, Final if sys.version_info >= (3, 12): from typing import override @@ -23,17 +24,30 @@ from mqt import syrec - from ..simulation_run_model import QtSimulationRunModel + from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel + from ..workers.simulation_run_worker_rework import SimulationRunResult -from ...logger_utils import log_error_to_console, log_info_to_console +from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..styled_item_delegates.simulation_run_execution_styled_item_delegate import ( SimulationRunExecutionStyledItemDelegate, ) -from ..workers.simulation_run_worker import SimulationRunResult, SimulationRunWorker +from ..workers.cancellable_worker_variants import QueueConfig +from ..workers.simulation_run_worker import SimulationRunWorker +from ..workers.simulation_run_worker_rework import SimulationRunWorkerRework from .base_progress_dialog import BaseProgressDialog +DEFAULT_SMALL_QUEUE_SIZE: Final[int] = 500 +MODEL_UPDATE_RUNTIME_FORMAT: Final[str] = ( + "Total model update runtime [in seconds]: {total_model_update_runtime_in_seconds:f}" +) + + +# TODO: Update base progress dialog to use new cancellable worker +# TODO: Rename cancellable worker to producer-consumer worker? +# TODO: Rework all other dialogs to use refactored base classes +# TODO: Move clarifying comments to all input state generator dialog/worker? class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__( @@ -43,10 +57,23 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: create_default_layout=False, user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) + self.worker_instance: SimulationRunWorkerRework | None = None self.annotatable_quantum_computation: syrec.annotatable_quantum_computation | None = None self.shared_simulation_runs_model: QtSimulationRunModel | None = None self.stop_at_first_output_state_mismatch: bool = False self.num_executed_simulation_runs: int = 0 + self.last_fetched_simulation_run_idx: int = 0 + self.total_model_update_runtime_in_seconds: float = 0 + + self.sim_run_model_queue_batch_size: int = 0 + # Since we want to be able to prematurely cancel the long running operation blocking operations like get() or put() of the threading.queue + # cannot be used thus we do not need to constrain the size of the queue. + self.sim_run_model_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() + + self.sim_run_result_queue_batch_size: int = 0 + # Since we want to be able to prematurely cancel the long running operation blocking operations like get() or put() of the threading.queue + # cannot be used thus we do not need to constrain the size of the queue. + self.sim_run_result_queue: queue.SimpleQueue[SimulationRunResult] = queue.SimpleQueue() self.dialog_button_box.accepted.connect(self.accept) self.dialog_button_box.rejected.connect(self._handle_simulation_runs_cancel_button_click) @@ -78,6 +105,11 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: layout.addLayout(simulation_runs_list_layout) layout.addWidget(self.progress_bar) layout.addWidget(self.total_runtime_info_text_lbl) + + self.total_model_update_runtime_lbl = QtWidgets.QLabel() + self.total_model_update_runtime_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.total_model_update_runtime_lbl) + # layout.addStretch() layout.addWidget(self.dialog_button_box) self.setLayout(layout) @@ -87,31 +119,39 @@ def start_simulations( annotatable_quantum_computation: syrec.annotatable_quantum_computation, shared_simulation_run_model: QtSimulationRunModel, stop_at_first_output_state_mismatch: bool, - batch_size: int = 100, + sim_run_model_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, + sim_run_result_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: self.annotatable_quantum_computation = annotatable_quantum_computation - self.shared_simulation_runs_model = shared_simulation_run_model - self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) - self.stop_at_first_output_state_mismatch = stop_at_first_output_state_mismatch - log_info_to_console( - f"Starting execution of simulation runs, stopping after first output mismatch flag is set to {self.stop_at_first_output_state_mismatch}" - ) - expected_input_state_size: Final[int] = self.annotatable_quantum_computation.num_data_qubits - if batch_size <= 0 or expected_input_state_size <= 0: + if ( + sim_run_model_queue_batch_size <= 0 + or sim_run_result_queue_batch_size <= 0 + or expected_input_state_size <= 0 + ): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Invalid input parameters detected", - message_box_content=f"Expected batch size (value={batch_size}) as well as the expected input state size (value={expected_input_state_size}) to be positive integers!", + message_box_content=f"Expected simulation run model queue batch size (value={sim_run_model_queue_batch_size}), simulation run result queue batch size (value={sim_run_result_queue_batch_size}) as well as the expected input state size (value={expected_input_state_size}) to be positive integers!", is_cancellable=False, ) self.reject() return + self.sim_run_model_queue_batch_size = sim_run_model_queue_batch_size + self.sim_run_result_queue_batch_size = sim_run_result_queue_batch_size + + self.shared_simulation_runs_model = shared_simulation_run_model + self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) + self.stop_at_first_output_state_mismatch = stop_at_first_output_state_mismatch + log_info_to_console( + f"Starting execution of simulation runs, stopping after first output mismatch flag is set to {self.stop_at_first_output_state_mismatch}" + ) + expected_total_num_simulation_runs: Final[int] = shared_simulation_run_model.rowCount(QtCore.QModelIndex()) self.title_lbl.setText( - f"Executing {expected_total_num_simulation_runs} simulation runs with batch size {batch_size}!" + f"Executing {expected_total_num_simulation_runs} simulation runs with batch sizes (Sim. run model queue={sim_run_model_queue_batch_size}, Sim. run result queue={sim_run_result_queue_batch_size})!" ) if self.progress_bar is not None: if not self._can_value_can_be_used_as_progress_bar_max_value(expected_total_num_simulation_runs): @@ -131,30 +171,46 @@ def start_simulations( is_cancellable=False, ) + if not self._reset_previous_simulation_runs(): + return + # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker = SimulationRunWorker( + self.worker_instance = SimulationRunWorkerRework( self.annotatable_quantum_computation, - self.shared_simulation_runs_model, expected_input_state_size, - batch_size, self.stop_at_first_output_state_mismatch, + worker_recv_queue_config=QueueConfig( + queue_instance=self.sim_run_model_queue, queue_batch_size=sim_run_model_queue_batch_size + ), + worker_send_queue_config=QueueConfig( + queue_instance=self.sim_run_result_queue, queue_batch_size=sim_run_result_queue_batch_size + ), ) + self.worker_thread = QtCore.QThread() - self.worker.moveToThread(self.worker_thread) - self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.finished.connect( + self.worker_instance.moveToThread(self.worker_thread) + self.worker_thread.started.connect( + self.worker_instance.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker_instance.finished.connect( self._handle_all_simulation_run_executions_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.batchCompleted.connect( + self.worker_instance.batchCompleted.connect( self._handle_simulation_run_execution_batch_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.failed.connect(self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker_instance.requestingData.connect( + self._enqueue_next_simulation_runs, QtCore.Qt.ConnectionType.QueuedConnection + ) + self.worker_instance.failed.connect( + self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection + ) self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) + self._enqueue_next_simulation_runs() # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. @override @@ -198,7 +254,7 @@ def _handle_simulation_runs_failure(self, err: Exception) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_simulation_runs_cancel_button_click(self) -> bool: - if self.worker is None: + if self.worker_instance is None: return True if show_and_request_ok_in_optionally_cancellable_notification( @@ -214,54 +270,111 @@ def _handle_simulation_runs_cancel_button_click(self) -> bool: return True return False - @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] - def _handle_simulation_run_execution_batch_done( - self, simulation_run_execution_duration_in_seconds: float, batch_data: object - ) -> None: + @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] + def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_in_seconds: float) -> None: if self.stop_processing_recv_batches: return - if not SimulationRunWorker.is_batch_data_list_of_expected_type( - batch_data, SimulationRunResult, parent_widget_for_error_notification=self - ): - if self.worker is not None: - self.worker.ack_batch_processed() - return + n_received_sim_run_execution_results: int = 0 + to_be_updated_sim_run_number: int = -1 - casted_batch_data: Final[list[SimulationRunResult]] = cast("list[SimulationRunResult]", batch_data) - generated_batch_size: Final[int] = len(casted_batch_data) - if self.shared_simulation_runs_model is None: - log_error_to_console("Shared simulation runs model was not initialized during handling of batch!") - self._handle_non_recoverable_error(None) - return - - to_be_updated_simulation_run_number: int = 0 + batch_results_processing_start_timestamp: Final[float] = SimulationRunWorkerRework.get_timestamp() try: - for i in range(generated_batch_size): - to_be_updated_simulation_run_number = casted_batch_data[i].simulation_run_number + assert self.shared_simulation_runs_model is not None + for _ in range(self.sim_run_result_queue_batch_size): + simulation_run_result: SimulationRunResult = self.sim_run_result_queue.get_nowait() + to_be_updated_sim_run_number = simulation_run_result.simulation_run_number self.shared_simulation_runs_model.update_model_using_simulation_run_result( - self.shared_simulation_runs_model.index(to_be_updated_simulation_run_number), - casted_batch_data[i].actual_output_state, - casted_batch_data[i].do_expected_and_actual_outputs_match, - simulation_run_execution_duration_in_seconds * 1000 - if simulation_run_execution_duration_in_seconds > 0 - else 0, + self.shared_simulation_runs_model.index(to_be_updated_sim_run_number), + simulation_run_result.actual_output_state, + simulation_run_result.do_expected_and_actual_outputs_match, + simulation_run_result.sim_runtime_in_ms, ) + n_received_sim_run_execution_results += 1 + except queue.Empty: + pass except Exception as err: self._handle_non_recoverable_error( - f"Error during update of shared simulation run model with data from simulation run execution result of simulation run #{to_be_updated_simulation_run_number}, reason: {SimulationRunDialog._stringify_error(err)}" + f"Error during update of shared simulation run model with data from simulation run execution result of simulation run #{to_be_updated_sim_run_number}, reason: {SimulationRunDialog._stringify_error(err)}" ) return - if self.worker is not None: - self.worker.ack_batch_processed() + batch_results_processing_duration_in_seconds: Final[float] = ( + SimulationRunWorkerRework.calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_results_processing_start_timestamp + ).duration + ) - self._update_progress_text_with_batch_info(generated_batch_size, simulation_run_execution_duration_in_seconds) - self._accumulate_and_update_total_runtime(simulation_run_execution_duration_in_seconds) - self.num_executed_simulation_runs += generated_batch_size + self._update_progress_text_with_batch_info( + n_received_sim_run_execution_results, batch_generation_duration_in_seconds + ) + self._update_total_model_runtime_and_label(batch_results_processing_duration_in_seconds) + self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) + self.num_executed_simulation_runs += n_received_sim_run_execution_results if self.progress_bar is not None: self.progress_bar.setValue(self.num_executed_simulation_runs) + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _enqueue_next_simulation_runs(self) -> None: + try: + assert self.shared_simulation_runs_model is not None + for i in range( + self.last_fetched_simulation_run_idx, + self.last_fetched_simulation_run_idx + self.sim_run_model_queue_batch_size, + ): + to_be_enqueued_sim_run_model: SimulationRunModel | None = ( + self.shared_simulation_runs_model.get_simulation_run_model(i) + ) + self.last_fetched_simulation_run_idx += 1 + self.sim_run_model_queue.put(to_be_enqueued_sim_run_model) + if to_be_enqueued_sim_run_model is None: + break + # After having enqueued a new batch for the worker add a small delay before allowing the worker to produce new items + # which should improve the responsiveness of the UI due to the delay being enqueued into the UI threads event-queue thus + # given other events (mouse-clicks, resizes, etc.) to execute before the delayed functor is called + delay_in_ms: Final[int] = 250 + QtCore.QTimer.singleShot(delay_in_ms, self._allow_worker_to_produce_new_items) + except Exception as err: + self._handle_non_recoverable_error( + f"Error during enqueue of new simulation runs, reason: {SimulationRunDialog._stringify_error(err)}" + ) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _allow_worker_to_produce_new_items(self) -> None: + if self.worker_instance is None: + return + + try: + self.worker_instance.notify_to_continue_processing() + except Exception as err: + self._handle_non_recoverable_error( + f"Error while trying to notify simulation run execution worker about new batch data being available, reason: {SimulationRunDialog._stringify_error(err)}" + ) + + def _update_total_model_runtime_and_label(self, batch_model_update_runtime_in_seconds: float) -> None: + self.total_model_update_runtime_in_seconds += batch_model_update_runtime_in_seconds + self.total_model_update_runtime_lbl.setText( + MODEL_UPDATE_RUNTIME_FORMAT.format( + total_model_update_runtime_in_seconds=self.total_model_update_runtime_in_seconds + ) + ) + + def _reset_previous_simulation_runs(self) -> bool: + progress_info_msg: Final[str] = "Resetting previous simulation run results!" + self.progress_info_text_lbl.setText(progress_info_msg) + log_info_to_console(progress_info_msg) + + try: + assert self.shared_simulation_runs_model is not None + self.shared_simulation_runs_model.reset_prev_simulation_run_execution_results() + except Exception as err: + self._handle_non_recoverable_error( + f"Error during reset of previous simulation run execution results prior to new simulation, reason: {SimulationRunDialog._stringify_error(err)}" + ) + return False + else: + return True + def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: self.progress_info_text_lbl.setText("") if self.progress_bar is not None: diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py new file mode 100644 index 00000000..b6f20f99 --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py @@ -0,0 +1,123 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import sys +import threading +import time +from dataclasses import dataclass +from typing import TYPE_CHECKING, Final, Generic, TypeVar + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +from PyQt6 import QtCore + +if TYPE_CHECKING: + import queue + +RecvQueueElemType = TypeVar("RecvQueueElemType") +SendQueueElemType = TypeVar("SendQueueElemType") +QueueElemType = TypeVar("QueueElemType") + + +@dataclass(frozen=True) +class BatchTimestamps: + start: float + end: float + duration: float + + +@dataclass(frozen=True) +class QueueConfig(Generic[QueueElemType]): + queue_instance: queue.SimpleQueue[QueueElemType] + queue_batch_size: int + + +class CancellableProducerWorker(QtCore.QObject, Generic[SendQueueElemType]): # type: ignore[misc] + finished = QtCore.pyqtSignal(bool) + failed = QtCore.pyqtSignal(Exception) + batchCompleted = QtCore.pyqtSignal(float) # noqa: N815 + + def __init__(self, worker_send_queue_config: QueueConfig[SendQueueElemType]) -> None: + super().__init__() + self.cancellation_requested_flag: threading.Event = threading.Event() + self.send_queue: queue.SimpleQueue[SendQueueElemType] = worker_send_queue_config.queue_instance + self.send_queue_batch_size: int = worker_send_queue_config.queue_batch_size + self.cancelled_or_continue_processing_condition: threading.Condition = threading.Condition() + + def notify_to_continue_processing(self) -> None: + """Notify the producer to continue producing new elements. This can also be used to rate-limit the producer to only emit new batches when the consumer is ready.""" + with self.cancelled_or_continue_processing_condition: + self.cancelled_or_continue_processing_condition.notify() + + def request_cancellation(self) -> None: + """Request a cancellation of the long running producer operation in a thread-safe manner.""" + self.cancellation_requested_flag.set() + self.notify_to_continue_processing() + + def is_cancellation_requested(self) -> bool: + """Check whether cancellation of the long running operation is requested in a thread-safe manner.""" + return self.cancellation_requested_flag.is_set() + + def _wait_on_cancellation_or_input_data(self) -> None: + """Block the caller of this function until either cancellation is requested or one can continue producing new elements""" + with self.cancelled_or_continue_processing_condition: + self.cancelled_or_continue_processing_condition.wait() + + def _assert_valid_user_provided_parameter_values(self) -> None: + if self.send_queue_batch_size < 1: + msg = f"Send queue batch size must be larger than 0 but was actually {self.send_queue_batch_size}!" + raise ValueError(msg) + + @staticmethod + def get_timestamp() -> float: + return time.perf_counter() + + @staticmethod + def calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestamp: float) -> BatchTimestamps: + batch_end_timestamp: Final[float] = CancellableProducerWorker.get_timestamp() + batch_duration = batch_end_timestamp - batch_start_timestamp + return BatchTimestamps(batch_start_timestamp, batch_end_timestamp, batch_duration) + + +class CancellableProducerConsumerWorker( + CancellableProducerWorker[SendQueueElemType], Generic[RecvQueueElemType, SendQueueElemType] +): + requestingData = QtCore.pyqtSignal() # noqa: N815 + + def __init__( + self, + worker_send_queue_config: QueueConfig[SendQueueElemType], + worker_recv_queue_config: QueueConfig[RecvQueueElemType | None], + ) -> None: + super().__init__(worker_send_queue_config) + self.recv_queue: queue.SimpleQueue[RecvQueueElemType | None] = worker_recv_queue_config.queue_instance + self.recv_queue_batch_size: int = worker_recv_queue_config.queue_batch_size + + def _can_continue_processing_or_is_cancellation_requested(self) -> bool: + """Check whether elements in the receive queue exist or if cancellation was requested""" + return not self.recv_queue.empty() or self.is_cancellation_requested() + + @override + def _wait_on_cancellation_or_input_data(self) -> None: + """Block the caller of this function until either new elements in the receive queue exist or cancelation was requested""" + with self.cancelled_or_continue_processing_condition: + self.cancelled_or_continue_processing_condition.wait_for( + self._can_continue_processing_or_is_cancellation_requested + ) + + @override + def _assert_valid_user_provided_parameter_values(self) -> None: + super()._assert_valid_user_provided_parameter_values() + if self.recv_queue_batch_size < 1: + msg = f"Receive queue batch size must be larger than 0 but was actually {self.recv_queue_batch_size}!" + raise ValueError(msg) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py new file mode 100644 index 00000000..9a7dfccb --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py @@ -0,0 +1,165 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +from __future__ import annotations + +import queue +from dataclasses import dataclass +from typing import TYPE_CHECKING, Final + +from PyQt6 import QtCore + +from mqt import syrec + +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel +from .cancellable_worker_variants import CancellableProducerConsumerWorker + +if TYPE_CHECKING: + from .cancellable_worker_variants import BatchTimestamps, QueueConfig + + +@dataclass(frozen=True) +class SimulationRunResult: + simulation_run_number: int + actual_output_state: syrec.n_bit_values_container + do_expected_and_actual_outputs_match: bool | None + sim_runtime_in_ms: float + + +class SimulationRunWorkerRework(CancellableProducerConsumerWorker[SimulationRunModel, SimulationRunResult]): + def __init__( + self, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + expected_input_state_size: int, + stop_at_first_output_state_mismatch: bool, + worker_recv_queue_config: QueueConfig[SimulationRunModel | None], + worker_send_queue_config: QueueConfig[SimulationRunResult], + ) -> None: + super().__init__( + worker_send_queue_config=worker_send_queue_config, + worker_recv_queue_config=worker_recv_queue_config, + ) + self.expected_input_state_size = expected_input_state_size + self.annotatable_quantum_computation = annotatable_quantum_computation + self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_simulations(self) -> None: + curr_sim_run_num: int = 0 + request_more_queue_size_threshold: Final[int] = int(self.send_queue_batch_size * 0.2) + + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None + + found_outputs_mismatch: bool = False + has_reached_end_sentinel: bool = False + + try: + self._assert_valid_user_provided_parameter_values() + batch_start_timestamp = SimulationRunWorkerRework.get_timestamp() + n_remaining_batch_elems_to_generate = self.send_queue_batch_size + + while self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): + self._wait_on_cancellation_or_input_data() + + one_time_request_new_data_flag: bool = False + for _i in range(self.send_queue_batch_size): + if not self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): + break + + try: + dequeued_sim_run_model: SimulationRunModel | None = self.recv_queue.get( + block=False, timeout=0.2 + ) + has_reached_end_sentinel = dequeued_sim_run_model is None + # We use an element that is None as the sentinel value of the receive queue (i.e. dequeueing None means that we have reached processed the last enqueued element from the sender) + if has_reached_end_sentinel: + break + + if ( + not one_time_request_new_data_flag + and self.recv_queue.qsize() < request_more_queue_size_threshold + ): + self.requestingData.emit() + # The sender could take some time to produce new data so we do not want to repeatedly trigger this process by emitting the associated signal + # but only notify the sender once in the current loop. This can still trigger multiple signal emits depending on how fast the sender enqueues elements + # in the receive queue but will at least limit the signal emits for the current number of remaining queue elements. + one_time_request_new_data_flag = True + + # The mypy type-checker does not seem to infer that the dequeued simulation run model should be not None at this point since we already covered + # the None case in our check for the sentinel value + assert dequeued_sim_run_model is not None + sim_run_execution_result: SimulationRunResult = ( + SimulationRunWorkerRework._perform_single_sim_run_execution( + self.annotatable_quantum_computation, + curr_sim_run_num, + dequeued_sim_run_model.input_state, + dequeued_sim_run_model.expected_output_state, + ) + ) + self.send_queue.put_nowait(sim_run_execution_result) + found_outputs_mismatch |= ( + sim_run_execution_result.do_expected_and_actual_outputs_match is not None + and not sim_run_execution_result.do_expected_and_actual_outputs_match + ) + n_remaining_batch_elems_to_generate -= 1 + curr_sim_run_num += 1 + except queue.Empty: + self.requestingData.emit() + break + + if self.is_cancellation_requested(): + break + + if n_remaining_batch_elems_to_generate > 0 and not has_reached_end_sentinel: + # We dequeued all elements of the receive queue but have not reached the required batch size in the send queue to emit a new batch. + # Since we are expecting more elements from the sender due to not having reached the sentinel value of the receive queue we simply continue + # in the processing queue + continue + + n_remaining_batch_elems_to_generate = self.send_queue_batch_size + batch_timestamps = SimulationRunWorkerRework.calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + self.batchCompleted.emit(batch_timestamps.duration) + batch_start_timestamp = batch_timestamps.end + self.finished.emit(self.is_cancellation_requested()) + except Exception as error: + error_msg = f"Error in simulation run execution worker (curr. simulation run idx: {curr_sim_run_num}), reason: {type(error)=}, {error=}" + log_error_to_console(error_msg) + self.failed.emit(error) + + def _should_continue_processing(self, output_state_mismatch_flag: bool, reached_end_sentinel_flag: bool) -> bool: + return ( + not self.is_cancellation_requested() + and (not self.should_stop_at_first_output_state_mismatch or not output_state_mismatch_flag) + and not reached_end_sentinel_flag + ) + + @staticmethod + def _perform_single_sim_run_execution( + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + sim_run_num: int, + input_state: syrec.n_bit_values_container, + expected_output_state: syrec.n_bit_values_container | None, + ) -> SimulationRunResult: + actual_output_state = syrec.n_bit_values_container(input_state.size()) + + sim_start_timestamp: Final[float] = SimulationRunWorkerRework.get_timestamp() + syrec.simple_simulation(actual_output_state, annotatable_quantum_computation, input_state) + do_output_states_match: Final[bool | None] = SimulationRunModel.do_output_states_match( + expected_output_state, actual_output_state + ) + sim_duration_in_ms: Final[float] = ( + SimulationRunWorkerRework.calc_batch_duration_and_return_end_timestamp_in_seconds( + sim_start_timestamp + ).duration + * 1000 + ) + return SimulationRunResult(sim_run_num, actual_output_state, do_output_states_match, sim_duration_in_ms) From fd8dda56b328c4950ea21aca2fc644a48c6e1bdb Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 1 Feb 2026 17:14:23 +0100 Subject: [PATCH 66/88] Reworked all input states genertion worker to use new producer worker base class --- .../all_input_states_generator_dialog.py | 75 ++++--- .../dialogs/base_progress_dialog.py | 35 ++- .../dialogs/simulation_run_dialog.py | 55 ++--- .../simulation_view/simulation_run_model.py | 11 +- .../all_input_states_generator_worker.py | 101 +++++---- .../workers/cancellable_worker_variants.py | 17 +- .../workers/simulation_run_worker.py | 205 ++++++++++-------- .../workers/simulation_run_worker_rework.py | 165 -------------- 8 files changed, 258 insertions(+), 406 deletions(-) delete mode 100644 python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 17635f5b..c928ebad 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -8,6 +8,7 @@ from __future__ import annotations +import queue import sys from typing import TYPE_CHECKING, Final @@ -21,13 +22,13 @@ if TYPE_CHECKING: from PyQt6 import QtGui, QtWidgets - from ..simulation_run_model import QtSimulationRunModel + from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel -from ...logger_utils import log_error_to_console, log_info_to_console +from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification -from ..simulation_run_model import SimulationRunModel from ..workers.all_input_states_generator_worker import AllInputStatesGeneratorWorker -from .base_progress_dialog import BaseProgressDialog +from ..workers.cancellable_worker_variants import QueueConfig +from .base_progress_dialog import DEFAULT_MEDIUM_QUEUE_SIZE, DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, BaseProgressDialog class AllInputStatesGeneratorDialog(BaseProgressDialog[AllInputStatesGeneratorWorker]): @@ -38,16 +39,21 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: optional_progress_bar_text_format="Generated %v out of %m input states", ) self.shared_simulation_runs_model: QtSimulationRunModel | None = None + self.worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() + self.worker_send_queue_batch_size: int = 0 self.num_generated_input_states: int = 0 self.dialog_button_box.accepted.connect(self.accept) self.dialog_button_box.rejected.connect(self._handle_input_state_generation_cancel_button_click) def start_generation( - self, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, batch_size: int = 1000 + self, + shared_simulation_runs_model: QtSimulationRunModel, + expected_input_state_size: int, + worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model - self.title_lbl.setText(f"Generating simulation runs with batch size {batch_size}!") + self.title_lbl.setText(f"Generating simulation runs with batch size {worker_send_queue_batch_size}!") # Since the operands of the exponentiation operator are casted to floats, the result (a float) could exceed the maximum storable value in an integer so we need to check this error case since # otherwise setting the maximum value of the QtWidgets.QProgressBar would raise an exception/no matching overload will be found since said function expects an integer parameter. @@ -70,8 +76,25 @@ def start_generation( is_cancellable=False, ) + if worker_send_queue_batch_size < 1: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Invalid input parameters detected", + message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) to be a positive integer!", + is_cancellable=False, + ) + super().reject() + return + # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker = AllInputStatesGeneratorWorker(expected_input_state_size, batch_size) + self.worker_send_queue_batch_size = worker_send_queue_batch_size + self.worker = AllInputStatesGeneratorWorker( + expected_input_state_size, + worker_send_queue_config=QueueConfig( + queue_instance=self.worker_send_queue, queue_batch_size=self.worker_send_queue_batch_size + ), + ) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) self.worker.batchCompleted.connect( @@ -112,44 +135,30 @@ def reject(self) -> None: def _handle_input_state_generator_failure(self, err: Exception) -> None: self._handle_non_recoverable_error(err) - @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] - def _handle_generated_input_state_batch( - self, batch_generation_duration_in_seconds: float, batch_data: object - ) -> None: + @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] + def _handle_generated_input_state_batch(self, batch_generation_duration_in_seconds: float) -> None: if self.stop_processing_recv_batches: return - if not AllInputStatesGeneratorWorker.is_batch_data_list_of_expected_type( - batch_data, SimulationRunModel, parent_widget_for_error_notification=self - ): - if self.worker is not None: - self.worker.ack_batch_processed() - return - - generated_simulation_run_models: Final[list[SimulationRunModel]] = batch_data # type: ignore[assignment] - self._update_progress_text_with_batch_info( - len(generated_simulation_run_models), batch_generation_duration_in_seconds - ) - - if self.shared_simulation_runs_model is None: - log_error_to_console("Shared simulation runs model was not initialized during handling of batch!") - self._handle_non_recoverable_error(None) - return - + n_dequeued_batch_elems: int = 0 try: - self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) + assert self.shared_simulation_runs_model is not None + for _ in range(self.worker_send_queue_batch_size): + self.shared_simulation_runs_model.add_simulation_run_model(self.worker_send_queue.get_nowait()) + n_dequeued_batch_elems += 1 + except queue.Empty: + pass except Exception as sim_run_model_err: self._handle_non_recoverable_error(sim_run_model_err) return - if self.worker is not None: - self.worker.ack_batch_processed() - + self._update_progress_text_with_batch_info(n_dequeued_batch_elems, batch_generation_duration_in_seconds) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_generated_input_states += len(generated_simulation_run_models) + self.num_generated_input_states += n_dequeued_batch_elems if self.progress_bar is not None: self.progress_bar.setValue(self.num_generated_input_states) self.progress_info_text_lbl.setText("") + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_input_state_generator_finished(self, was_cancellation_requested: bool) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 93d17aed..b268d1d2 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -8,15 +8,13 @@ from __future__ import annotations -from typing import Final, Generic, TypeVar +from typing import Any, Final, Generic, TypeVar from PyQt6 import QtCore, QtGui, QtWidgets from ...logger_utils import log_error_to_console, log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification -from ..workers.cancellable_base_worker import CancellableBaseWorker - -T = TypeVar("T", bound=CancellableBaseWorker) +from ..workers.cancellable_worker_variants import CancellableProducerConsumerWorker, CancellableProducerWorker DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( "Total runtime [in seconds] (excluding model updates, internal waits): {total_runtime_in_seconds:f}" @@ -27,9 +25,14 @@ SMALL_DIALOG_WIDTH: Final[int] = 600 SMALL_DIALOG_HEIGHT: Final[int] = 300 +DEFAULT_SMALL_QUEUE_SIZE: Final[int] = 500 +DEFAULT_MEDIUM_QUEUE_SIZE: Final[int] = 1000 +DEFAULT_WORKER_CONTINUE_DELAY_IN_MS: Final[int] = 250 + +WorkerType = TypeVar("WorkerType", bound=CancellableProducerWorker[Any] | CancellableProducerConsumerWorker[Any, Any]) -class BaseProgressDialog(QtWidgets.QDialog, Generic[T]): # type: ignore[misc] +class BaseProgressDialog(QtWidgets.QDialog, Generic[WorkerType]): # type: ignore[misc] """Base class for progress dialogs with worker thread management. Note: Instances of this dialog are designed to be used only once. @@ -48,7 +51,7 @@ def __init__( super().__init__(parent) self.worker_thread: QtCore.QThread | None = None - self.worker: T | None = None + self.worker: WorkerType | None = None self.stop_processing_recv_batches: bool = False self.total_runtime_in_seconds: float = 0 @@ -142,6 +145,18 @@ def get_center_screen_position_for_size(dialog_size: QtCore.QSize) -> QtCore.QPo - ((dialog_size.height() // 2) if dialog_size.height() > 0 else 0), ) + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _allow_worker_to_continue(self) -> None: + if self.worker is None: + return + + try: + self.worker.notify_to_continue_processing() + except Exception as err: + self._handle_non_recoverable_error( + f"Error while trying to notify simulation run execution worker about new batch data being available, reason: {BaseProgressDialog._stringify_error(err)}" + ) + def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: self.progress_info_text_lbl.setText( DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT.format( @@ -219,6 +234,10 @@ def _update_displayed_error_text( ) self.error_text_lbl.setText(err_msg) + def _reset_workers(self) -> None: + self.worker_thread = None + self.worker = None + @staticmethod def _change_dialog_button_enable_state( dialog_button_box: QtWidgets.QDialogButtonBox, @@ -239,10 +258,6 @@ def _change_dialog_button_enable_state( is_cancellable=False, ) - def _reset_workers(self) -> None: - self.worker_thread = None - self.worker = None - @staticmethod def _stringify_error(error: Exception) -> str: return f"Error during long running worker operation! Reason: {type(error)=}, {error=}" diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index c8eca873..04547677 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -25,7 +25,7 @@ from mqt import syrec from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel - from ..workers.simulation_run_worker_rework import SimulationRunResult + from ..workers.simulation_run_worker import SimulationRunResult from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification @@ -34,10 +34,7 @@ ) from ..workers.cancellable_worker_variants import QueueConfig from ..workers.simulation_run_worker import SimulationRunWorker -from ..workers.simulation_run_worker_rework import SimulationRunWorkerRework -from .base_progress_dialog import BaseProgressDialog - -DEFAULT_SMALL_QUEUE_SIZE: Final[int] = 500 +from .base_progress_dialog import DEFAULT_SMALL_QUEUE_SIZE, DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, BaseProgressDialog MODEL_UPDATE_RUNTIME_FORMAT: Final[str] = ( "Total model update runtime [in seconds]: {total_model_update_runtime_in_seconds:f}" @@ -57,7 +54,6 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: create_default_layout=False, user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) - self.worker_instance: SimulationRunWorkerRework | None = None self.annotatable_quantum_computation: syrec.annotatable_quantum_computation | None = None self.shared_simulation_runs_model: QtSimulationRunModel | None = None self.stop_at_first_output_state_mismatch: bool = False @@ -124,11 +120,7 @@ def start_simulations( ) -> None: self.annotatable_quantum_computation = annotatable_quantum_computation expected_input_state_size: Final[int] = self.annotatable_quantum_computation.num_data_qubits - if ( - sim_run_model_queue_batch_size <= 0 - or sim_run_result_queue_batch_size <= 0 - or expected_input_state_size <= 0 - ): + if sim_run_model_queue_batch_size < 1 or sim_run_result_queue_batch_size < 1 or expected_input_state_size < 1: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -136,7 +128,7 @@ def start_simulations( message_box_content=f"Expected simulation run model queue batch size (value={sim_run_model_queue_batch_size}), simulation run result queue batch size (value={sim_run_result_queue_batch_size}) as well as the expected input state size (value={expected_input_state_size}) to be positive integers!", is_cancellable=False, ) - self.reject() + super().reject() return self.sim_run_model_queue_batch_size = sim_run_model_queue_batch_size @@ -175,7 +167,7 @@ def start_simulations( return # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker_instance = SimulationRunWorkerRework( + self.worker = SimulationRunWorker( self.annotatable_quantum_computation, expected_input_state_size, self.stop_at_first_output_state_mismatch, @@ -188,22 +180,18 @@ def start_simulations( ) self.worker_thread = QtCore.QThread() - self.worker_instance.moveToThread(self.worker_thread) - self.worker_thread.started.connect( - self.worker_instance.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection - ) - self.worker_instance.finished.connect( + self.worker.moveToThread(self.worker_thread) + self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.finished.connect( self._handle_all_simulation_run_executions_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker_instance.batchCompleted.connect( + self.worker.batchCompleted.connect( self._handle_simulation_run_execution_batch_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker_instance.requestingData.connect( + self.worker.requestingData.connect( self._enqueue_next_simulation_runs, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker_instance.failed.connect( - self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection - ) + self.worker.failed.connect(self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection) self.worker_thread.finished.connect(self.worker_thread.deleteLater) self.worker_thread.finished.connect(self._reset_workers) @@ -254,7 +242,7 @@ def _handle_simulation_runs_failure(self, err: Exception) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_simulation_runs_cancel_button_click(self) -> bool: - if self.worker_instance is None: + if self.worker is None: return True if show_and_request_ok_in_optionally_cancellable_notification( @@ -278,7 +266,7 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ n_received_sim_run_execution_results: int = 0 to_be_updated_sim_run_number: int = -1 - batch_results_processing_start_timestamp: Final[float] = SimulationRunWorkerRework.get_timestamp() + batch_results_processing_start_timestamp: Final[float] = SimulationRunWorker.get_timestamp() try: assert self.shared_simulation_runs_model is not None for _ in range(self.sim_run_result_queue_batch_size): @@ -300,7 +288,7 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ return batch_results_processing_duration_in_seconds: Final[float] = ( - SimulationRunWorkerRework.calc_batch_duration_and_return_end_timestamp_in_seconds( + SimulationRunWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_results_processing_start_timestamp ).duration ) @@ -332,25 +320,12 @@ def _enqueue_next_simulation_runs(self) -> None: # After having enqueued a new batch for the worker add a small delay before allowing the worker to produce new items # which should improve the responsiveness of the UI due to the delay being enqueued into the UI threads event-queue thus # given other events (mouse-clicks, resizes, etc.) to execute before the delayed functor is called - delay_in_ms: Final[int] = 250 - QtCore.QTimer.singleShot(delay_in_ms, self._allow_worker_to_produce_new_items) + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) except Exception as err: self._handle_non_recoverable_error( f"Error during enqueue of new simulation runs, reason: {SimulationRunDialog._stringify_error(err)}" ) - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def _allow_worker_to_produce_new_items(self) -> None: - if self.worker_instance is None: - return - - try: - self.worker_instance.notify_to_continue_processing() - except Exception as err: - self._handle_non_recoverable_error( - f"Error while trying to notify simulation run execution worker about new batch data being available, reason: {SimulationRunDialog._stringify_error(err)}" - ) - def _update_total_model_runtime_and_label(self, batch_model_update_runtime_in_seconds: float) -> None: self.total_model_update_runtime_in_seconds += batch_model_update_runtime_in_seconds self.total_model_update_runtime_lbl.setText( diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 43138af7..41a38e7e 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -290,19 +290,20 @@ def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: def get_all_simulation_run_models(self) -> Iterable[SimulationRunModel]: yield from self.simulation_run_models - def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> bool: - n_simulation_runs: int = len(self.simulation_run_models) + def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> None: + n_simulation_runs: Final[int] = self.rowCount(QtCore.QModelIndex()) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) self.simulation_run_models.append(simulation_run_model) self.endInsertRows() - return True def add_simulation_run_models(self, to_be_added_simulation_run_models: list[SimulationRunModel]) -> None: if len(to_be_added_simulation_run_models) == 0: return - idx_of_first_new_sim_run_model: int = len(self.simulation_run_models) - idx_of_last_new_sim_run_model: int = idx_of_first_new_sim_run_model + len(to_be_added_simulation_run_models) - 1 + idx_of_first_new_sim_run_model: Final[int] = self.rowCount(QtCore.QModelIndex()) + idx_of_last_new_sim_run_model: Final[int] = ( + idx_of_first_new_sim_run_model + len(to_be_added_simulation_run_models) - 1 + ) self.beginInsertRows(QtCore.QModelIndex(), idx_of_first_new_sim_run_model, idx_of_last_new_sim_run_model) self.simulation_run_models.extend(to_be_added_simulation_run_models) self.endInsertRows() diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index 28f3b38d..8d45f05c 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -8,91 +8,88 @@ from __future__ import annotations -import time +import sys from typing import TYPE_CHECKING, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + from PyQt6 import QtCore from mqt import syrec from ...logger_utils import log_error_to_console from ..simulation_run_model import SimulationRunModel -from .cancellable_base_worker import CancellableBaseWorker +from .cancellable_worker_variants import CancellableProducerWorker if TYPE_CHECKING: - from .cancellable_base_worker import BatchTimestamps + from .cancellable_worker_variants import BatchTimestamps, QueueConfig -class AllInputStatesGeneratorWorker(CancellableBaseWorker): - def __init__(self, expected_input_state_size: int, batch_size: int) -> None: - super().__init__(do_batches_require_ack=True) +class AllInputStatesGeneratorWorker(CancellableProducerWorker[SimulationRunModel]): + def __init__( + self, expected_input_state_size: int, worker_send_queue_config: QueueConfig[SimulationRunModel] + ) -> None: + super().__init__(worker_send_queue_config) self.expected_input_state_size: Final[int] = expected_input_state_size - self.batch_size: Final[int] = batch_size @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_generation(self) -> None: - try: - AllInputStatesGeneratorWorker._validate_parameters(self.expected_input_state_size, self.batch_size) - self_raised_error_msg: str | None = None - if self.wait_on_batch_processed_acknowledgement_condition is None: - self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" - log_error_to_console(self_raised_error_msg) - self.failed.emit(ValueError(self_raised_error_msg)) - return - - integer_encoding_input_state: int = 0 - n_states_to_generate: Final[int] = 2**self.expected_input_state_size + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None + integer_encoding_first_input_state_of_batch: int = 0 - batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] - batch_start_timestamp: float = 0 - batch_timestamps: BatchTimestamps | None = None + try: + self._assert_valid_user_provided_parameter_values() - batch_idx: int = 0 - while not self.is_cancellation_requested() and integer_encoding_input_state < n_states_to_generate: - batch_start_timestamp = AllInputStatesGeneratorWorker._get_timestamp() - for _ in range(self.batch_size): - if self.is_cancellation_requested() or integer_encoding_input_state == n_states_to_generate: + # We are assuming that the caller has validated that the 2^x operation will not overflow the maximum value of a 32 bit integer. + n_states_to_generate: Final[int] = 2**self.expected_input_state_size + batch_start_timestamp = AllInputStatesGeneratorWorker.get_timestamp() + while ( + not self.is_cancellation_requested() + and integer_encoding_first_input_state_of_batch < n_states_to_generate + ): + self._wait_on_cancellation_or_input_data() + for integer_encoding_input_state in range( + integer_encoding_first_input_state_of_batch, + min(integer_encoding_first_input_state_of_batch + self.send_queue_batch_size, n_states_to_generate), + ): + if self.is_cancellation_requested(): break - batch_data[batch_idx] = AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( - self.expected_input_state_size, integer_encoding_input_state + self.send_queue.put_nowait( + AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self.expected_input_state_size, integer_encoding_input_state + ) ) - integer_encoding_input_state += 1 - batch_idx += 1 - if batch_idx > 0 and batch_idx != self.batch_size: - del batch_data[batch_idx:] + if self.is_cancellation_requested(): + break + + # The addition operation will produce the wrong integer encoding the next input state in case of an cancellation request but this is ok since + # the cancellation also stops the generation of further input states. + integer_encoding_first_input_state_of_batch += self.send_queue_batch_size batch_timestamps = ( - AllInputStatesGeneratorWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + AllInputStatesGeneratorWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) - with QtCore.QMutexLocker(self.batch_ack_mutex): - if not self.is_cancellation_requested(): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) - # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using - # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. - time.sleep(0.1) - - for i in range(len(batch_data)): - batch_data[i] = None - batch_idx = 0 + self.batchCompleted.emit(batch_timestamps.duration) + batch_start_timestamp = batch_timestamps.end self.finished.emit(self.is_cancellation_requested()) except Exception as error: self_raised_error_msg = f"Error in all input states generator worker! Reason: {type(error)=}, {error=}" log_error_to_console(self_raised_error_msg) self.failed.emit(error) - @staticmethod - def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: - if expected_input_state_size < 1: - msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" - raise ValueError(msg) - - if batch_size < 1: - msg = f"Batch size must be larger than 0 but was actually {batch_size}" + @override + def _assert_valid_user_provided_parameter_values(self) -> None: + super()._assert_valid_user_provided_parameter_values() + if self.expected_input_state_size < 1: + msg = f"Expected input state size must be a positive integer but was actually {self.expected_input_state_size}!" raise ValueError(msg) @staticmethod diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py index b6f20f99..5b67ad40 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py @@ -31,9 +31,9 @@ @dataclass(frozen=True) class BatchTimestamps: - start: float - end: float - duration: float + start: float = 0 + end: float = 0 + duration: float = 0 @dataclass(frozen=True) @@ -68,10 +68,16 @@ def is_cancellation_requested(self) -> bool: """Check whether cancellation of the long running operation is requested in a thread-safe manner.""" return self.cancellation_requested_flag.is_set() + def _can_continue_processing_or_is_cancellation_requested(self) -> bool: + """Check whether the consumer has dequeued all elements from the producer queue (i.e. this worker) or if cancellation was requested""" + return self.send_queue.empty() or self.is_cancellation_requested() + def _wait_on_cancellation_or_input_data(self) -> None: - """Block the caller of this function until either cancellation is requested or one can continue producing new elements""" + """Block the caller of this function until either cancellation is requested or when the consumer has dequeued all items from the producer queue (i.e. this worker)""" with self.cancelled_or_continue_processing_condition: - self.cancelled_or_continue_processing_condition.wait() + self.cancelled_or_continue_processing_condition.wait_for( + self._can_continue_processing_or_is_cancellation_requested + ) def _assert_valid_user_provided_parameter_values(self) -> None: if self.send_queue_batch_size < 1: @@ -103,6 +109,7 @@ def __init__( self.recv_queue: queue.SimpleQueue[RecvQueueElemType | None] = worker_recv_queue_config.queue_instance self.recv_queue_batch_size: int = worker_recv_queue_config.queue_batch_size + @override def _can_continue_processing_or_is_cancellation_requested(self) -> bool: """Check whether elements in the receive queue exist or if cancellation was requested""" return not self.recv_queue.empty() or self.is_cancellation_requested() diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 3f58199d..215687fc 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -8,7 +8,7 @@ from __future__ import annotations -import time +import queue from dataclasses import dataclass from typing import TYPE_CHECKING, Final @@ -18,11 +18,10 @@ from ...logger_utils import log_error_to_console from ..simulation_run_model import SimulationRunModel -from .cancellable_base_worker import CancellableBaseWorker +from .cancellable_worker_variants import CancellableProducerConsumerWorker if TYPE_CHECKING: - from ..simulation_run_model import QtSimulationRunModel - from .cancellable_base_worker import BatchTimestamps + from .cancellable_worker_variants import BatchTimestamps, QueueConfig @dataclass(frozen=True) @@ -30,124 +29,138 @@ class SimulationRunResult: simulation_run_number: int actual_output_state: syrec.n_bit_values_container do_expected_and_actual_outputs_match: bool | None + sim_runtime_in_ms: float -class SimulationRunWorker(CancellableBaseWorker): +class SimulationRunWorker(CancellableProducerConsumerWorker[SimulationRunModel, SimulationRunResult]): def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, - shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, - batch_size: int, stop_at_first_output_state_mismatch: bool, + worker_recv_queue_config: QueueConfig[SimulationRunModel | None], + worker_send_queue_config: QueueConfig[SimulationRunResult], ) -> None: - super().__init__(do_batches_require_ack=True) - self.batch_size = batch_size + super().__init__( + worker_send_queue_config=worker_send_queue_config, + worker_recv_queue_config=worker_recv_queue_config, + ) self.expected_input_state_size = expected_input_state_size - self.shared_simulation_runs_model = shared_simulation_runs_model self.annotatable_quantum_computation = annotatable_quantum_computation self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: curr_sim_run_num: int = 0 + request_more_queue_size_threshold: Final[int] = int(self.send_queue_batch_size * 0.2) + + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None + + found_outputs_mismatch: bool = False + has_reached_end_sentinel: bool = False try: - SimulationRunWorker._validate_parameters(self.expected_input_state_size, self.batch_size) - self_raised_error_msg: str | None = None - - if self.wait_on_batch_processed_acknowledgement_condition is None: - self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" - log_error_to_console(self_raised_error_msg) - self.failed.emit(ValueError(self_raised_error_msg)) - return - - batch_data: list[SimulationRunResult | None] = [None for _ in range(self.batch_size)] - batch_start_timestamp: float = 0 - batch_timestamps: BatchTimestamps | None = None - n_sim_runs_to_execute: Final[int] = self.shared_simulation_runs_model.rowCount(QtCore.QModelIndex()) - self.shared_simulation_runs_model.reset_prev_simulation_run_execution_results() - - batch_idx: int = 0 - do_output_states_match: bool | None = None - found_first_output_state_mismatch: bool = False - while ( - not self.is_cancellation_requested() - and curr_sim_run_num < n_sim_runs_to_execute - and (not self.should_stop_at_first_output_state_mismatch or not found_first_output_state_mismatch) - ): - batch_start_timestamp = SimulationRunWorker._get_timestamp() - for _ in range(self.batch_size): + self._assert_valid_user_provided_parameter_values() + batch_start_timestamp = SimulationRunWorker.get_timestamp() + n_remaining_batch_elems_to_generate: int = self.send_queue_batch_size + + while self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): + self._wait_on_cancellation_or_input_data() + + one_time_request_new_data_flag: bool = False + for _ in range(self.send_queue_batch_size): if ( - self.is_cancellation_requested() - or curr_sim_run_num == n_sim_runs_to_execute - or (self.should_stop_at_first_output_state_mismatch and found_first_output_state_mismatch) + not self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel) + or n_remaining_batch_elems_to_generate < 0 ): break - curr_sim_run_model: SimulationRunModel = SimulationRunWorker._fetch_sim_model_or_throw( - self.shared_simulation_runs_model, curr_sim_run_num - ) - curr_input_state: syrec.n_bit_values_container = curr_sim_run_model.input_state - expected_output_state: syrec.n_bit_values_container | None = ( - curr_sim_run_model.expected_output_state - ) - actual_output_state = syrec.n_bit_values_container(self.expected_input_state_size) - syrec.simple_simulation(actual_output_state, self.annotatable_quantum_computation, curr_input_state) - do_output_states_match = SimulationRunModel.do_output_states_match( - expected_output_state, actual_output_state - ) - batch_data[batch_idx] = SimulationRunResult( - curr_sim_run_num, - actual_output_state, - do_output_states_match, - ) - - found_first_output_state_mismatch = ( - self.should_stop_at_first_output_state_mismatch - and do_output_states_match is not None - and not do_output_states_match - ) - curr_sim_run_num += 1 - batch_idx += 1 - if batch_idx > 0 and batch_idx != self.batch_size: - del batch_data[batch_idx:] - - batch_timestamps = SimulationRunWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + try: + dequeued_sim_run_model: SimulationRunModel | None = self.recv_queue.get( + block=False, timeout=0.2 + ) + has_reached_end_sentinel = dequeued_sim_run_model is None + # We use an element that is None as the sentinel value of the receive queue (i.e. dequeueing None means that we have reached processed the last enqueued element from the sender) + if has_reached_end_sentinel: + break + + if ( + not one_time_request_new_data_flag + and self.recv_queue.qsize() < request_more_queue_size_threshold + ): + self.requestingData.emit() + # The sender could take some time to produce new data so we do not want to repeatedly trigger this process by emitting the associated signal + # but only notify the sender once in the current loop. This can still trigger multiple signal emits depending on how fast the sender enqueues elements + # in the receive queue but will at least limit the signal emits for the current number of remaining queue elements. + one_time_request_new_data_flag = True + + # The mypy type-checker does not seem to infer that the dequeued simulation run model should be not None at this point since we already covered + # the None case in our check for the sentinel value + assert dequeued_sim_run_model is not None + sim_run_execution_result: SimulationRunResult = ( + SimulationRunWorker._perform_single_sim_run_execution( + self.annotatable_quantum_computation, + curr_sim_run_num, + dequeued_sim_run_model.input_state, + dequeued_sim_run_model.expected_output_state, + ) + ) + self.send_queue.put_nowait(sim_run_execution_result) + found_outputs_mismatch |= ( + sim_run_execution_result.do_expected_and_actual_outputs_match is not None + and not sim_run_execution_result.do_expected_and_actual_outputs_match + ) + n_remaining_batch_elems_to_generate -= 1 + curr_sim_run_num += 1 + except queue.Empty: + self.requestingData.emit() + break + + if self.is_cancellation_requested(): + break + + if n_remaining_batch_elems_to_generate > 0 and not has_reached_end_sentinel: + # We dequeued all elements of the receive queue but have not reached the required batch size in the send queue to emit a new batch. + # Since we are expecting more elements from the sender due to not having reached the sentinel value of the receive queue we simply continue + # in the processing queue + continue + + n_remaining_batch_elems_to_generate = self.send_queue_batch_size + batch_timestamps = SimulationRunWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) - self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) - with QtCore.QMutexLocker(self.batch_ack_mutex): - if not self.is_cancellation_requested(): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) - # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using - # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. - time.sleep(0.2) - - for i in range(len(batch_data)): - batch_data[i] = None - batch_idx = 0 + self.batchCompleted.emit(batch_timestamps.duration) + batch_start_timestamp = batch_timestamps.end self.finished.emit(self.is_cancellation_requested()) except Exception as error: - self_raised_error_msg = f"Error in simulation run execution worker (curr. simulation run idx: {curr_sim_run_num}), reason: {type(error)=}, {error=}" - log_error_to_console(self_raised_error_msg) + error_msg = f"Error in simulation run execution worker (curr. simulation run idx: {curr_sim_run_num}), reason: {type(error)=}, {error=}" + log_error_to_console(error_msg) self.failed.emit(error) - @staticmethod - def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: - if expected_input_state_size < 1: - msg = f"Expected state size must be a positive integer but was actually {expected_input_state_size}" - raise ValueError(msg) - - if batch_size < 1: - msg = f"Batch size must be larger than 0 but was actually {batch_size}" - raise ValueError(msg) + def _should_continue_processing(self, output_state_mismatch_flag: bool, reached_end_sentinel_flag: bool) -> bool: + return ( + not self.is_cancellation_requested() + and (not self.should_stop_at_first_output_state_mismatch or not output_state_mismatch_flag) + and not reached_end_sentinel_flag + ) @staticmethod - def _fetch_sim_model_or_throw(sim_runs_model: QtSimulationRunModel, sim_run_num: int) -> SimulationRunModel: - sim_run_model: SimulationRunModel | None = sim_runs_model.get_simulation_run_model(sim_run_num) - - if sim_run_model is None: - msg = f"Failed to fetch simulation run model #{sim_run_num}" - raise ValueError(msg) - return sim_run_model + def _perform_single_sim_run_execution( + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + sim_run_num: int, + input_state: syrec.n_bit_values_container, + expected_output_state: syrec.n_bit_values_container | None, + ) -> SimulationRunResult: + actual_output_state = syrec.n_bit_values_container(input_state.size()) + + sim_start_timestamp: Final[float] = SimulationRunWorker.get_timestamp() + syrec.simple_simulation(actual_output_state, annotatable_quantum_computation, input_state) + do_output_states_match: Final[bool | None] = SimulationRunModel.do_output_states_match( + expected_output_state, actual_output_state + ) + sim_duration_in_ms: Final[float] = ( + SimulationRunWorker.calc_batch_duration_and_return_end_timestamp_in_seconds(sim_start_timestamp).duration + * 1000 + ) + return SimulationRunResult(sim_run_num, actual_output_state, do_output_states_match, sim_duration_in_ms) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py deleted file mode 100644 index 9a7dfccb..00000000 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker_rework.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -from __future__ import annotations - -import queue -from dataclasses import dataclass -from typing import TYPE_CHECKING, Final - -from PyQt6 import QtCore - -from mqt import syrec - -from ...logger_utils import log_error_to_console -from ..simulation_run_model import SimulationRunModel -from .cancellable_worker_variants import CancellableProducerConsumerWorker - -if TYPE_CHECKING: - from .cancellable_worker_variants import BatchTimestamps, QueueConfig - - -@dataclass(frozen=True) -class SimulationRunResult: - simulation_run_number: int - actual_output_state: syrec.n_bit_values_container - do_expected_and_actual_outputs_match: bool | None - sim_runtime_in_ms: float - - -class SimulationRunWorkerRework(CancellableProducerConsumerWorker[SimulationRunModel, SimulationRunResult]): - def __init__( - self, - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - expected_input_state_size: int, - stop_at_first_output_state_mismatch: bool, - worker_recv_queue_config: QueueConfig[SimulationRunModel | None], - worker_send_queue_config: QueueConfig[SimulationRunResult], - ) -> None: - super().__init__( - worker_send_queue_config=worker_send_queue_config, - worker_recv_queue_config=worker_recv_queue_config, - ) - self.expected_input_state_size = expected_input_state_size - self.annotatable_quantum_computation = annotatable_quantum_computation - self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch - - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def start_simulations(self) -> None: - curr_sim_run_num: int = 0 - request_more_queue_size_threshold: Final[int] = int(self.send_queue_batch_size * 0.2) - - batch_start_timestamp: float = 0 - batch_timestamps: BatchTimestamps | None = None - - found_outputs_mismatch: bool = False - has_reached_end_sentinel: bool = False - - try: - self._assert_valid_user_provided_parameter_values() - batch_start_timestamp = SimulationRunWorkerRework.get_timestamp() - n_remaining_batch_elems_to_generate = self.send_queue_batch_size - - while self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): - self._wait_on_cancellation_or_input_data() - - one_time_request_new_data_flag: bool = False - for _i in range(self.send_queue_batch_size): - if not self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): - break - - try: - dequeued_sim_run_model: SimulationRunModel | None = self.recv_queue.get( - block=False, timeout=0.2 - ) - has_reached_end_sentinel = dequeued_sim_run_model is None - # We use an element that is None as the sentinel value of the receive queue (i.e. dequeueing None means that we have reached processed the last enqueued element from the sender) - if has_reached_end_sentinel: - break - - if ( - not one_time_request_new_data_flag - and self.recv_queue.qsize() < request_more_queue_size_threshold - ): - self.requestingData.emit() - # The sender could take some time to produce new data so we do not want to repeatedly trigger this process by emitting the associated signal - # but only notify the sender once in the current loop. This can still trigger multiple signal emits depending on how fast the sender enqueues elements - # in the receive queue but will at least limit the signal emits for the current number of remaining queue elements. - one_time_request_new_data_flag = True - - # The mypy type-checker does not seem to infer that the dequeued simulation run model should be not None at this point since we already covered - # the None case in our check for the sentinel value - assert dequeued_sim_run_model is not None - sim_run_execution_result: SimulationRunResult = ( - SimulationRunWorkerRework._perform_single_sim_run_execution( - self.annotatable_quantum_computation, - curr_sim_run_num, - dequeued_sim_run_model.input_state, - dequeued_sim_run_model.expected_output_state, - ) - ) - self.send_queue.put_nowait(sim_run_execution_result) - found_outputs_mismatch |= ( - sim_run_execution_result.do_expected_and_actual_outputs_match is not None - and not sim_run_execution_result.do_expected_and_actual_outputs_match - ) - n_remaining_batch_elems_to_generate -= 1 - curr_sim_run_num += 1 - except queue.Empty: - self.requestingData.emit() - break - - if self.is_cancellation_requested(): - break - - if n_remaining_batch_elems_to_generate > 0 and not has_reached_end_sentinel: - # We dequeued all elements of the receive queue but have not reached the required batch size in the send queue to emit a new batch. - # Since we are expecting more elements from the sender due to not having reached the sentinel value of the receive queue we simply continue - # in the processing queue - continue - - n_remaining_batch_elems_to_generate = self.send_queue_batch_size - batch_timestamps = SimulationRunWorkerRework.calc_batch_duration_and_return_end_timestamp_in_seconds( - batch_start_timestamp - ) - self.batchCompleted.emit(batch_timestamps.duration) - batch_start_timestamp = batch_timestamps.end - self.finished.emit(self.is_cancellation_requested()) - except Exception as error: - error_msg = f"Error in simulation run execution worker (curr. simulation run idx: {curr_sim_run_num}), reason: {type(error)=}, {error=}" - log_error_to_console(error_msg) - self.failed.emit(error) - - def _should_continue_processing(self, output_state_mismatch_flag: bool, reached_end_sentinel_flag: bool) -> bool: - return ( - not self.is_cancellation_requested() - and (not self.should_stop_at_first_output_state_mismatch or not output_state_mismatch_flag) - and not reached_end_sentinel_flag - ) - - @staticmethod - def _perform_single_sim_run_execution( - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - sim_run_num: int, - input_state: syrec.n_bit_values_container, - expected_output_state: syrec.n_bit_values_container | None, - ) -> SimulationRunResult: - actual_output_state = syrec.n_bit_values_container(input_state.size()) - - sim_start_timestamp: Final[float] = SimulationRunWorkerRework.get_timestamp() - syrec.simple_simulation(actual_output_state, annotatable_quantum_computation, input_state) - do_output_states_match: Final[bool | None] = SimulationRunModel.do_output_states_match( - expected_output_state, actual_output_state - ) - sim_duration_in_ms: Final[float] = ( - SimulationRunWorkerRework.calc_batch_duration_and_return_end_timestamp_in_seconds( - sim_start_timestamp - ).duration - * 1000 - ) - return SimulationRunResult(sim_run_num, actual_output_state, do_output_states_match, sim_duration_in_ms) From 76dc5a4a8e4408c15bbd81d3ecedf53f48774409 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 1 Feb 2026 19:14:22 +0100 Subject: [PATCH 67/88] Refactored simulation run json import worker and dialog to use producer-consumer base classes --- .../all_input_states_generator_dialog.py | 26 ++--- .../simulation_run_json_import_dialog.py | 84 +++++++++------- .../simulation_run_json_import_worker.py | 98 +++++++++---------- 3 files changed, 108 insertions(+), 100 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index c928ebad..980c521e 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -55,6 +55,17 @@ def start_generation( self.shared_simulation_runs_model = shared_simulation_runs_model self.title_lbl.setText(f"Generating simulation runs with batch size {worker_send_queue_batch_size}!") + if worker_send_queue_batch_size < 1 or expected_input_state_size < 1: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Invalid input parameters detected", + message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) and expected input state size(value={expected_input_state_size}) to be a positive integer!", + is_cancellable=False, + ) + super().reject() + return + # Since the operands of the exponentiation operator are casted to floats, the result (a float) could exceed the maximum storable value in an integer so we need to check this error case since # otherwise setting the maximum value of the QtWidgets.QProgressBar would raise an exception/no matching overload will be found since said function expects an integer parameter. n_expected_sim_runs_to_generate: Final[float] = 2**expected_input_state_size @@ -76,17 +87,6 @@ def start_generation( is_cancellable=False, ) - if worker_send_queue_batch_size < 1: - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.ERROR, - message_box_parent=self, - message_box_title="Invalid input parameters detected", - message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) to be a positive integer!", - is_cancellable=False, - ) - super().reject() - return - # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker_send_queue_batch_size = worker_send_queue_batch_size self.worker = AllInputStatesGeneratorWorker( @@ -148,8 +148,8 @@ def _handle_generated_input_state_batch(self, batch_generation_duration_in_secon n_dequeued_batch_elems += 1 except queue.Empty: pass - except Exception as sim_run_model_err: - self._handle_non_recoverable_error(sim_run_model_err) + except Exception as sim_run_model_addition_err: + self._handle_non_recoverable_error(sim_run_model_addition_err) return self._update_progress_text_with_batch_info(n_dequeued_batch_elems, batch_generation_duration_in_seconds) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 679297cd..3912892b 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -8,8 +8,9 @@ from __future__ import annotations +import queue import sys -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING if sys.version_info >= (3, 12): from typing import override @@ -23,13 +24,13 @@ from PyQt6 import QtGui - from ..simulation_run_model import QtSimulationRunModel + from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel -from ...logger_utils import log_error_to_console, log_info_to_console +from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification -from ..simulation_run_model import SimulationRunModel +from ..workers.cancellable_worker_variants import QueueConfig from ..workers.simulation_run_json_import_worker import SimulationRunJsonImportWorker -from .base_progress_dialog import BaseProgressDialog +from .base_progress_dialog import DEFAULT_MEDIUM_QUEUE_SIZE, DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, BaseProgressDialog class SimulationRunJsonImportDialog(BaseProgressDialog[SimulationRunJsonImportWorker]): @@ -43,6 +44,9 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.num_imported_simulation_runs: int = 0 self.shared_simulation_runs_model: QtSimulationRunModel | None = None + self.worker_send_queue_batch_size: int = 0 + self.worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() + self.dialog_button_box.accepted.connect(self.accept) self.dialog_button_box.rejected.connect(self._handle_import_from_file_cancel_button_click) @@ -72,19 +76,39 @@ def start_import( path_to_json_file: Path, shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, - batch_size: int = 1000, + worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, ) -> None: self.shared_simulation_runs_model = shared_simulation_runs_model - self.title_lbl.setText(f"Importing simulation runs from .json file with batch size {batch_size}!") + self.title_lbl.setText( + f"Importing simulation runs from .json file with batch size {worker_send_queue_batch_size}!" + ) self.import_origin_info_lbl.setText(f"Import source: {path_to_json_file!s}") + if worker_send_queue_batch_size < 1 or expected_input_state_size < 1: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Invalid input parameters detected", + message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) and expected input state size(value={expected_input_state_size}) to be a positive integer!", + is_cancellable=False, + ) + super().reject() + return + + self.worker_send_queue_batch_size = worker_send_queue_batch_size # Some helpful links are the official QThread documentation but some helpful explanaitions were also found in: # - https://www.haccks.com/posts/how-to-use-qthread-correctly-p1/ # We are creating a worker object that will perform a long running operation in the future with the worker # instance being a member of the dialog class/object. Since the worker will define slots for the cancellation of # the long running operation that it executes but also emits signals, said worker needs to be implemented as a QObject that will run its own event loop # instead of subclassing QThread which executes its slots in the thread in which the QThread was created and might not execute its own event loop. - self.worker = SimulationRunJsonImportWorker(path_to_json_file, expected_input_state_size, batch_size) + self.worker = SimulationRunJsonImportWorker( + path_to_json_file, + expected_input_state_size, + worker_send_queue_config=QueueConfig( + queue_instance=self.worker_send_queue, queue_batch_size=self.worker_send_queue_batch_size + ), + ) # Create a new QThread that manages one system thread WITHOUT BEING AN ACTUAL THREAD (see: https://doc.qt.io/qtforpython-6/PySide6/QtCore/QThread.html#detailed-description) self.worker_thread = QtCore.QThread() # We are now modifying the thread affinity (https://doc.qt.io/qt-6/qobject.html#thread-affinity) of the worker object to the worker thread. @@ -143,43 +167,35 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: def _handle_importer_failure(self, err: Exception) -> None: self._handle_non_recoverable_error(err) - @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] - def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: + @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] + def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: float) -> None: if self.stop_processing_recv_batches: return - if not SimulationRunJsonImportWorker.is_batch_data_list_of_expected_type( - batch_data, SimulationRunModel, parent_widget_for_error_notification=self - ): - if self.worker is not None: - self.worker.ack_batch_processed() - return - - generated_simulation_run_models: Final[list[SimulationRunModel]] = batch_data # type: ignore[assignment] - self._update_progress_text_with_batch_info( - len(generated_simulation_run_models), batch_generation_duration_in_seconds - ) - - if self.shared_simulation_runs_model is None: - log_error_to_console("Shared simulation runs model was not initialized during handling of batch!") - self._handle_non_recoverable_error(None) - return - + n_dequeued_batch_elems: int = 0 try: - self.shared_simulation_runs_model.add_simulation_run_models(generated_simulation_run_models) - except Exception as sim_run_model_err: - self._handle_non_recoverable_error(sim_run_model_err) + assert self.shared_simulation_runs_model is not None + for _ in range(self.worker_send_queue_batch_size): + self.shared_simulation_runs_model.add_simulation_run_model(self.worker_send_queue.get_nowait()) + n_dequeued_batch_elems += 1 + except queue.Empty: + pass + except Exception as sim_run_model_addition_err: + self._handle_non_recoverable_error(sim_run_model_addition_err) return - if self.worker is not None: - self.worker.ack_batch_processed() - + self._update_progress_text_with_batch_info(n_dequeued_batch_elems, batch_generation_duration_in_seconds) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_imported_simulation_runs += len(generated_simulation_run_models) + self.num_imported_simulation_runs += n_dequeued_batch_elems self.num_imported_simulation_runs_info_lbl.setText( f"Num. imported simulation runs: {self.num_imported_simulation_runs}" ) + if self.progress_bar is not None: + self.progress_bar.setValue(self.num_generated_input_states) + self.progress_info_text_lbl.setText("") + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) + @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_import_completion(self, was_cancellation_requested: bool) -> None: self.progress_info_text_lbl.setText("Simulation run import finished!") diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 158df83a..28bf8e81 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -8,9 +8,14 @@ from __future__ import annotations -import time +import sys from typing import TYPE_CHECKING, Any, Final +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + # The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) # TODO: Try catch and fallback incase that import fails import ijson.backends.yajl2_c as ijson @@ -20,54 +25,46 @@ from ...logger_utils import log_error_to_console, log_info_to_console from ..simulation_run_model import SimulationRunModel -from .cancellable_base_worker import CancellableBaseWorker +from .cancellable_worker_variants import CancellableProducerWorker if TYPE_CHECKING: from pathlib import Path - from .cancellable_base_worker import BatchTimestamps + from .cancellable_worker_variants import BatchTimestamps, QueueConfig SIMULATION_RUNS_JSON_KEY: Final[str] = "simulationRuns" INPUT_STATE_JSON_KEY: Final[str] = "in" EXPECTED_OUTPUT_STATE_JSON_KEY: Final[str] = "out" -class SimulationRunJsonImportWorker(CancellableBaseWorker): - def __init__(self, path_to_json_file: Path, expected_input_state_size: int, batch_size: int) -> None: - super().__init__(do_batches_require_ack=True) - +class SimulationRunJsonImportWorker(CancellableProducerWorker[SimulationRunModel]): + def __init__( + self, + path_to_json_file: Path, + expected_input_state_size: int, + worker_send_queue_config: QueueConfig[SimulationRunModel], + ) -> None: + super().__init__(worker_send_queue_config) self.path_to_json_file = path_to_json_file self.expected_input_state_size = expected_input_state_size - self.batch_size = batch_size @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_import(self) -> None: - try: - SimulationRunJsonImportWorker._validate_parameters(self.expected_input_state_size, self.batch_size) - self_raised_error_msg: str | None = None - - if self.wait_on_batch_processed_acknowledgement_condition is None: - self_raised_error_msg = "Internal batch processed acknowledgement condition was not initialized" - log_error_to_console(self_raised_error_msg) - self.failed.emit(ValueError(self_raised_error_msg)) - return + batch_start_timestamp: float = SimulationRunJsonImportWorker.get_timestamp() + batch_timestamps: BatchTimestamps | None = None - batch_idx: int = 0 - batch_data: list[SimulationRunModel | None] = [None for _ in range(self.batch_size)] + try: + self._assert_valid_user_provided_parameter_values() + n_remaining_input_states_to_import_in_batch: int = self.send_queue_batch_size # Reading bytes instead of strings leads to better parser performance with self.path_to_json_file.open("rb") as file: - batch_start_timestamp: float = SimulationRunJsonImportWorker._get_timestamp() - batch_timestamps: BatchTimestamps | None = None # The json parser starts at the first element matching the prefix which in our case starts at an element with key 'simulationRuns' that is expected # to be a property of the top level element (i.e. the path to the 'simulationRuns' element is relative to the top level element). # Additionally, with the postfix '.item', only the entries of a JSON array are processed. If the 'simulationRuns' entry value is no # array then no objects will be parsed. parser = ijson.items(file, prefix=f"{SIMULATION_RUNS_JSON_KEY}.item") for arr_elem in parser: - if self.is_cancellation_requested(): - break - # the ison.items(...) function converts JSON objects to python dictionaries (https://pypi.org/project/ijson/#options). However, we # need to discard any other type of JSON elements (integers, strings, array, etc.) by checking whether we are actually processing a # python dictionary. @@ -77,54 +74,49 @@ def start_import(self) -> None: ) continue - batch_data[batch_idx] = SimulationRunJsonImportWorker._try_deserialize_simulation_run( - self.expected_input_state_size, arr_elem + self.send_queue.put_nowait( + SimulationRunJsonImportWorker._try_deserialize_simulation_run( + self.expected_input_state_size, arr_elem + ) ) - batch_idx += 1 - if batch_idx < self.batch_size: + n_remaining_input_states_to_import_in_batch -= 1 + if self.is_cancellation_requested(): + break + + if n_remaining_input_states_to_import_in_batch > 0: continue batch_timestamps = ( - SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + SimulationRunJsonImportWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) + self.batchCompleted.emit(batch_timestamps.duration) batch_start_timestamp = batch_timestamps.end - self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) - with QtCore.QMutexLocker(self.batch_ack_mutex): - if not self.is_cancellation_requested(): - self.wait_on_batch_processed_acknowledgement_condition.wait(self.batch_ack_mutex) - # An artificial delay improves the responsiveness of the UI but does not seem like the best solution. However, using - # a delayed acknowledgement in the UI thread would increase the complexity of the implementation of the UI. - time.sleep(0.1) + n_remaining_input_states_to_import_in_batch = self.send_queue_batch_size + self._wait_on_cancellation_or_input_data() - for i in range(self.batch_size): - batch_data[i] = None - batch_idx = 0 - - if batch_idx != 0 and not self.is_cancellation_requested(): - del batch_data[batch_idx:] + # If we reached the end of the input .json file without reaching our batch threshold + # emit the current enqueued elements to the consumer. + if n_remaining_input_states_to_import_in_batch < self.send_queue_batch_size: batch_timestamps = ( - SimulationRunJsonImportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( + SimulationRunJsonImportWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) ) - self.batchCompleted.emit(batch_timestamps.duration, batch_data.copy()) - self.finished.emit(self.is_cancellation_requested()) + self.batchCompleted.emit(batch_timestamps.duration) + self.finished.emit(self.is_cancellation_requested()) except Exception as error: self_raised_error_msg = f"Error in simulaton run import worker! Reason: {type(error)=}, {error=}" log_error_to_console(self_raised_error_msg) self.failed.emit(error) - @staticmethod - def _validate_parameters(expected_input_state_size: int, batch_size: int) -> None: - if expected_input_state_size < 1: - msg = f"Expected input state size must be a positive integer but was actually {expected_input_state_size}!" - raise ValueError(msg) - - if batch_size < 1: - msg = f"Batch size must be larger than 0 but was actually {batch_size}" + @override + def _assert_valid_user_provided_parameter_values(self) -> None: + super()._assert_valid_user_provided_parameter_values() + if self.expected_input_state_size < 1: + msg = f"Expected input state size must be a positive integer but was actually {self.expected_input_state_size}!" raise ValueError(msg) @staticmethod From a33c6d203dca75b853830c9ff50e0c1ff8a27664 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 1 Feb 2026 20:35:35 +0100 Subject: [PATCH 68/88] Refactored simulation run json exportt worker and dialog to use producer-consumer base classes --- .../quantum_circuit_simulation_dialog.py | 7 +- .../simulation_run_json_export_dialog.py | 97 +++++++++--- .../simulation_run_json_import_dialog.py | 2 +- .../simulation_view/simulation_run_model.py | 21 +-- .../workers/cancellable_base_worker.py | 140 ---------------- .../simulation_run_json_export_worker.py | 149 ++++++++++++------ .../workers/simulation_run_worker.py | 2 +- 7 files changed, 183 insertions(+), 235 deletions(-) delete mode 100644 python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index a23f7317..b0ef29b4 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -347,13 +347,12 @@ def handle_simulation_run_selection_change( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_add_btn_click(self) -> None: - if not self.simulation_runs_model.add_simulation_run_model( + self.simulation_runs_model.add_simulation_run_model( SimulationRunModel( input_state=syrec.n_bit_values_container(self.expected_input_output_state_size), expected_output_state=None, ) - ): - return + ) optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() if not assert_all_required_widgets_found_or_close_dialog( @@ -481,7 +480,7 @@ def handle_sim_run_save_to_file_btn_click(self) -> None: self.simulation_run_export_to_file_dialog.start_export( Path(filename), self.associated_stringified_syrec_program, - self.simulation_runs_model.get_all_simulation_run_models(), + self.simulation_runs_model, self.simulation_runs_model.rowCount(QtCore.QModelIndex()), ) self.simulation_run_export_to_file_dialog.show() diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index cbace68c..cbd3bdb2 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -8,8 +8,9 @@ from __future__ import annotations +import queue import sys -from typing import TYPE_CHECKING, Final, cast +from typing import TYPE_CHECKING, Final if sys.version_info >= (3, 12): from typing import override @@ -19,16 +20,16 @@ from PyQt6 import QtCore, QtWidgets if TYPE_CHECKING: - from collections.abc import Iterable from pathlib import Path from PyQt6 import QtGui - from ..simulation_run_model import SimulationRunModel + from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification +from ..workers.cancellable_worker_variants import QueueConfig from ..workers.simulation_run_json_export_worker import ExportedBatchData, SimulationRunJsonExportWorker -from .base_progress_dialog import BaseProgressDialog +from .base_progress_dialog import DEFAULT_SMALL_QUEUE_SIZE, DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, BaseProgressDialog EXPORTED_SIM_RUNS_DATA_LABEL: Final[str] = ( "In total {n_exported_sim_runs:d} simulation runs were exported with {n_skipped_sim_runs:d} simulation runs being skipped" @@ -46,6 +47,13 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.num_processed_sim_runs: int = 0 self.total_num_exported_sim_runs: int = 0 self.total_num_skipped_sim_runs: int = 0 + self.last_exported_sim_run_num: int = 0 + + self.worker_recv_queue_batch_size: int = 0 + self.worker_send_queue: queue.SimpleQueue[ExportedBatchData] = queue.SimpleQueue() + self.worker_recv_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() + self.shared_simulation_runs_model: QtSimulationRunModel | None = None + self.dialog_button_box.accepted.connect(self.accept) self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) @@ -72,13 +80,24 @@ def start_export( self, export_location: Path, associated_stringified_syrec_program: str, - sim_runs_to_export: Iterable[SimulationRunModel], + shared_simulation_run_model: QtSimulationRunModel, num_sim_runs_to_export: int, - batch_size: int = 500, + worker_recv_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: - self.title_lbl.setText(f"Exporting simulation runs with batch size {batch_size}!") + self.title_lbl.setText(f"Exporting simulation runs with batch size {worker_recv_queue_batch_size}!") self.export_location_info_lbl.setText(f"Export destination: {export_location!s}") + if worker_recv_queue_batch_size < 1 or num_sim_runs_to_export < 1: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Invalid input parameters detected", + message_box_content=f"Expected worker receive queue batch size (value={worker_recv_queue_batch_size}) and number of expected simulation runs (value={num_sim_runs_to_export}) to be a positive integers!", + is_cancellable=False, + ) + super().reject() + return + if self.progress_bar is not None: if not self._can_value_can_be_used_as_progress_bar_max_value(num_sim_runs_to_export): # We do not ask for confirmation to close the dialog since we faulted before the export started. @@ -97,13 +116,23 @@ def start_export( is_cancellable=False, ) + self.shared_simulation_runs_model = shared_simulation_run_model + self.worker_recv_queue_batch_size = worker_recv_queue_batch_size # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker = SimulationRunJsonExportWorker( - export_location, associated_stringified_syrec_program, sim_runs_to_export, batch_size + export_location, + associated_stringified_syrec_program, + worker_send_queue_config=QueueConfig(queue_instance=self.worker_send_queue, queue_batch_size=1), + worker_recv_queue_config=QueueConfig( + queue_instance=self.worker_recv_queue, queue_batch_size=self.worker_recv_queue_batch_size + ), ) self.worker_thread = QtCore.QThread() self.worker.moveToThread(self.worker_thread) self.worker.batchCompleted.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) + self.worker.requestingData.connect( + self._enqueue_next_simulation_runs_to_export, QtCore.Qt.ConnectionType.QueuedConnection + ) self.worker.finished.connect(self._handle_export_completion, QtCore.Qt.ConnectionType.QueuedConnection) self.worker.failed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) @@ -115,6 +144,7 @@ def start_export( self.worker_thread.finished.connect(self._reset_workers) self.worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) + self._enqueue_next_simulation_runs_to_export() # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. @override @@ -138,32 +168,59 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: def _handle_export_failure(self, err: Exception) -> None: self._handle_non_recoverable_error(err) - @QtCore.pyqtSlot(float, object) # type: ignore[untyped-decorator] - def _handle_batch_exported(self, batch_generation_duration_in_seconds: float, batch_data: object) -> None: - if not SimulationRunJsonExportWorker.is_batch_data_of_type( - batch_data, ExportedBatchData, parent_widget_for_error_notification=self - ): - if self.worker is not None: - self.worker.ack_batch_processed() + @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] + def _handle_batch_exported(self, batch_generation_duration_in_seconds: float) -> None: + batch_data: ExportedBatchData = ExportedBatchData(exported_sim_runs=0, skipped_sim_runs=0) + try: + batch_data = self.worker_send_queue.get_nowait() + except queue.Empty: + # TODO: This should not happen + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) + return + except Exception as err: + self._handle_non_recoverable_error(err) return - casted_batch_data: Final[ExportedBatchData] = cast("ExportedBatchData", batch_data) self.progress_info_text_lbl.setText( - f"Batch completed! Exported {casted_batch_data.exported_sim_runs} and skipping {casted_batch_data.skipped_sim_runs} simulation runs. Runtime [in seconds]: {batch_generation_duration_in_seconds}" + f"Batch completed! Exported {batch_data.exported_sim_runs} and skipping {batch_data.skipped_sim_runs} simulation runs. Runtime [in seconds]: {batch_generation_duration_in_seconds}" ) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_processed_sim_runs += casted_batch_data.exported_sim_runs + casted_batch_data.skipped_sim_runs + self.num_processed_sim_runs += batch_data.exported_sim_runs + batch_data.skipped_sim_runs if self.progress_bar is not None: self.progress_bar.setValue(self.num_processed_sim_runs) - self.total_num_exported_sim_runs += casted_batch_data.exported_sim_runs - self.total_num_skipped_sim_runs += casted_batch_data.skipped_sim_runs + self.total_num_exported_sim_runs += batch_data.exported_sim_runs + self.total_num_skipped_sim_runs += batch_data.skipped_sim_runs self.exported_sim_runs_data_lbl.setText( EXPORTED_SIM_RUNS_DATA_LABEL.format( n_exported_sim_runs=self.total_num_exported_sim_runs, n_skipped_sim_runs=self.total_num_skipped_sim_runs ) ) + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _enqueue_next_simulation_runs_to_export(self) -> None: + try: + assert self.shared_simulation_runs_model is not None + for i in range( + self.last_exported_sim_run_num, self.last_exported_sim_run_num + self.worker_recv_queue_batch_size + ): + to_be_enqueued_sim_run_model: SimulationRunModel | None = ( + self.shared_simulation_runs_model.get_simulation_run_model(i) + ) + self.last_exported_sim_run_num += 1 + self.worker_recv_queue.put_nowait(to_be_enqueued_sim_run_model) + if to_be_enqueued_sim_run_model is None: + break + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) + except Exception as err: + # TODO: Better error + self._handle_non_recoverable_error( + ValueError( + f"Error during enqueue of new simulation runs, reason: {SimulationRunJsonExportDialog._stringify_error(err)}" + ) + ) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_export_completion(self, was_cancellation_requested: bool) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 3912892b..d7d3f58d 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -89,7 +89,7 @@ def start_import( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Invalid input parameters detected", - message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) and expected input state size(value={expected_input_state_size}) to be a positive integer!", + message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) and expected input state size(value={expected_input_state_size}) to be a positive integers!", is_cancellable=False, ) super().reject() diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 41a38e7e..36f376b8 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -10,7 +10,7 @@ import sys from dataclasses import dataclass -from typing import TYPE_CHECKING, Final +from typing import Final if sys.version_info >= (3, 12): from typing import override @@ -23,9 +23,6 @@ from ..logger_utils import log_error_to_console -if TYPE_CHECKING: - from collections.abc import Iterable - # Some debugging tips: https://www.eso.org/~eltmgr/ECS/documents-latest/CUT/sphinx_doc/latest/docs/500_gui_development.html#gdb # First custom item data role usable according to: https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum SIMULATION_RUN_IO_STATE_QT_ROLE: Final[int] = QtCore.Qt.ItemDataRole.UserRole @@ -287,27 +284,13 @@ def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: return self.simulation_run_models[index] return None - def get_all_simulation_run_models(self) -> Iterable[SimulationRunModel]: - yield from self.simulation_run_models - + # TODO: Should we perform a validation here? def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> None: n_simulation_runs: Final[int] = self.rowCount(QtCore.QModelIndex()) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) self.simulation_run_models.append(simulation_run_model) self.endInsertRows() - def add_simulation_run_models(self, to_be_added_simulation_run_models: list[SimulationRunModel]) -> None: - if len(to_be_added_simulation_run_models) == 0: - return - - idx_of_first_new_sim_run_model: Final[int] = self.rowCount(QtCore.QModelIndex()) - idx_of_last_new_sim_run_model: Final[int] = ( - idx_of_first_new_sim_run_model + len(to_be_added_simulation_run_models) - 1 - ) - self.beginInsertRows(QtCore.QModelIndex(), idx_of_first_new_sim_run_model, idx_of_last_new_sim_run_model) - self.simulation_run_models.extend(to_be_added_simulation_run_models) - self.endInsertRows() - def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: if not index.isValid(): return False diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py b/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py deleted file mode 100644 index b3a6ea45..00000000 --- a/python/mqt/syrec/simulation_view/workers/cancellable_base_worker.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -from __future__ import annotations - -import time -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Final, TypeVar - -from PyQt6 import QtCore - -from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification - -if TYPE_CHECKING: - from PyQt6 import QtWidgets - -T = TypeVar("T") - - -@dataclass(frozen=True) -class BatchTimestamps: - start: float - end: float - duration: float - - -class CancellableBaseWorker(QtCore.QObject): # type: ignore[misc] - batchCompleted = QtCore.pyqtSignal(float, object) # noqa: N815 - # While the cancellation operation is assumed to request the cancellation of the internal worker - # as well as the worker_thread, due to the Qt event loop the QThread.finished signal is received - # after the finished signal of the worker, i.e. the order of events in case of a cancellation or error will be: - # [Optionally error thrown in worker] -> Cancellation requested -> Finished -> QThread.finished - # - # This could lead to the worker attempting to perform a double shutdown of the worker/worker_thread, - # assuming that the cancel/error handler will request the worker shutdown, when the finished slot of the worker - # perform the shutdown of the worker. Thus we introduce an additional flag in the finished signal to perform a - # conditional shutdown of the worker in the slot that is connected to the finished signal. - finished = QtCore.pyqtSignal(bool) - failed = QtCore.pyqtSignal(Exception) - - def __init__(self, do_batches_require_ack: bool) -> None: - super().__init__() - - self.cancellation_requested = False - self.cancellation_flag_mutex = QtCore.QReadWriteLock() - self.batch_ack_mutex: QtCore.QMutex | None = QtCore.QMutex() if do_batches_require_ack else None - self.wait_on_batch_processed_acknowledgement_condition: QtCore.QWaitCondition | None = ( - QtCore.QWaitCondition() if do_batches_require_ack else None - ) - - def request_cancellation(self) -> None: - # Since the wait of the QWaitCondition can only be 'cancelled' by either a wakeX call or by providing a timeout value with the - # latter probably leading to a while-loop construct repeatedly performing temporary waits (until the timer elapses), the programmer - # needs to make sure that the cancellation operation will both set the cancellation flag as well as waking the QWaitCondition in a single - # operation (i.e. while locking the batch_ack_mutex) - if self.batch_ack_mutex is not None and self.wait_on_batch_processed_acknowledgement_condition: - with QtCore.QMutexLocker(self.batch_ack_mutex): - self.set_cancellation_requested_flag(True) - self.wait_on_batch_processed_acknowledgement_condition.wakeAll() - else: - self.set_cancellation_requested_flag(True) - - def ack_batch_processed(self) -> None: - if self.batch_ack_mutex is not None and self.wait_on_batch_processed_acknowledgement_condition is not None: - with QtCore.QMutexLocker(self.batch_ack_mutex): - self.wait_on_batch_processed_acknowledgement_condition.wakeAll() - - def is_cancellation_requested(self) -> bool: - cancellation_requested: bool = False - self.cancellation_flag_mutex.lockForRead() - cancellation_requested = self.cancellation_requested - self.cancellation_flag_mutex.unlock() - return cancellation_requested - - def set_cancellation_requested_flag(self, flag_value: bool) -> None: - self.cancellation_flag_mutex.lockForWrite() - self.cancellation_requested = flag_value - self.cancellation_flag_mutex.unlock() - - @staticmethod - def is_batch_data_list_of_expected_type( - batch_data: Any, expected_batch_element_type: type[T], parent_widget_for_error_notification: QtWidgets.QWidget - ) -> bool: - if not isinstance(batch_data, list): - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.WARNING, - message_box_parent=parent_widget_for_error_notification, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be a list of {expected_batch_element_type} but was actually {type(batch_data)}! This should not happen.", - is_cancellable=False, - ) - return False - - mismatched_elem_type: type | None = next( - (type(elem) for elem in batch_data if not isinstance(elem, expected_batch_element_type)), - None, - ) - if mismatched_elem_type is None: - # All elements of list match expected element type or list was empty - return True - - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.WARNING, - message_box_parent=parent_widget_for_error_notification, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be a list of {expected_batch_element_type} but was actually a list that contained an element of type {mismatched_elem_type}! This should not happen.", - is_cancellable=False, - ) - return False - - @staticmethod - def is_batch_data_of_type( - batch_data: Any, expected_batch_type: type[T], parent_widget_for_error_notification: QtWidgets.QWidget - ) -> bool: - if isinstance(batch_data, expected_batch_type): - return True - - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.WARNING, - message_box_parent=parent_widget_for_error_notification, - message_box_title="Cannot handle batch data", - message_box_content=f"Expected batch data to be of type {expected_batch_type} but was actually of type {type(batch_data)}! This should not happen.", - is_cancellable=False, - ) - return False - - @staticmethod - def _get_timestamp() -> float: - return time.perf_counter() - - @staticmethod - def _calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestamp: float) -> BatchTimestamps: - batch_end_timestamp: Final[float] = CancellableBaseWorker._get_timestamp() - batch_duration = batch_end_timestamp - batch_start_timestamp - return BatchTimestamps(batch_start_timestamp, batch_end_timestamp, batch_duration) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 748af08d..d1e59f83 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +import queue import re from dataclasses import dataclass from typing import TYPE_CHECKING, Any, Final @@ -17,13 +18,12 @@ from ...logger_utils import log_error_to_console from ..simulation_run_model import SimulationRunModel -from .cancellable_base_worker import CancellableBaseWorker +from .cancellable_worker_variants import CancellableProducerConsumerWorker if TYPE_CHECKING: - from collections.abc import Iterable from pathlib import Path - from .cancellable_base_worker import BatchTimestamps + from .cancellable_worker_variants import BatchTimestamps, QueueConfig @dataclass(frozen=True) @@ -32,81 +32,124 @@ class ExportedBatchData: skipped_sim_runs: int -class SimulationRunJsonExportWorker(CancellableBaseWorker): +class SimulationRunJsonExportWorker(CancellableProducerConsumerWorker[SimulationRunModel, ExportedBatchData]): def __init__( self, path_to_json_file: Path, associated_stringified_syrec_program: str, - simulation_runs_to_export: Iterable[SimulationRunModel], - export_batch_size: int, + worker_recv_queue_config: QueueConfig[SimulationRunModel | None], + worker_send_queue_config: QueueConfig[ExportedBatchData], ) -> None: - super().__init__(do_batches_require_ack=False) + super().__init__( + worker_send_queue_config=worker_send_queue_config, + worker_recv_queue_config=worker_recv_queue_config, + ) self.associated_stringified_syrec_program = associated_stringified_syrec_program self.path_to_json_file: Final[Path] = path_to_json_file - self.simulation_runs_to_export: Iterable[SimulationRunModel] = simulation_runs_to_export - self.export_batch_size: Final[int] = export_batch_size @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_export(self) -> None: + request_more_queue_size_threshold: Final[int] = int(self.recv_queue_batch_size * 0.2) + + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None + + n_remaining_sim_runs_in_batch_to_process: int = self.recv_queue_batch_size + n_skipped_sim_runs_in_batch: int = 0 + n_exported_sim_runs_in_batch: int = 0 + has_exported_first_batch: bool = False + has_reached_end_sentinel: bool = False + try: - SimulationRunJsonExportWorker._validate_parameters(self.export_batch_size) + self._assert_valid_user_provided_parameter_values() - n_generated_batches: int = 0 - batch_idx: int = 0 - n_skipped_sim_runs_in_batch: int = 0 - n_exported_sim_runs_in_batch: int = 0 + batch_start_timestamp = SimulationRunJsonExportWorker.get_timestamp() with self.path_to_json_file.open("w") as file: file.write( f'{{"inputCircuit":"{SimulationRunJsonExportWorker.convert_to_single_line_string(self.associated_stringified_syrec_program)}", "simulationRuns":[' ) - batch_start_timestamp: float = SimulationRunJsonExportWorker._get_timestamp() - batch_timestamps: BatchTimestamps | None = None - for sim_run in self.simulation_runs_to_export: - if self.is_cancellation_requested(): - break - if sim_run.expected_output_state is None: - n_skipped_sim_runs_in_batch += 1 - else: - if n_exported_sim_runs_in_batch > 0 or ( - n_exported_sim_runs_in_batch == 0 and n_generated_batches > 0 + while ( + not self.is_cancellation_requested() + and n_remaining_sim_runs_in_batch_to_process > 0 + and not has_reached_end_sentinel + ): + self._wait_on_cancellation_or_input_data() + + one_time_request_new_data_flag: bool = False + for _ in range(self.recv_queue_batch_size): + if self.is_cancellation_requested() or n_remaining_sim_runs_in_batch_to_process < 0: + break + + dequeued_sim_run_model: SimulationRunModel | None = None + try: + dequeued_sim_run_model = self.recv_queue.get(block=False, timeout=0.2) + except queue.Empty: + self.requestingData.emit() + break + + has_reached_end_sentinel = dequeued_sim_run_model is None + # We use an element that is None as the sentinel value of the receive queue (i.e. dequeueing None means that we have reached processed the last enqueued element from the sender) + if has_reached_end_sentinel: + break + + if ( + not one_time_request_new_data_flag + and self.recv_queue.qsize() < request_more_queue_size_threshold + ): + self.requestingData.emit() + # The sender could take some time to produce new data so we do not want to repeatedly trigger this process by emitting the associated signal + # but only notify the sender once in the current loop. This can still trigger multiple signal emits depending on how fast the sender enqueues elements + # in the receive queue but will at least limit the signal emits for the current number of remaining queue elements. + one_time_request_new_data_flag = True + + n_remaining_sim_runs_in_batch_to_process -= 1 + # The mypy type-checker does not seem to infer that the dequeued simulation run model should be not None at this point since we already covered + # the None case in our check for the sentinel value + assert dequeued_sim_run_model is not None + if dequeued_sim_run_model.expected_output_state is None: + n_skipped_sim_runs_in_batch += 1 + continue + + if SimulationRunJsonExportWorker._should_sim_run_export_delimiter_be_serialized( + has_exported_first_batch, n_exported_sim_runs_in_batch ): file.write(",") - file.write(json.dumps(sim_run, default=SimulationRunJsonExportWorker.serialize_to_json)) + + file.write( + json.dumps(dequeued_sim_run_model, default=SimulationRunJsonExportWorker.serialize_to_json) + ) n_exported_sim_runs_in_batch += 1 - batch_idx += 1 - if batch_idx == self.export_batch_size: - batch_timestamps = ( - SimulationRunJsonExportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( - batch_start_timestamp - ) + if self.is_cancellation_requested(): + break + + if n_remaining_sim_runs_in_batch_to_process > 0 and not has_reached_end_sentinel: + continue + + batch_timestamps = ( + SimulationRunJsonExportWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp ) - batch_start_timestamp = batch_timestamps.end - self.batchCompleted.emit( - batch_timestamps.duration, - ExportedBatchData(n_exported_sim_runs_in_batch, n_skipped_sim_runs_in_batch), + ) + batch_start_timestamp = batch_timestamps.end + self.batchCompleted.emit(batch_timestamps.duration) + self.send_queue.put_nowait( + ExportedBatchData( + exported_sim_runs=n_exported_sim_runs_in_batch, skipped_sim_runs=n_skipped_sim_runs_in_batch ) - batch_idx = 0 - n_skipped_sim_runs_in_batch = 0 - n_exported_sim_runs_in_batch = 0 - n_generated_batches += 1 + ) + + n_skipped_sim_runs_in_batch = 0 + n_exported_sim_runs_in_batch = 0 + has_exported_first_batch = True + n_remaining_sim_runs_in_batch_to_process = self.recv_queue_batch_size + # An error during during the serialization of the simulation runs to their .json representation will cause the content of the # exported to .json file to be invalid .json due to the simulation runs JSON array as well as the top level JSON object missing # their closing symbol. file.write("]}") - - if batch_idx > 0 and not self.is_cancellation_requested(): - batch_timestamps = ( - SimulationRunJsonExportWorker._calc_batch_duration_and_return_end_timestamp_in_seconds( - batch_start_timestamp - ) - ) - self.batchCompleted.emit( - batch_timestamps.duration, - ExportedBatchData(batch_idx - n_skipped_sim_runs_in_batch, n_skipped_sim_runs_in_batch), - ) self.finished.emit(self.is_cancellation_requested()) except Exception as error: error_msg: Final[str] = ( @@ -115,6 +158,12 @@ def start_export(self) -> None: log_error_to_console(error_msg) self.failed.emit(error) + @staticmethod + def _should_sim_run_export_delimiter_be_serialized( + has_exported_first_batch: bool, num_exported_elements_in_current_batch: int + ) -> bool: + return num_exported_elements_in_current_batch > 0 or has_exported_first_batch + @staticmethod def _validate_parameters(batch_size: int) -> None: if batch_size < 1: diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 215687fc..1de7b89a 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -52,7 +52,7 @@ def __init__( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: curr_sim_run_num: int = 0 - request_more_queue_size_threshold: Final[int] = int(self.send_queue_batch_size * 0.2) + request_more_queue_size_threshold: Final[int] = int(self.recv_queue_batch_size * 0.2) batch_start_timestamp: float = 0 batch_timestamps: BatchTimestamps | None = None From 4d04744c780b4a8a8464e725fe8049dc917ab685 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 1 Feb 2026 22:34:44 +0100 Subject: [PATCH 69/88] Moved QtSimulationRunModel property shared by all simulation run dialogs into base class and is now required to be passed as an constructor argument instead of when starting the dialog operation thus no longer being 'nullable' --- .../quantum_circuit_simulation_dialog.py | 26 ++++--- .../all_input_states_generator_dialog.py | 41 +++++------ .../dialogs/base_progress_dialog.py | 8 ++- .../dialogs/simulation_run_dialog.py | 31 ++++---- .../simulation_run_json_export_dialog.py | 23 +++--- .../simulation_run_json_import_dialog.py | 29 +++----- .../workers/cancellable_worker_variants.py | 4 ++ .../workers/simulation_run_worker.py | 72 +++++++++---------- 8 files changed, 112 insertions(+), 122 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index b0ef29b4..e202a4ac 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -475,12 +475,13 @@ def handle_sim_run_save_to_file_btn_click(self) -> None: if not filename: return - self.simulation_run_export_to_file_dialog = SimulationRunJsonExportDialog(self) + self.simulation_run_export_to_file_dialog = SimulationRunJsonExportDialog( + parent=self, shared_simulation_runs_model=self.simulation_runs_model + ) self.simulation_run_export_to_file_dialog.finished.connect(self.handle_sim_run_export_to_file_dialog_close) self.simulation_run_export_to_file_dialog.start_export( Path(filename), self.associated_stringified_syrec_program, - self.simulation_runs_model, self.simulation_runs_model.rowCount(QtCore.QModelIndex()), ) self.simulation_run_export_to_file_dialog.show() @@ -502,10 +503,12 @@ def handle_open_and_start_all_input_states_generator_dialog(self, input_state_si ) return - self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog(self) + self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog( + parent=self, shared_simulation_runs_model=self.simulation_runs_model + ) self.all_input_states_generator_dialog.finished.connect(self.handle_input_states_generator_dialog_close) self.all_input_states_generator_dialog.show() - self.all_input_states_generator_dialog.start_generation(self.simulation_runs_model, input_state_size) + self.all_input_states_generator_dialog.start_generation(input_state_size) @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_input_states_generator_dialog_close(self, result: int) -> None: @@ -707,12 +710,14 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma ) return - self.simulation_run_dialog = SimulationRunDialog(self) + self.simulation_run_dialog = SimulationRunDialog( + parent=self, + shared_simulation_runs_model=self.simulation_runs_model, + annotatable_quantum_computation=self.annotatable_quantum_computation, + ) self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) self.simulation_run_dialog.show() - self.simulation_run_dialog.start_simulations( - self.annotatable_quantum_computation, self.simulation_runs_model, stop_at_first_output_state_mismatch - ) + self.simulation_run_dialog.start_simulations(stop_at_first_output_state_mismatch) @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_runs_dialog_close(self, _: int) -> None: @@ -800,12 +805,13 @@ def open_import_from_file_dialog(self) -> None: return self.simulation_runs_model.delete_all_simulation_run_models() - self.simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog(self) + self.simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog( + parent=self, shared_simulation_runs_model=self.simulation_runs_model + ) self.simulation_run_import_from_file_dialog.finished.connect(self.handle_import_from_file_dialog_close) self.simulation_run_import_from_file_dialog.show() self.simulation_run_import_from_file_dialog.start_import( Path(selected_filename_lbl.text()), - self.simulation_runs_model, expected_input_state_size=self.annotatable_quantum_computation.num_data_qubits, ) diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 980c521e..312cd14c 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -32,13 +32,13 @@ class AllInputStatesGeneratorDialog(BaseProgressDialog[AllInputStatesGeneratorWorker]): - def __init__(self, parent: QtWidgets.QWidget) -> None: + def __init__(self, parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSimulationRunModel) -> None: super().__init__( parent, + shared_simulation_runs_model, dialog_title="Generating simulation runs...", optional_progress_bar_text_format="Generated %v out of %m input states", ) - self.shared_simulation_runs_model: QtSimulationRunModel | None = None self.worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() self.worker_send_queue_batch_size: int = 0 self.num_generated_input_states: int = 0 @@ -48,19 +48,15 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: def start_generation( self, - shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, ) -> None: - self.shared_simulation_runs_model = shared_simulation_runs_model - self.title_lbl.setText(f"Generating simulation runs with batch size {worker_send_queue_batch_size}!") - if worker_send_queue_batch_size < 1 or expected_input_state_size < 1: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Invalid input parameters detected", - message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) and expected input state size(value={expected_input_state_size}) to be a positive integer!", + message_box_content=f"Expected worker send queue batch size (value={worker_send_queue_batch_size}) and expected input state size(value={expected_input_state_size}) to be positive integers!", is_cancellable=False, ) super().reject() @@ -87,6 +83,8 @@ def start_generation( is_cancellable=False, ) + self.title_lbl.setText(f"Generating simulation runs with batch size {worker_send_queue_batch_size}!") + # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker_send_queue_batch_size = worker_send_queue_batch_size self.worker = AllInputStatesGeneratorWorker( @@ -142,11 +140,11 @@ def _handle_generated_input_state_batch(self, batch_generation_duration_in_secon n_dequeued_batch_elems: int = 0 try: - assert self.shared_simulation_runs_model is not None for _ in range(self.worker_send_queue_batch_size): self.shared_simulation_runs_model.add_simulation_run_model(self.worker_send_queue.get_nowait()) n_dequeued_batch_elems += 1 except queue.Empty: + # The last batch generated by the worker could contain less than the expected batch size elements thus an empty queue should not be treated as an error pass except Exception as sim_run_model_addition_err: self._handle_non_recoverable_error(sim_run_model_addition_err) @@ -158,12 +156,14 @@ def _handle_generated_input_state_batch(self, batch_generation_duration_in_secon if self.progress_bar is not None: self.progress_bar.setValue(self.num_generated_input_states) self.progress_info_text_lbl.setText("") + QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_input_state_generator_finished(self, was_cancellation_requested: bool) -> None: - self.progress_info_text_lbl.setText("Input state generator finished!") - log_info_to_console("Input state generator finished!") + info_msg: Final[str] = "Input state generator finished!" + self.progress_info_text_lbl.setText(info_msg) + log_info_to_console(info_msg) if self.progress_bar is not None: self.progress_bar.setVisible(False) @@ -195,7 +195,7 @@ def _handle_input_state_generation_cancel_button_click(self) -> bool: return True return False - def _handle_non_recoverable_error(self, err: Exception | None) -> None: + def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: self.progress_info_text_lbl.setText("") if err is not None: # We want to log the source of the error as close as possible to the origin of the actual error thus we need to skip a few stack frames @@ -203,23 +203,14 @@ def _handle_non_recoverable_error(self, err: Exception | None) -> None: # lowest level in the stacktrace): logger (std) -> logger_utils (custom) -> update_displayed_error_text (custom) -> the current function. self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) - if self.shared_simulation_runs_model is not None: - try: - self.shared_simulation_runs_model.delete_all_simulation_run_models() - except Exception: - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.ERROR, - message_box_parent=self, - message_box_title="Internal error!", - message_box_content="Failed to delete all simulation run models during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", - is_cancellable=False, - ) - else: + try: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + except Exception: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, - message_box_title="Internal state error!", - message_box_content="Shared simulation runs model was not initialized during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", + message_box_title="Internal error!", + message_box_content="Failed to delete all simulation run models during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", is_cancellable=False, ) diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index b268d1d2..f3ba669d 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -8,7 +8,7 @@ from __future__ import annotations -from typing import Any, Final, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar from PyQt6 import QtCore, QtGui, QtWidgets @@ -16,6 +16,9 @@ from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from ..workers.cancellable_worker_variants import CancellableProducerConsumerWorker, CancellableProducerWorker +if TYPE_CHECKING: + from ..simulation_run_model import QtSimulationRunModel + DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( "Total runtime [in seconds] (excluding model updates, internal waits): {total_runtime_in_seconds:f}" ) @@ -42,6 +45,7 @@ class BaseProgressDialog(QtWidgets.QDialog, Generic[WorkerType]): # type: ignor def __init__( self, parent: QtWidgets.QWidget, + shared_simulation_runs_model: QtSimulationRunModel, dialog_title: str, optional_progress_bar_text_format: str | None = None, create_default_layout: bool = True, @@ -52,6 +56,8 @@ def __init__( self.worker_thread: QtCore.QThread | None = None self.worker: WorkerType | None = None + self.shared_simulation_runs_model: QtSimulationRunModel = shared_simulation_runs_model + self.stop_processing_recv_batches: bool = False self.total_runtime_in_seconds: float = 0 diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 04547677..c286a1d0 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -41,34 +41,31 @@ ) -# TODO: Update base progress dialog to use new cancellable worker -# TODO: Rename cancellable worker to producer-consumer worker? -# TODO: Rework all other dialogs to use refactored base classes -# TODO: Move clarifying comments to all input state generator dialog/worker? class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): - def __init__(self, parent: QtWidgets.QWidget) -> None: + def __init__( + self, + parent: QtWidgets.QWidget, + shared_simulation_runs_model: QtSimulationRunModel, + annotatable_quantum_computation: syrec.annotatable_quantum_computation, + ) -> None: super().__init__( parent, + shared_simulation_runs_model, dialog_title="Executing simulation runs...", optional_progress_bar_text_format="Executed simulation run %v of %m", create_default_layout=False, user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) - self.annotatable_quantum_computation: syrec.annotatable_quantum_computation | None = None - self.shared_simulation_runs_model: QtSimulationRunModel | None = None + self.annotatable_quantum_computation: syrec.annotatable_quantum_computation = annotatable_quantum_computation self.stop_at_first_output_state_mismatch: bool = False self.num_executed_simulation_runs: int = 0 self.last_fetched_simulation_run_idx: int = 0 self.total_model_update_runtime_in_seconds: float = 0 self.sim_run_model_queue_batch_size: int = 0 - # Since we want to be able to prematurely cancel the long running operation blocking operations like get() or put() of the threading.queue - # cannot be used thus we do not need to constrain the size of the queue. self.sim_run_model_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() self.sim_run_result_queue_batch_size: int = 0 - # Since we want to be able to prematurely cancel the long running operation blocking operations like get() or put() of the threading.queue - # cannot be used thus we do not need to constrain the size of the queue. self.sim_run_result_queue: queue.SimpleQueue[SimulationRunResult] = queue.SimpleQueue() self.dialog_button_box.accepted.connect(self.accept) @@ -106,19 +103,15 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.total_model_update_runtime_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.total_model_update_runtime_lbl) - # layout.addStretch() layout.addWidget(self.dialog_button_box) self.setLayout(layout) def start_simulations( self, - annotatable_quantum_computation: syrec.annotatable_quantum_computation, - shared_simulation_run_model: QtSimulationRunModel, stop_at_first_output_state_mismatch: bool, sim_run_model_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, sim_run_result_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: - self.annotatable_quantum_computation = annotatable_quantum_computation expected_input_state_size: Final[int] = self.annotatable_quantum_computation.num_data_qubits if sim_run_model_queue_batch_size < 1 or sim_run_result_queue_batch_size < 1 or expected_input_state_size < 1: show_and_request_ok_in_optionally_cancellable_notification( @@ -134,14 +127,15 @@ def start_simulations( self.sim_run_model_queue_batch_size = sim_run_model_queue_batch_size self.sim_run_result_queue_batch_size = sim_run_result_queue_batch_size - self.shared_simulation_runs_model = shared_simulation_run_model self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) self.stop_at_first_output_state_mismatch = stop_at_first_output_state_mismatch log_info_to_console( f"Starting execution of simulation runs, stopping after first output mismatch flag is set to {self.stop_at_first_output_state_mismatch}" ) - expected_total_num_simulation_runs: Final[int] = shared_simulation_run_model.rowCount(QtCore.QModelIndex()) + expected_total_num_simulation_runs: Final[int] = self.shared_simulation_runs_model.rowCount( + QtCore.QModelIndex() + ) self.title_lbl.setText( f"Executing {expected_total_num_simulation_runs} simulation runs with batch sizes (Sim. run model queue={sim_run_model_queue_batch_size}, Sim. run result queue={sim_run_result_queue_batch_size})!" ) @@ -268,7 +262,6 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ batch_results_processing_start_timestamp: Final[float] = SimulationRunWorker.get_timestamp() try: - assert self.shared_simulation_runs_model is not None for _ in range(self.sim_run_result_queue_batch_size): simulation_run_result: SimulationRunResult = self.sim_run_result_queue.get_nowait() to_be_updated_sim_run_number = simulation_run_result.simulation_run_number @@ -280,6 +273,7 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ ) n_received_sim_run_execution_results += 1 except queue.Empty: + # The last batch generated by the worker could contain less than the expected batch size elements thus an empty queue should not be treated as an error pass except Exception as err: self._handle_non_recoverable_error( @@ -305,7 +299,6 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _enqueue_next_simulation_runs(self) -> None: try: - assert self.shared_simulation_runs_model is not None for i in range( self.last_fetched_simulation_run_idx, self.last_fetched_simulation_run_idx + self.sim_run_model_queue_batch_size, diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index cbd3bdb2..41ef8c65 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -37,9 +37,10 @@ class SimulationRunJsonExportDialog(BaseProgressDialog[SimulationRunJsonExportWorker]): - def __init__(self, parent: QtWidgets.QWidget) -> None: + def __init__(self, parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSimulationRunModel) -> None: super().__init__( parent, + shared_simulation_runs_model, dialog_title="Exporting simulation runs...", optional_progress_bar_text_format="Processed simulation run %v of %m", create_default_layout=False, @@ -52,7 +53,6 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.worker_recv_queue_batch_size: int = 0 self.worker_send_queue: queue.SimpleQueue[ExportedBatchData] = queue.SimpleQueue() self.worker_recv_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() - self.shared_simulation_runs_model: QtSimulationRunModel | None = None self.dialog_button_box.accepted.connect(self.accept) self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) @@ -80,7 +80,6 @@ def start_export( self, export_location: Path, associated_stringified_syrec_program: str, - shared_simulation_run_model: QtSimulationRunModel, num_sim_runs_to_export: int, worker_recv_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: @@ -116,7 +115,6 @@ def start_export( is_cancellable=False, ) - self.shared_simulation_runs_model = shared_simulation_run_model self.worker_recv_queue_batch_size = worker_recv_queue_batch_size # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self.worker = SimulationRunJsonExportWorker( @@ -174,7 +172,14 @@ def _handle_batch_exported(self, batch_generation_duration_in_seconds: float) -> try: batch_data = self.worker_send_queue.get_nowait() except queue.Empty: - # TODO: This should not happen + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.WARNING, + message_box_parent=self, + message_box_title="Encountered empty queue!", + message_box_content="The send queue of the simulation run export worker should at least contain one element (since only a single entry per batch is created) but the queue was empty.", + is_cancellable=False, + log_contents=True, + ) QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) return except Exception as err: @@ -202,7 +207,6 @@ def _handle_batch_exported(self, batch_generation_duration_in_seconds: float) -> @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _enqueue_next_simulation_runs_to_export(self) -> None: try: - assert self.shared_simulation_runs_model is not None for i in range( self.last_exported_sim_run_num, self.last_exported_sim_run_num + self.worker_recv_queue_batch_size ): @@ -215,11 +219,8 @@ def _enqueue_next_simulation_runs_to_export(self) -> None: break QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) except Exception as err: - # TODO: Better error self._handle_non_recoverable_error( - ValueError( - f"Error during enqueue of new simulation runs, reason: {SimulationRunJsonExportDialog._stringify_error(err)}" - ) + f"Error during enqueue of new simulation runs, reason: {SimulationRunJsonExportDialog._stringify_error(err)}" ) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] @@ -258,7 +259,7 @@ def _handle_export_to_file_cancel_button_click(self) -> bool: return True return False - def _handle_non_recoverable_error(self, err: Exception | None) -> None: + def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: self.progress_info_text_lbl.setText("") if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index d7d3f58d..88110dec 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -34,15 +34,15 @@ class SimulationRunJsonImportDialog(BaseProgressDialog[SimulationRunJsonImportWorker]): - def __init__(self, parent: QtWidgets.QWidget) -> None: + def __init__(self, parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSimulationRunModel) -> None: super().__init__( parent, + shared_simulation_runs_model, dialog_title="Importing simulation runs...", optional_progress_bar_text_format=None, create_default_layout=False, ) self.num_imported_simulation_runs: int = 0 - self.shared_simulation_runs_model: QtSimulationRunModel | None = None self.worker_send_queue_batch_size: int = 0 self.worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() @@ -74,11 +74,9 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: def start_import( self, path_to_json_file: Path, - shared_simulation_runs_model: QtSimulationRunModel, expected_input_state_size: int, worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, ) -> None: - self.shared_simulation_runs_model = shared_simulation_runs_model self.title_lbl.setText( f"Importing simulation runs from .json file with batch size {worker_send_queue_batch_size}!" ) @@ -174,11 +172,11 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f n_dequeued_batch_elems: int = 0 try: - assert self.shared_simulation_runs_model is not None for _ in range(self.worker_send_queue_batch_size): self.shared_simulation_runs_model.add_simulation_run_model(self.worker_send_queue.get_nowait()) n_dequeued_batch_elems += 1 except queue.Empty: + # The last batch generated by the worker could contain less than the expected batch size elements thus an empty queue should not be treated as an error pass except Exception as sim_run_model_addition_err: self._handle_non_recoverable_error(sim_run_model_addition_err) @@ -229,28 +227,19 @@ def _handle_import_from_file_cancel_button_click(self) -> bool: return True return False - def _handle_non_recoverable_error(self, err: Exception | None) -> None: + def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: self.progress_info_text_lbl.setText("") if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) - if self.shared_simulation_runs_model is not None: - try: - self.shared_simulation_runs_model.delete_all_simulation_run_models() - except Exception: - show_and_request_ok_in_optionally_cancellable_notification( - message_box_type=MessageBoxType.ERROR, - message_box_parent=self, - message_box_title="Internal error!", - message_box_content="Failed to delete all simulation run models during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", - is_cancellable=False, - ) - else: + try: + self.shared_simulation_runs_model.delete_all_simulation_run_models() + except Exception: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, - message_box_title="Internal state error!", - message_box_content="Shared simulation runs model was not initialized during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", + message_box_title="Internal error!", + message_box_content="Failed to delete all simulation run models during handling of non-recoverable error!\nThis should not happen, cancelling long running operation!", is_cancellable=False, ) diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py index 5b67ad40..ce00d4ee 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py @@ -50,6 +50,8 @@ class CancellableProducerWorker(QtCore.QObject, Generic[SendQueueElemType]): # def __init__(self, worker_send_queue_config: QueueConfig[SendQueueElemType]) -> None: super().__init__() self.cancellation_requested_flag: threading.Event = threading.Event() + # The requirement to cancel the long running operation performed by the worker "forces" us to use the non-blocking get_nowait(...) and put_nowait(...) functions of the unbounded + # queue.SimpleQueue container. Correctly handling the expected batch sizes is the responsibility of the user of this unbounded queue. self.send_queue: queue.SimpleQueue[SendQueueElemType] = worker_send_queue_config.queue_instance self.send_queue_batch_size: int = worker_send_queue_config.queue_batch_size self.cancelled_or_continue_processing_condition: threading.Condition = threading.Condition() @@ -106,6 +108,8 @@ def __init__( worker_recv_queue_config: QueueConfig[RecvQueueElemType | None], ) -> None: super().__init__(worker_send_queue_config) + # The requirement to cancel the long running operation performed by the worker "forces" us to use the non-blocking get_nowait(...) and put_nowait(...) functions of the unbounded + # queue.SimpleQueue container. Correctly handling the expected batch sizes is the responsibility of the user of this unbounded queue. self.recv_queue: queue.SimpleQueue[RecvQueueElemType | None] = worker_recv_queue_config.queue_instance self.recv_queue_batch_size: int = worker_recv_queue_config.queue_batch_size diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 1de7b89a..cbca5671 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -76,47 +76,47 @@ def start_simulations(self) -> None: ): break + dequeued_sim_run_model: SimulationRunModel | None = None try: - dequeued_sim_run_model: SimulationRunModel | None = self.recv_queue.get( - block=False, timeout=0.2 - ) - has_reached_end_sentinel = dequeued_sim_run_model is None - # We use an element that is None as the sentinel value of the receive queue (i.e. dequeueing None means that we have reached processed the last enqueued element from the sender) - if has_reached_end_sentinel: - break - - if ( - not one_time_request_new_data_flag - and self.recv_queue.qsize() < request_more_queue_size_threshold - ): - self.requestingData.emit() - # The sender could take some time to produce new data so we do not want to repeatedly trigger this process by emitting the associated signal - # but only notify the sender once in the current loop. This can still trigger multiple signal emits depending on how fast the sender enqueues elements - # in the receive queue but will at least limit the signal emits for the current number of remaining queue elements. - one_time_request_new_data_flag = True - - # The mypy type-checker does not seem to infer that the dequeued simulation run model should be not None at this point since we already covered - # the None case in our check for the sentinel value - assert dequeued_sim_run_model is not None - sim_run_execution_result: SimulationRunResult = ( - SimulationRunWorker._perform_single_sim_run_execution( - self.annotatable_quantum_computation, - curr_sim_run_num, - dequeued_sim_run_model.input_state, - dequeued_sim_run_model.expected_output_state, - ) - ) - self.send_queue.put_nowait(sim_run_execution_result) - found_outputs_mismatch |= ( - sim_run_execution_result.do_expected_and_actual_outputs_match is not None - and not sim_run_execution_result.do_expected_and_actual_outputs_match - ) - n_remaining_batch_elems_to_generate -= 1 - curr_sim_run_num += 1 + dequeued_sim_run_model = self.recv_queue.get(block=False, timeout=0.2) except queue.Empty: self.requestingData.emit() break + has_reached_end_sentinel = dequeued_sim_run_model is None + # We use an element that is None as the sentinel value of the receive queue (i.e. dequeueing None means that we have reached processed the last enqueued element from the sender) + if has_reached_end_sentinel: + break + + if ( + not one_time_request_new_data_flag + and self.recv_queue.qsize() < request_more_queue_size_threshold + ): + self.requestingData.emit() + # The sender could take some time to produce new data so we do not want to repeatedly trigger this process by emitting the associated signal + # but only notify the sender once in the current loop. This can still trigger multiple signal emits depending on how fast the sender enqueues elements + # in the receive queue but will at least limit the signal emits for the current number of remaining queue elements. + one_time_request_new_data_flag = True + + # The mypy type-checker does not seem to infer that the dequeued simulation run model should be not None at this point since we already covered + # the None case in our check for the sentinel value + assert dequeued_sim_run_model is not None + sim_run_execution_result: SimulationRunResult = ( + SimulationRunWorker._perform_single_sim_run_execution( + self.annotatable_quantum_computation, + curr_sim_run_num, + dequeued_sim_run_model.input_state, + dequeued_sim_run_model.expected_output_state, + ) + ) + self.send_queue.put_nowait(sim_run_execution_result) + found_outputs_mismatch |= ( + sim_run_execution_result.do_expected_and_actual_outputs_match is not None + and not sim_run_execution_result.do_expected_and_actual_outputs_match + ) + n_remaining_batch_elems_to_generate -= 1 + curr_sim_run_num += 1 + if self.is_cancellation_requested(): break From bd3b1af66c30bcaa472a6f46950905cf28a66dc6 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Sun, 1 Feb 2026 23:02:37 +0100 Subject: [PATCH 70/88] Added import failure and default fallback for ijson package --- python/mqt/syrec/logger_utils.py | 1 + .../simulation_run_json_import_worker.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/python/mqt/syrec/logger_utils.py b/python/mqt/syrec/logger_utils.py index 2a54c00b..a925ee5b 100644 --- a/python/mqt/syrec/logger_utils.py +++ b/python/mqt/syrec/logger_utils.py @@ -18,6 +18,7 @@ def configure_default_console_logger() -> None: level=logging.DEBUG, format="%(asctime)s-%(levelname)s-[%(filename)s:%(lineno)s - %(funcName)20s()]-%(message)s", datefmt="%H:%M:%S", + force=True, ) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 28bf8e81..42ce3757 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -16,14 +16,23 @@ else: from typing_extensions import override -# The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) -# TODO: Try catch and fallback incase that import fails -import ijson.backends.yajl2_c as ijson +from ...logger_utils import configure_default_console_logger, log_error_to_console, log_info_to_console + +try: + # The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) + # Requires that the pre-built python wheel for the yajl c-extension exists for the platform that executed this python script. + # This should be the case for the majority of all platforms. + import ijson.backends.yajl2_c as ijson +except ImportError: + configure_default_console_logger() + log_error_to_console("yajl2 C-extension not available, falling back to pure-Python parser!") + # pure-Python fallback is always present but might not be the fastest + import ijson + from PyQt6 import QtCore from mqt import syrec -from ...logger_utils import log_error_to_console, log_info_to_console from ..simulation_run_model import SimulationRunModel from .cancellable_worker_variants import CancellableProducerWorker From 81aa987a4029312baea7b91c5deb68fd8cc6fe71 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl Date: Tue, 3 Feb 2026 23:09:26 +0100 Subject: [PATCH 71/88] Added some doc strings and refactored some shared functionality in simulation run model --- .../dialogs/base_progress_dialog.py | 14 +++- .../dialogs/simulation_run_dialog.py | 1 - .../simulation_view/simulation_run_model.py | 60 ++++++++------ .../all_input_states_generator_worker.py | 6 +- .../workers/cancellable_worker_variants.py | 83 ++++++++++++++----- .../simulation_run_json_export_worker.py | 8 +- .../simulation_run_json_import_worker.py | 6 +- .../workers/simulation_run_worker.py | 8 +- 8 files changed, 125 insertions(+), 61 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index f3ba669d..79a165c8 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -36,10 +36,22 @@ class BaseProgressDialog(QtWidgets.QDialog, Generic[WorkerType]): # type: ignore[misc] - """Base class for progress dialogs with worker thread management. + """ + Base class for progress dialogs with worker thread management. Note: Instances of this dialog are designed to be used only once. Create a new instance for each operation rather than reusing the same dialog. + + If not specified, the dialog will be opened as a modal dialog that is centered over the parent window with a default widget layout defined as (read from top to bottom): + + <PROGRESS_INFO> + <ERROR_TEXT> + <OPTIONAL_PROGRESS_BAR> + <TOTAL_RUNTIME_INFO> + <OPEN_BTN> <CLOSE_BTN> + + Inputs: + WorkerType: Defines the type of worker employed by the dialog to perform its long running operation. """ def __init__( diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index c286a1d0..b910e52d 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -88,7 +88,6 @@ def __init__( self.simulation_runs_list_view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) self.simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) - # self.simulation_runs_list_view.selectionModel().selectionChanged.connect(self.handle_simulation_run_selection_change) simulation_runs_list_scrollarea = QtWidgets.QScrollArea() simulation_runs_list_scrollarea.setAutoFillBackground(False) diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 36f376b8..de074204 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -55,14 +55,12 @@ def __init__( actual_output_state: syrec.n_bit_values_container | None = None, create_new_n_bit_values_container_instances: bool = False, ) -> None: - if expected_output_state is not None and input_state.size() != expected_output_state.size(): - msg = f"Expected output state size (n_qubits = {expected_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" - log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) - raise ValueError(msg) - if actual_output_state is not None and input_state.size() != actual_output_state.size(): - msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match input state size (n_qubits = {input_state.size()})" - log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) - raise ValueError(msg) + SimulationRunModel._assert_n_bit_value_container_sizes_match( + input_state, "input state", expected_output_state, "expected output state" + ) + SimulationRunModel._assert_n_bit_value_container_sizes_match( + input_state, "input state", actual_output_state, "actual output state" + ) if not create_new_n_bit_values_container_instances: self.input_state = input_state @@ -102,10 +100,9 @@ def set_result_of_simulation_execution( do_expected_and_actual_output_states_match: bool | None, execution_runtime_in_ms: float, ) -> None: - if actual_output_state.size() != self.input_state.size(): - msg = f"Actual output state size (n_qubits = {actual_output_state.size()}) did not match input state size (n_qubits = {self.input_state.size()})" - log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) - raise ValueError(msg) + SimulationRunModel._assert_n_bit_value_container_sizes_match( + self.input_state, "input state", actual_output_state, "actual output state" + ) if execution_runtime_in_ms < 0: msg = f"Invalid execution runtime value {execution_runtime_in_ms}" log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) @@ -136,15 +133,12 @@ def update_user_editable_data( edited_input_state: syrec.n_bit_values_container, edited_expected_output_state: syrec.n_bit_values_container | None, ) -> None: - if self.input_state.size() != edited_input_state.size(): - msg = f"Updated input state size state size (n_qubits = {edited_input_state.size()}) did not match current input state size (n_qubits = {self.input_state.size()})" - log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) - raise ValueError(msg) - - if edited_expected_output_state is not None and edited_expected_output_state.size() != self.input_state.size(): - msg = f"Expected output state size (n_qubits = {edited_expected_output_state.size()}) did not match input state size (n_qubits = {self.input_state.size()})" - log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) - raise ValueError(msg) + SimulationRunModel._assert_n_bit_value_container_sizes_match( + self.input_state, "input state", edited_input_state, "edited input state" + ) + SimulationRunModel._assert_n_bit_value_container_sizes_match( + self.input_state, "input state", edited_expected_output_state, "edited expected output state" + ) did_input_state_change: bool = False for i in range(self.input_state.size()): @@ -175,11 +169,9 @@ def do_output_states_match( if expected_output_state is None: return None - if expected_output_state.size() != actual_output_state.size(): - msg = f"Expected output state to have {expected_output_state.size()} qubits but actual output state contained {actual_output_state.size()} qubits!" - log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) - raise ValueError(msg) - + SimulationRunModel._assert_n_bit_value_container_sizes_match( + expected_output_state, "expected output state", actual_output_state, "actual output state" + ) return all( actual_output_state.test(i) == expected_output_state.test(i) for i in range(actual_output_state.size()) ) @@ -194,6 +186,21 @@ def _update_n_bit_values_container_qubit_value( n_bit_values_container.set(qubit, new_qubit_value) return True + @staticmethod + def _assert_n_bit_value_container_sizes_match( + expected_container: syrec.n_bit_values_container, + expected_container_name: str, + optional_actual_container: syrec.n_bit_values_container | None, + actual_container_name: str, + ) -> None: + if optional_actual_container is None: + return + + if expected_container.size() != optional_actual_container.size(): + msg = f"{expected_container_name} to have {expected_container.size()} qubits but {actual_container_name} contained {optional_actual_container.size()} qubits!" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=2) + raise ValueError(msg) + # Example delegate: https://stackoverflow.com/questions/53105343/is-it-possible-to-add-a-custom-widget-into-a-qlistview class QtSimulationRunModel(QtCore.QAbstractListModel): # type: ignore[misc] @@ -284,7 +291,6 @@ def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: return self.simulation_run_models[index] return None - # TODO: Should we perform a validation here? def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> None: n_simulation_runs: Final[int] = self.rowCount(QtCore.QModelIndex()) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index 8d45f05c..92e494be 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -54,7 +54,9 @@ def start_generation(self) -> None: self._wait_on_cancellation_or_input_data() for integer_encoding_input_state in range( integer_encoding_first_input_state_of_batch, - min(integer_encoding_first_input_state_of_batch + self.send_queue_batch_size, n_states_to_generate), + min( + integer_encoding_first_input_state_of_batch + self._send_queue_batch_size, n_states_to_generate + ), ): if self.is_cancellation_requested(): break @@ -70,7 +72,7 @@ def start_generation(self) -> None: # The addition operation will produce the wrong integer encoding the next input state in case of an cancellation request but this is ok since # the cancellation also stops the generation of further input states. - integer_encoding_first_input_state_of_batch += self.send_queue_batch_size + integer_encoding_first_input_state_of_batch += self._send_queue_batch_size batch_timestamps = ( AllInputStatesGeneratorWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( diff --git a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py index ce00d4ee..449a26fc 100644 --- a/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py +++ b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py @@ -43,47 +43,71 @@ class QueueConfig(Generic[QueueElemType]): class CancellableProducerWorker(QtCore.QObject, Generic[SendQueueElemType]): # type: ignore[misc] + """ + Defines a worker executing a long running operation producing batches of elements. + + While executing the long running operation the worker is able to emit the following signals: + - finished(bool): Worker has finished its long running operation without errors, the boolean argument defines whether cancellation of the worker was requested before it complete normally. + - failed(Exception): An exception occurred during the long running operation. + - batchCompleted(float): The worker has produced a new batch of items, the runtime (in seconds) to produce a new batch is passed as the argument of the signal. + + Inputs: + SendQueueElemType: The workers send queue element type. + + Attributes: + send_queue: A thread-safe FIFO queue storing the elements produced by the worker. The queue is expected to be unbounded. + """ + finished = QtCore.pyqtSignal(bool) failed = QtCore.pyqtSignal(Exception) batchCompleted = QtCore.pyqtSignal(float) # noqa: N815 def __init__(self, worker_send_queue_config: QueueConfig[SendQueueElemType]) -> None: super().__init__() - self.cancellation_requested_flag: threading.Event = threading.Event() # The requirement to cancel the long running operation performed by the worker "forces" us to use the non-blocking get_nowait(...) and put_nowait(...) functions of the unbounded # queue.SimpleQueue container. Correctly handling the expected batch sizes is the responsibility of the user of this unbounded queue. self.send_queue: queue.SimpleQueue[SendQueueElemType] = worker_send_queue_config.queue_instance - self.send_queue_batch_size: int = worker_send_queue_config.queue_batch_size - self.cancelled_or_continue_processing_condition: threading.Condition = threading.Condition() + # A thread-safe boolean flag to enable cooperative cancellation of the long running operation. + self._cancellation_requested_flag: threading.Event = threading.Event() + # Defines after how many elements the worker will emit the batchCompleted signal. + self._send_queue_batch_size: int = worker_send_queue_config.queue_batch_size + # A condition variable usable to control the production of new elements the production of new elements in the worker by notifying. + self._cancelled_or_continue_processing_condition: threading.Condition = threading.Condition() def notify_to_continue_processing(self) -> None: """Notify the producer to continue producing new elements. This can also be used to rate-limit the producer to only emit new batches when the consumer is ready.""" - with self.cancelled_or_continue_processing_condition: - self.cancelled_or_continue_processing_condition.notify() + with self._cancelled_or_continue_processing_condition: + self._cancelled_or_continue_processing_condition.notify() def request_cancellation(self) -> None: """Request a cancellation of the long running producer operation in a thread-safe manner.""" - self.cancellation_requested_flag.set() + self._cancellation_requested_flag.set() self.notify_to_continue_processing() def is_cancellation_requested(self) -> bool: """Check whether cancellation of the long running operation is requested in a thread-safe manner.""" - return self.cancellation_requested_flag.is_set() + return self._cancellation_requested_flag.is_set() def _can_continue_processing_or_is_cancellation_requested(self) -> bool: - """Check whether the consumer has dequeued all elements from the producer queue (i.e. this worker) or if cancellation was requested""" + """Check whether the consumer has dequeued all elements from the producer queue (i.e. this worker) or if cancellation was requested.""" return self.send_queue.empty() or self.is_cancellation_requested() def _wait_on_cancellation_or_input_data(self) -> None: - """Block the caller of this function until either cancellation is requested or when the consumer has dequeued all items from the producer queue (i.e. this worker)""" - with self.cancelled_or_continue_processing_condition: - self.cancelled_or_continue_processing_condition.wait_for( + """Blocks until either cancellation is requested or when the send queue is empty.""" + with self._cancelled_or_continue_processing_condition: + self._cancelled_or_continue_processing_condition.wait_for( self._can_continue_processing_or_is_cancellation_requested ) def _assert_valid_user_provided_parameter_values(self) -> None: - if self.send_queue_batch_size < 1: - msg = f"Send queue batch size must be larger than 0 but was actually {self.send_queue_batch_size}!" + """ + Validate the user-provided worker configuration parameters. + + Raises: + ValueError: An invalid value for the send queue batch size was passed. + """ + if self._send_queue_batch_size < 1: + msg = f"Send queue batch size must be larger than 0 but was actually {self._send_queue_batch_size}!" raise ValueError(msg) @staticmethod @@ -100,6 +124,20 @@ def calc_batch_duration_and_return_end_timestamp_in_seconds(batch_start_timestam class CancellableProducerConsumerWorker( CancellableProducerWorker[SendQueueElemType], Generic[RecvQueueElemType, SendQueueElemType] ): + """ + Defines a worker executing a long running operation that both consumes and produces elements. + + While executing the long running operation the worker is able to emit the following signals in addition to the signals inherited from its base class: + - requestingData: Emitted when the worker is about to run out of elements to consume. + + Inputs: + RecvQueueElemType: The workers receive queue element type. + SendQueueElemType: The workers send queue element type. + + Attributes: + recv_queue: A thread-safe FIFO queue used by the worker to fetch new elements to consume from. The queue is expected to be unbounded. + """ + requestingData = QtCore.pyqtSignal() # noqa: N815 def __init__( @@ -111,7 +149,8 @@ def __init__( # The requirement to cancel the long running operation performed by the worker "forces" us to use the non-blocking get_nowait(...) and put_nowait(...) functions of the unbounded # queue.SimpleQueue container. Correctly handling the expected batch sizes is the responsibility of the user of this unbounded queue. self.recv_queue: queue.SimpleQueue[RecvQueueElemType | None] = worker_recv_queue_config.queue_instance - self.recv_queue_batch_size: int = worker_recv_queue_config.queue_batch_size + # Defines how many elements the worker will consumer per batch. + self._recv_queue_batch_size: int = worker_recv_queue_config.queue_batch_size @override def _can_continue_processing_or_is_cancellation_requested(self) -> bool: @@ -120,15 +159,21 @@ def _can_continue_processing_or_is_cancellation_requested(self) -> bool: @override def _wait_on_cancellation_or_input_data(self) -> None: - """Block the caller of this function until either new elements in the receive queue exist or cancelation was requested""" - with self.cancelled_or_continue_processing_condition: - self.cancelled_or_continue_processing_condition.wait_for( + """Blocks until either new elements in the receive queue exist or cancelation was requested""" + with self._cancelled_or_continue_processing_condition: + self._cancelled_or_continue_processing_condition.wait_for( self._can_continue_processing_or_is_cancellation_requested ) @override def _assert_valid_user_provided_parameter_values(self) -> None: + """ + Validate the user-provided worker configuration parameters + + Raises: + ValueError: An invalid value for the receive queue batch size was passed or validation of the base class parameters failed. + """ super()._assert_valid_user_provided_parameter_values() - if self.recv_queue_batch_size < 1: - msg = f"Receive queue batch size must be larger than 0 but was actually {self.recv_queue_batch_size}!" + if self._recv_queue_batch_size < 1: + msg = f"Receive queue batch size must be larger than 0 but was actually {self._recv_queue_batch_size}!" raise ValueError(msg) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index d1e59f83..b2ceb5ce 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -50,12 +50,12 @@ def __init__( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_export(self) -> None: - request_more_queue_size_threshold: Final[int] = int(self.recv_queue_batch_size * 0.2) + request_more_queue_size_threshold: Final[int] = int(self._recv_queue_batch_size * 0.2) batch_start_timestamp: float = 0 batch_timestamps: BatchTimestamps | None = None - n_remaining_sim_runs_in_batch_to_process: int = self.recv_queue_batch_size + n_remaining_sim_runs_in_batch_to_process: int = self._recv_queue_batch_size n_skipped_sim_runs_in_batch: int = 0 n_exported_sim_runs_in_batch: int = 0 has_exported_first_batch: bool = False @@ -78,7 +78,7 @@ def start_export(self) -> None: self._wait_on_cancellation_or_input_data() one_time_request_new_data_flag: bool = False - for _ in range(self.recv_queue_batch_size): + for _ in range(self._recv_queue_batch_size): if self.is_cancellation_requested() or n_remaining_sim_runs_in_batch_to_process < 0: break @@ -144,7 +144,7 @@ def start_export(self) -> None: n_skipped_sim_runs_in_batch = 0 n_exported_sim_runs_in_batch = 0 has_exported_first_batch = True - n_remaining_sim_runs_in_batch_to_process = self.recv_queue_batch_size + n_remaining_sim_runs_in_batch_to_process = self._recv_queue_batch_size # An error during during the serialization of the simulation runs to their .json representation will cause the content of the # exported to .json file to be invalid .json due to the simulation runs JSON array as well as the top level JSON object missing diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 42ce3757..455a8e21 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -65,7 +65,7 @@ def start_import(self) -> None: try: self._assert_valid_user_provided_parameter_values() - n_remaining_input_states_to_import_in_batch: int = self.send_queue_batch_size + n_remaining_input_states_to_import_in_batch: int = self._send_queue_batch_size # Reading bytes instead of strings leads to better parser performance with self.path_to_json_file.open("rb") as file: # The json parser starts at the first element matching the prefix which in our case starts at an element with key 'simulationRuns' that is expected @@ -103,12 +103,12 @@ def start_import(self) -> None: self.batchCompleted.emit(batch_timestamps.duration) batch_start_timestamp = batch_timestamps.end - n_remaining_input_states_to_import_in_batch = self.send_queue_batch_size + n_remaining_input_states_to_import_in_batch = self._send_queue_batch_size self._wait_on_cancellation_or_input_data() # If we reached the end of the input .json file without reaching our batch threshold # emit the current enqueued elements to the consumer. - if n_remaining_input_states_to_import_in_batch < self.send_queue_batch_size: + if n_remaining_input_states_to_import_in_batch < self._send_queue_batch_size: batch_timestamps = ( SimulationRunJsonImportWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index cbca5671..629f35df 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -52,7 +52,7 @@ def __init__( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: curr_sim_run_num: int = 0 - request_more_queue_size_threshold: Final[int] = int(self.recv_queue_batch_size * 0.2) + request_more_queue_size_threshold: Final[int] = int(self._recv_queue_batch_size * 0.2) batch_start_timestamp: float = 0 batch_timestamps: BatchTimestamps | None = None @@ -63,13 +63,13 @@ def start_simulations(self) -> None: try: self._assert_valid_user_provided_parameter_values() batch_start_timestamp = SimulationRunWorker.get_timestamp() - n_remaining_batch_elems_to_generate: int = self.send_queue_batch_size + n_remaining_batch_elems_to_generate: int = self._send_queue_batch_size while self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): self._wait_on_cancellation_or_input_data() one_time_request_new_data_flag: bool = False - for _ in range(self.send_queue_batch_size): + for _ in range(self._send_queue_batch_size): if ( not self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel) or n_remaining_batch_elems_to_generate < 0 @@ -126,7 +126,7 @@ def start_simulations(self) -> None: # in the processing queue continue - n_remaining_batch_elems_to_generate = self.send_queue_batch_size + n_remaining_batch_elems_to_generate = self._send_queue_batch_size batch_timestamps = SimulationRunWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( batch_start_timestamp ) From 9600082d92ded692ce6d879fd4b0e8a30500f8bf Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 03:00:45 +0100 Subject: [PATCH 72/88] Simulation run execution mode can now be selected via dropdown. Execution of single simulation run is now possible. --- .../quantum_circuit_simulation_dialog.py | 279 ++++++++++++++---- .../dialogs/simulation_run_dialog.py | 69 ++++- .../simulation_view/simulation_run_model.py | 13 +- .../workers/simulation_run_worker.py | 22 +- 4 files changed, 321 insertions(+), 62 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index e202a4ac..6e23b741 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -10,6 +10,7 @@ import re import sys +from enum import Enum from pathlib import Path from typing import Final, cast @@ -18,6 +19,11 @@ else: from typing_extensions import override +if sys.version_info >= (3, 11): + from typing import assert_never +else: + from typing_extensions import assert_never + from PyQt6 import QtCore, QtGui, QtWidgets from mqt import syrec @@ -45,13 +51,22 @@ EDIT_SIM_RUN_BTN_NAME: Final[str] = "edit_sim_run_btn" DELETE_SIM_RUN_BTN_NAME: Final[str] = "delete_sim_run_btn" SAVE_SIM_RUNS_TO_FILE_BTN_NAME: Final[str] = "save_sims_to_file_btn" -RUN_SIM_RUNS_BTN_NAME: Final[str] = "run_sims_btn" -RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME: Final[str] = "run_sims_stop_first_failure_btn" SIMULATION_RUNS_LIST_VIEW_NAME: Final[str] = "sim_runs_list_view" +SIM_RUN_EXECUTION_TRIGGER_BTN_NAME: Final[str] = "sim_run_exec_trigger_btn" +SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME: Final[str] = "sim_run_exec_mode_dropbown" IMPORT_FROM_FILE_NO_FILE_SELECTED_PLACEHOLDER_TEXT: Final[str] = "<NONE>" +# TODO: Add simulation runs button is sometimes not enabled? +# TODO: Mark private variables of workers with underscore prefix +# TODO: Structural pattern matching might only be supported in python >= 3.10 but project also supports 3.9 +class SimulationRunExecutionMode(Enum): + RUN_ALL = 0 + RUN_ALL_STOP_AT_FIRST_FAILURE = 1 + RUN_SINGLE = 2 + + class QuantumCircuitSimulationDialog(QtWidgets.QDialog): # type: ignore[misc] # We would like to reuse the regex in multiple one-time use dialog instances. Additionally, the regex is used only once in these instances so declaring the # regex as an instance variable seems wasteful. Whether a compiled or non-compiled regex should be used would have to be benchmarked. @@ -97,6 +112,7 @@ def __init__( self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self.simulation_run_dialog: SimulationRunDialog | None = None + self.shared_selected_sim_run_execution_mode_dropdown_index: int = 0 self.prev_active_simulation_runs_tab_idx: int = 0 self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) @@ -223,6 +239,39 @@ def initialize_simulation_runs_tab_widget( simulation_runs_execution_buttons_layout = QtWidgets.QHBoxLayout() simulation_runs_execution_buttons_layout.addStretch() + sim_run_execution_mode_controls_layout = QtWidgets.QGridLayout() + simulation_runs_execution_buttons_layout.addLayout(sim_run_execution_mode_controls_layout) + + sim_run_execution_mode_dropdown_lbl = QtWidgets.QLabel("Simulation run execution mode:") + sim_run_execution_mode_dropdown = QtWidgets.QComboBox(objectName=SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME) + sim_run_execution_mode_dropdown.insertItem(0, "Run all simulation runs", SimulationRunExecutionMode.RUN_ALL) + sim_run_execution_mode_dropdown.insertItem( + 1, + "Run all simulation runs (stop at first output qubit value mismatch)", + SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE, + ) + sim_run_execution_mode_dropdown.insertItem( + 2, "Run selected simulation run", SimulationRunExecutionMode.RUN_SINGLE + ) + sim_run_execution_mode_dropdown.setPlaceholderText("<UNKNOWN EXECUTION MODE>") + sim_run_execution_mode_dropdown.setSizeAdjustPolicy(QtWidgets.QComboBox.SizeAdjustPolicy.AdjustToContents) + sim_run_execution_mode_dropdown.setEnabled(False) + sim_run_execution_mode_dropdown.currentIndexChanged.connect( + self.handle_simulation_run_execution_mode_selection_change + ) + + sim_run_execution_mode_controls_layout.addWidget(sim_run_execution_mode_dropdown_lbl, 0, 0) + sim_run_execution_mode_controls_layout.addWidget(sim_run_execution_mode_dropdown, 1, 0) + + sim_run_execution_trigger_btn = QtWidgets.QPushButton( + QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart), + "Execute simulation runs", + objectName=SIM_RUN_EXECUTION_TRIGGER_BTN_NAME, + ) + sim_run_execution_trigger_btn.clicked.connect(self._open_simulation_runs_execution_dialog) + sim_run_execution_trigger_btn.setEnabled(False) + sim_run_execution_mode_controls_layout.addWidget(sim_run_execution_trigger_btn, 0, 1) + save_simulation_runs_to_file_button = QtWidgets.QPushButton( QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.DocumentSave), "Save simulation runs to file", @@ -233,31 +282,7 @@ def initialize_simulation_runs_tab_widget( save_simulation_runs_to_file_button.setToolTip( "Save the defined simulation runs to a .json file (only the input and expected output qubit values of simulation runs in which both input and output qubit values are known are exported)" ) - simulation_runs_execution_buttons_layout.addWidget(save_simulation_runs_to_file_button) - - run_simulation_runs_button = QtWidgets.QPushButton( - QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart), - "Run simulation runs", - objectName=RUN_SIM_RUNS_BTN_NAME, - ) - run_simulation_runs_button.setEnabled(False) - run_simulation_runs_button.clicked.connect(self.handle_run_all_simulation_runs_button_click) - simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_button) - - run_simulation_runs_stop_at_first_failure_button = QtWidgets.QPushButton( - QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart), - "Run simulation runs (stop at first output qubit values mismatch)", - objectName=RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME, - ) - run_simulation_runs_stop_at_first_failure_button.setEnabled(False) - run_simulation_runs_stop_at_first_failure_button.clicked.connect( - self.handle_run_all_simulation_runs_stop_at_first_failure_button_click - ) - run_simulation_runs_stop_at_first_failure_button.setToolTip( - "Perform a simulation of all defined simulation runs until a mismatch between the expected and actual output state qubit values is detected (the value of both output states needs to be known)" - ) - simulation_runs_execution_buttons_layout.addWidget(run_simulation_runs_stop_at_first_failure_button) - + sim_run_execution_mode_controls_layout.addWidget(save_simulation_runs_to_file_button, 1, 1) simulation_runs_execution_buttons_layout.addStretch() tab_wrapper_widget_layout.addSpacing(manual_y_space_size) @@ -340,9 +365,8 @@ def handle_simulation_run_selection_change( add_simulation_run_btn.setEnabled(not is_list_item_selected) edit_simulation_run_btn.setEnabled(is_list_item_selected) delete_simulation_run_btn.setEnabled(is_list_item_selected) - self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, - not is_list_item_selected and (self.simulation_runs_model.rowCount(QtCore.QModelIndex()) < sys.maxsize), + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( + curr_active_tab_widget, is_list_item_selected ) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -641,10 +665,20 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i optional_to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( switched_to_tab_index ) + optional_sim_run_exec_mode_dropdown_in_switched_to_tab: QtWidgets.QComboBox | None = ( + optional_to_be_switched_to_tab_widget.findChild(QtWidgets.QComboBox, SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME) + if optional_to_be_switched_to_tab_widget is not None + else None + ) + if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, - required_widgets=[optional_prev_active_tab_widget, optional_to_be_switched_to_tab_widget], - error_dialog_content="Failed to locate previous/current active tab widget during simulation run tab change handler", + required_widgets=[ + optional_prev_active_tab_widget, + optional_to_be_switched_to_tab_widget, + optional_sim_run_exec_mode_dropdown_in_switched_to_tab, + ], + error_dialog_content="Failed to locate previous/current active tab widget in simulation run tab change handler", ): if optional_to_be_switched_to_tab_widget is None: self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) @@ -687,18 +721,18 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self.handle_open_and_start_all_input_states_generator_dialog( self.annotatable_quantum_computation.num_data_qubits ) + + sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( + "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown_in_switched_to_tab + ) + # TODO: Error handling + sim_run_exec_mode_dropdown.setCurrentIndex(self.shared_selected_sim_run_execution_mode_dropdown_index) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) self.prev_active_simulation_runs_tab_idx = switched_to_tab_index @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def handle_run_all_simulation_runs_button_click(self) -> None: - self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=False) - - @QtCore.pyqtSlot() # type: ignore[untyped-decorator] - def handle_run_all_simulation_runs_stop_at_first_failure_button_click(self) -> None: - self.open_simulation_runs_execution_dialog(stop_at_first_output_state_mismatch=True) - - def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_mismatch: bool) -> None: + def _open_simulation_runs_execution_dialog(self) -> None: if self.simulation_run_dialog is not None: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -710,6 +744,60 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma ) return + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + + optional_simulation_runs_list_view: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + optional_sim_run_exec_mode_dropdown: QtWidgets.QComboBox | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QComboBox, SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[ + optional_curr_active_tab_widget, + optional_simulation_runs_list_view, + optional_sim_run_exec_mode_dropdown, + ], + error_dialog_content="Failed to locate all required QtWidgets in open simulation run execution dialog handler", + ): + return + + cast("QtWidgets.QTabWidget", optional_curr_active_tab_widget) + simulation_runs_list_view: Final[QtWidgets.QListView] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view + ) + sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( + "QtWidgets.QListView", optional_sim_run_exec_mode_dropdown + ) + + selected_sim_run_model_idx: QtCore.QModelIndex | None = None + curr_sim_run_exec_mode: Final[SimulationRunExecutionMode | None] = sim_run_exec_mode_dropdown.currentData() + # TODO: + assert curr_sim_run_exec_mode is not None + if curr_sim_run_exec_mode == SimulationRunExecutionMode.RUN_SINGLE: + curr_num_selected_simulation_runs: Final[int] = len(simulation_runs_list_view.selectedIndexes()) + # We are assuming that the QListView only supports single item selection but QListView only offers fetch of all selected indices + if curr_num_selected_simulation_runs > 0: + selected_sim_run_model_idx = simulation_runs_list_view.selectedIndexes()[0] + else: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Failed to determine selected simulation run!", + message_box_content=f"Tried to find the selected simulation run in the list of simulation runs but {curr_num_selected_simulation_runs} where selected!", + is_cancellable=False, + ) + return + self.simulation_run_dialog = SimulationRunDialog( parent=self, shared_simulation_runs_model=self.simulation_runs_model, @@ -717,7 +805,17 @@ def open_simulation_runs_execution_dialog(self, stop_at_first_output_state_misma ) self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) self.simulation_run_dialog.show() - self.simulation_run_dialog.start_simulations(stop_at_first_output_state_mismatch) + + match curr_sim_run_exec_mode: + case SimulationRunExecutionMode.RUN_SINGLE: + assert selected_sim_run_model_idx is not None + self.simulation_run_dialog.start_simulation(selected_sim_run_model_idx) + case SimulationRunExecutionMode.RUN_ALL | SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE: + self.simulation_run_dialog.start_simulations( + curr_sim_run_exec_mode == SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE + ) + case _: + assert_never(curr_sim_run_exec_mode) @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_runs_dialog_close(self, _: int) -> None: @@ -851,10 +949,11 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( self, tab_widget: QtWidgets.QWidget, should_controls_be_enabled: bool ) -> None: optional_run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( - QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_NAME + QtWidgets.QPushButton, SIM_RUN_EXECUTION_TRIGGER_BTN_NAME ) - optional_run_simulation_runs_stop_at_first_failure_btn: QtWidgets.QPushButton | None = tab_widget.findChild( - QtWidgets.QPushButton, RUN_SIM_RUNS_BTN_STOP_AT_FIRST_FAILURE_NAME + + optional_sim_run_exec_mode_dropdown: QtWidgets.QComboBox | None = tab_widget.findChild( + QtWidgets.QComboBox, SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME ) optional_save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, SAVE_SIM_RUNS_TO_FILE_BTN_NAME @@ -864,7 +963,7 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( error_notification_parent_widget=self, required_widgets=[ optional_run_simulation_runs_btn, - optional_run_simulation_runs_stop_at_first_failure_btn, + optional_sim_run_exec_mode_dropdown, optional_save_simulation_runs_to_file_btn, ], error_dialog_content="Failed to locate required QtWidgets during change of enabled state of simulation run execution controls", @@ -874,19 +973,71 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( run_simulation_runs_btn: Final[QtWidgets.QPushButton] = cast( "QtWidgets.QPushButton", optional_run_simulation_runs_btn ) - run_simulation_runs_stop_at_first_failure_btn: Final[QtWidgets.QPushButton] = cast( - "QtWidgets.QPushButton", optional_run_simulation_runs_stop_at_first_failure_btn + sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( + "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown ) save_simulation_runs_to_file_btn: Final[QtWidgets.QPushButton] = cast( "QtWidgets.QPushButton", optional_save_simulation_runs_to_file_btn ) run_simulation_runs_btn.setEnabled(should_controls_be_enabled) - run_simulation_runs_stop_at_first_failure_btn.setEnabled(should_controls_be_enabled) - save_simulation_runs_to_file_btn.setEnabled( - should_controls_be_enabled and not self.did_syrec_program_contain_comments + sim_run_exec_mode_dropdown.setEnabled(should_controls_be_enabled) + save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) + + def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( + self, tab_widget: QtWidgets.QWidget, is_simulation_run_selected: bool + ) -> None: + optional_run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( + QtWidgets.QPushButton, SIM_RUN_EXECUTION_TRIGGER_BTN_NAME ) + optional_sim_run_exec_mode_dropdown: QtWidgets.QComboBox | None = tab_widget.findChild( + QtWidgets.QComboBox, SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME + ) + optional_save_simulation_runs_to_file_btn: QtWidgets.QPushButton | None = tab_widget.findChild( + QtWidgets.QPushButton, SAVE_SIM_RUNS_TO_FILE_BTN_NAME + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[ + optional_run_simulation_runs_btn, + optional_sim_run_exec_mode_dropdown, + optional_save_simulation_runs_to_file_btn, + ], + error_dialog_content="Failed to locate required QtWidgets during change of enabled state of simulation run execution controls", + ): + return + + run_simulation_runs_btn: Final[QtWidgets.QPushButton] = cast( + "QtWidgets.QPushButton", optional_run_simulation_runs_btn + ) + sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( + "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown + ) + cast("QtWidgets.QPushButton", optional_save_simulation_runs_to_file_btn) + + curr_sim_run_exec_mode: Final[SimulationRunExecutionMode | None] = sim_run_exec_mode_dropdown.currentData() + if curr_sim_run_exec_mode is None: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Failed to determine simulation run execution mode", + message_box_content="Failed to determine simulation run execution mode while changing anbled state of simulation run execution controls.", + is_cancellable=True, + log_contents=False, + ) + return + + match curr_sim_run_exec_mode: + case SimulationRunExecutionMode.RUN_ALL | SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE: + run_simulation_runs_btn.setEnabled(not is_simulation_run_selected) + case SimulationRunExecutionMode.RUN_SINGLE: + run_simulation_runs_btn.setEnabled(is_simulation_run_selected) + case _: + # Added guard to handle new simulation run execution modes + assert_never(curr_sim_run_exec_mode) + def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( self.simulation_runs_tab_widget.currentIndex() @@ -929,3 +1080,31 @@ def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: add_sim_run_btn.setEnabled(True) edit_sim_run_btn.setEnabled(False) delete_sim_run_btn.setEnabled(False) + + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] + def handle_simulation_run_execution_mode_selection_change(self, selected_sim_run_index: int) -> None: + self.shared_selected_sim_run_execution_mode_dropdown_index = selected_sim_run_index + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + self.simulation_runs_tab_widget.currentIndex() + ) + + optional_simulation_runs_list_view: QtWidgets.QWidget | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_curr_active_tab_widget, optional_simulation_runs_list_view], + error_dialog_content="Failed to locate all required QtWidgets in simulation run execution mode selection change handler", + ): + return + + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + simulation_runs_list_view: Final[QtWidgets.QListView] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view + ) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( + curr_active_tab_widget, len(simulation_runs_list_view.selectedIndexes()) == 1 + ) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index b910e52d..f64e8d2e 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -29,6 +29,7 @@ from ...logger_utils import log_info_to_console from ...message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification +from ..simulation_run_model import SIMULATION_RUN_IO_STATE_QT_ROLE from ..styled_item_delegates.simulation_run_execution_styled_item_delegate import ( SimulationRunExecutionStyledItemDelegate, ) @@ -41,6 +42,22 @@ ) +# Instead of iterating through all rows of a QAbstractItemView (in our case the QListView displaying all simulation run models) and setting them hidden, implement a proxy model for the +# QAbstractItemView that does only display the simulation run model of interest. +class SimulationRunFilterModel(QtCore.QSortFilterProxyModel): # type: ignore[misc] + def __init__(self, parent: QtCore.QObject, idx_of_sim_run_model_of_interest: QtCore.QModelIndex) -> None: + super().__init__(parent) + self._idx_of_sim_run_model_of_interest: QtCore.QModelIndex = idx_of_sim_run_model_of_interest + + @override + def filterAcceptsRow(self, source_row: int, _: QtCore.QModelIndex) -> bool: + return ( + source_row == self._idx_of_sim_run_model_of_interest.row() + if self._idx_of_sim_run_model_of_interest.isValid() + else False + ) + + class SimulationRunDialog(BaseProgressDialog[SimulationRunWorker]): def __init__( self, @@ -57,6 +74,7 @@ def __init__( user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) self.annotatable_quantum_computation: syrec.annotatable_quantum_computation = annotatable_quantum_computation + self.optional_filtered_shared_sim_run_model: SimulationRunFilterModel | None = None self.stop_at_first_output_state_mismatch: bool = False self.num_executed_simulation_runs: int = 0 self.last_fetched_simulation_run_idx: int = 0 @@ -105,6 +123,22 @@ def __init__( layout.addWidget(self.dialog_button_box) self.setLayout(layout) + def start_simulation(self, idx_of_sim_run_to_execute: QtCore.QModelIndex) -> None: + self.sim_run_model_queue_batch_size = 1 + self.sim_run_result_queue_batch_size = 1 + + if self.progress_bar is not None: + self.progress_bar.setVisible(False) + + self.optional_filtered_shared_sim_run_model = SimulationRunFilterModel(self, idx_of_sim_run_to_execute) + self.optional_filtered_shared_sim_run_model.setSourceModel(self.shared_simulation_runs_model) + # self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) + self.simulation_runs_list_view.setModel(self.optional_filtered_shared_sim_run_model) + log_info_to_console(f"Starting execution of simulation run (index: {idx_of_sim_run_to_execute.row()})") + self.title_lbl.setText(f"Executing simulation run {idx_of_sim_run_to_execute.row()}!") + self._perform_single_sim_run_execution(idx_of_sim_run_to_execute) + self._change_dialog_ok_button_enable_state(True) + def start_simulations( self, stop_at_first_output_state_mismatch: bool, @@ -332,7 +366,6 @@ def _reset_previous_simulation_runs(self) -> bool: log_info_to_console(progress_info_msg) try: - assert self.shared_simulation_runs_model is not None self.shared_simulation_runs_model.reset_prev_simulation_run_execution_results() except Exception as err: self._handle_non_recoverable_error( @@ -352,3 +385,37 @@ def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: self._request_worker_cancellation() self._shutdown_worker_thread_and_await_completion() + + def _perform_single_sim_run_execution(self, idx_of_sim_run_to_execute: QtCore.QModelIndex) -> None: + try: + self.shared_simulation_runs_model.reset_prev_simulation_run_execution_result(idx_of_sim_run_to_execute) + + sim_run_for_idx: Final[SimulationRunModel | None] = self.shared_simulation_runs_model.data( + idx_of_sim_run_to_execute, SIMULATION_RUN_IO_STATE_QT_ROLE + ) + if sim_run_for_idx is None: + err_msg = f"Failed to fetch mode for simulation run {idx_of_sim_run_to_execute.row()}" + self._update_displayed_error_text( + err_msg, num_additionally_skipped_stack_frames_starting_from_this_function=1 + ) + return + + result: Final[SimulationRunResult] = SimulationRunWorker.perform_single_sim_run_execution( + self.annotatable_quantum_computation, + idx_of_sim_run_to_execute.row(), + sim_run_for_idx.input_state, + sim_run_for_idx.expected_output_state, + ) + self.shared_simulation_runs_model.update_model_using_simulation_run_result( + idx_of_sim_run_to_execute, + result.actual_output_state, + result.do_expected_and_actual_outputs_match, + result.sim_runtime_in_ms, + ) + + self._update_total_model_runtime_and_label(result.sim_runtime_in_ms) + self._accumulate_and_update_total_runtime(result.sim_runtime_in_ms) + except Exception as err: + self._handle_non_recoverable_error( + f"Error during reset of previous simulation run execution result of simulation run {idx_of_sim_run_to_execute.row()}, reason: {SimulationRunDialog._stringify_error(err)}" + ) diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index de074204..3071d0e4 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -10,7 +10,7 @@ import sys from dataclasses import dataclass -from typing import Final +from typing import Any, Final if sys.version_info >= (3, 12): from typing import override @@ -259,7 +259,7 @@ def rowCount(self, parent: QtCore.QModelIndex) -> int: return 0 if parent.isValid() else len(self.simulation_run_models) @override - def data(self, index: QtCore.QModelIndex, role: int) -> object: + def data(self, index: QtCore.QModelIndex, role: int) -> Any | None: if not index.isValid(): return None @@ -319,6 +319,15 @@ def reset_prev_simulation_run_execution_results(self) -> None: sim_run_model.reset_result_of_execution() self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(len(self.simulation_run_models) - 1, 0)) + def reset_prev_simulation_run_execution_result(self, idx_of_sim_run_to_reset: QtCore.QModelIndex) -> None: + if not self.is_model_index_valid(idx_of_sim_run_to_reset): + msg = "Invalid model index!" + log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) + raise ValueError(msg) + + self.simulation_run_models[idx_of_sim_run_to_reset.row()].reset_result_of_execution() + self.dataChanged.emit(idx_of_sim_run_to_reset, idx_of_sim_run_to_reset) + def update_edited_simulation_run_model( self, index: QtCore.QModelIndex, updated_simulation_run_data: SimulationRunModel ) -> None: diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 629f35df..c15276b6 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -36,7 +36,7 @@ class SimulationRunWorker(CancellableProducerConsumerWorker[SimulationRunModel, def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, - expected_input_state_size: int, + _expected_input_state_size: int, stop_at_first_output_state_mismatch: bool, worker_recv_queue_config: QueueConfig[SimulationRunModel | None], worker_send_queue_config: QueueConfig[SimulationRunResult], @@ -45,9 +45,9 @@ def __init__( worker_send_queue_config=worker_send_queue_config, worker_recv_queue_config=worker_recv_queue_config, ) - self.expected_input_state_size = expected_input_state_size - self.annotatable_quantum_computation = annotatable_quantum_computation - self.should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch + self._expected_input_state_size = _expected_input_state_size + self._annotatable_quantum_computation = annotatable_quantum_computation + self._should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: @@ -102,8 +102,8 @@ def start_simulations(self) -> None: # the None case in our check for the sentinel value assert dequeued_sim_run_model is not None sim_run_execution_result: SimulationRunResult = ( - SimulationRunWorker._perform_single_sim_run_execution( - self.annotatable_quantum_computation, + SimulationRunWorker.perform_single_sim_run_execution( + self._annotatable_quantum_computation, curr_sim_run_num, dequeued_sim_run_model.input_state, dequeued_sim_run_model.expected_output_state, @@ -120,7 +120,11 @@ def start_simulations(self) -> None: if self.is_cancellation_requested(): break - if n_remaining_batch_elems_to_generate > 0 and not has_reached_end_sentinel: + if ( + not (self._should_stop_at_first_output_state_mismatch and found_outputs_mismatch) + and n_remaining_batch_elems_to_generate > 0 + and not has_reached_end_sentinel + ): # We dequeued all elements of the receive queue but have not reached the required batch size in the send queue to emit a new batch. # Since we are expecting more elements from the sender due to not having reached the sentinel value of the receive queue we simply continue # in the processing queue @@ -141,12 +145,12 @@ def start_simulations(self) -> None: def _should_continue_processing(self, output_state_mismatch_flag: bool, reached_end_sentinel_flag: bool) -> bool: return ( not self.is_cancellation_requested() - and (not self.should_stop_at_first_output_state_mismatch or not output_state_mismatch_flag) + and (not self._should_stop_at_first_output_state_mismatch or not output_state_mismatch_flag) and not reached_end_sentinel_flag ) @staticmethod - def _perform_single_sim_run_execution( + def perform_single_sim_run_execution( annotatable_quantum_computation: syrec.annotatable_quantum_computation, sim_run_num: int, input_state: syrec.n_bit_values_container, From 800fba99bd0cf69c79340513f69ba72900a104dd Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 15:28:48 +0100 Subject: [PATCH 73/88] Added some docstrings and marked all protected/private instance variable identifiers with underscore prefix --- .../quantum_circuit_simulation_dialog.py | 224 +++++++++--------- .../all_input_states_generator_dialog.py | 72 +++--- .../dialogs/base_progress_dialog.py | 111 +++++---- .../dialogs/simulation_run_dialog.py | 196 +++++++-------- .../dialogs/simulation_run_editor_dialog.py | 170 +++++++------ .../simulation_run_json_export_dialog.py | 125 +++++----- .../simulation_run_json_import_dialog.py | 88 +++---- .../simulation_view/simulation_run_model.py | 65 +++-- .../all_input_states_generator_worker.py | 10 +- .../simulation_run_json_export_worker.py | 8 +- .../simulation_run_json_import_worker.py | 12 +- .../workers/simulation_run_worker.py | 8 +- 12 files changed, 553 insertions(+), 536 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 6e23b741..e3300f04 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -57,10 +57,12 @@ IMPORT_FROM_FILE_NO_FILE_SELECTED_PLACEHOLDER_TEXT: Final[str] = "<NONE>" +SOME_SIM_RUNS_TAB_WIDGET_NAME: Final[str] = "some_sim_runs_tab" +ALL_SIM_RUNS_TAB_WIDGET_NAME: Final[str] = "all_sim_runs_tab" +LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME: Final[str] = "LOAD_SIM_RUNS_FROM_FILE_TAB" + # TODO: Add simulation runs button is sometimes not enabled? -# TODO: Mark private variables of workers with underscore prefix -# TODO: Structural pattern matching might only be supported in python >= 3.10 but project also supports 3.9 class SimulationRunExecutionMode(Enum): RUN_ALL = 0 RUN_ALL_STOP_AT_FIRST_FAILURE = 1 @@ -79,20 +81,17 @@ def __init__( parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) - self.did_syrec_program_contain_comments: Final[bool] = ( + self._did_syrec_program_contain_comments: Final[bool] = ( self.__syrec_program_comment_regex.search(associated_stringified_syrec_program) is not None ) - self.associated_stringified_syrec_program: str = ( - associated_stringified_syrec_program if not self.did_syrec_program_contain_comments else "" + self._associated_stringified_syrec_program: Final[str] = ( + associated_stringified_syrec_program if not self._did_syrec_program_contain_comments else "" ) - self.annotatable_quantum_computation = annotatable_quantum_computation - self.some_sim_runs_tab_widget_name = "some_sim_runs_tab" - self.all_sim_runs_tab_widget_name = "all_sim_runs_tab" - self.load_sim_runs_from_file_tab_widget_name = "load_sim_runs_from_file_tab" - - self.title = "Define simulation runs for quantum computation" - self.setWindowTitle(self.title) + self._annotatable_quantum_computation: Final[syrec.annotatable_quantum_computation] = ( + annotatable_quantum_computation + ) + self.setWindowTitle("Define simulation runs for quantum computation") # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) self.setModal(True) @@ -105,37 +104,38 @@ def __init__( center_dialog_pos_for_size.x(), center_dialog_pos_for_size.y(), dialog_size.width(), dialog_size.height() ) - self.simulation_run_editor_dialog: SimulationRunEditorDialog | None = None - self.all_input_states_generator_dialog: AllInputStatesGeneratorDialog | None = None - self.simulation_run_import_from_file_dialog: SimulationRunJsonImportDialog | None = None - self.simulation_run_export_to_file_dialog: SimulationRunJsonExportDialog | None = None - self.expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits - self.simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) - self.simulation_run_dialog: SimulationRunDialog | None = None - self.shared_selected_sim_run_execution_mode_dropdown_index: int = 0 - - self.prev_active_simulation_runs_tab_idx: int = 0 - self.simulation_runs_tab_widget = QtWidgets.QTabWidget(self) - self.simulation_runs_tab_widget.currentChanged.connect(self.handle_simulation_runs_tab_widget_tab_changed) - self.simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, self.some_sim_runs_tab_widget_name), + self._simulation_run_editor_dialog: SimulationRunEditorDialog | None = None + self._all_input_states_generator_dialog: AllInputStatesGeneratorDialog | None = None + self._simulation_run_import_from_file_dialog: SimulationRunJsonImportDialog | None = None + self._simulation_run_export_to_file_dialog: SimulationRunJsonExportDialog | None = None + self._simulation_run_dialog: SimulationRunDialog | None = None + + self._expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits + self._simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) + self._shared_selected_sim_run_execution_mode_dropdown_index: int = 0 + self._prev_active_simulation_runs_tab_idx: int = 0 + + self._simulation_runs_tab_widget = QtWidgets.QTabWidget(self) + self._simulation_runs_tab_widget.currentChanged.connect(self.handle_simulation_runs_tab_widget_tab_changed) + self._simulation_runs_tab_widget.addTab( + self.initialize_simulation_runs_tab_widget(self._simulation_runs_model, SOME_SIM_RUNS_TAB_WIDGET_NAME), "Check some input-output mapping combinations", ) - self.simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self.simulation_runs_model, self.all_sim_runs_tab_widget_name), + self._simulation_runs_tab_widget.addTab( + self.initialize_simulation_runs_tab_widget(self._simulation_runs_model, ALL_SIM_RUNS_TAB_WIDGET_NAME), "Check all input-output mapping combinations", ) - self.simulation_runs_tab_widget.addTab( + self._simulation_runs_tab_widget.addTab( self.initialize_simulation_runs_tab_widget( - self.simulation_runs_model, - self.load_sim_runs_from_file_tab_widget_name, + self._simulation_runs_model, + LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME, create_load_from_file_controls=True, ), "Check input-output mapping combinations from file", ) self.layout = QtWidgets.QVBoxLayout() - self.layout.addWidget(self.simulation_runs_tab_widget) + self.layout.addWidget(self._simulation_runs_tab_widget) self.setLayout(self.layout) self.setSizeGripEnabled(True) @@ -150,7 +150,7 @@ def show_save_changes_reminder(self) -> None: ) def show_optional_comments_in_syrec_program_not_supported_notification(self) -> None: - if not self.did_syrec_program_contain_comments: + if not self._did_syrec_program_contain_comments: return show_and_request_ok_in_optionally_cancellable_notification( @@ -320,7 +320,7 @@ def handle_simulation_run_selection_change( if selected.isEmpty() == deselected.isEmpty(): return - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.currentWidget() if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, required_widgets=[optional_curr_active_tab_widget], @@ -371,14 +371,14 @@ def handle_simulation_run_selection_change( @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_add_btn_click(self) -> None: - self.simulation_runs_model.add_simulation_run_model( + self._simulation_runs_model.add_simulation_run_model( SimulationRunModel( - input_state=syrec.n_bit_values_container(self.expected_input_output_state_size), + input_state=syrec.n_bit_values_container(self._expected_input_output_state_size), expected_output_state=None, ) ) - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.currentWidget() if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, required_widgets=[optional_curr_active_tab_widget], @@ -406,7 +406,7 @@ def handle_simulation_run_add_btn_click(self) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_edit_btn_click(self) -> None: - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.currentWidget() optional_simulation_runs_list_view: QtWidgets.QListView | None = ( optional_curr_active_tab_widget.findChild(QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME) if optional_curr_active_tab_widget is not None @@ -450,38 +450,38 @@ def handle_simulation_run_edit_btn_click(self) -> None: create_new_n_bit_values_container_instances=True, ) - self.simulation_run_editor_dialog = SimulationRunEditorDialog( + self._simulation_run_editor_dialog = SimulationRunEditorDialog( simulation_runs_list_view.currentIndex(), copy_of_reference_sim_run_model, self ) - self.simulation_run_editor_dialog.finished.connect(self.handle_simulation_run_editor_dialog_close) - self.simulation_run_editor_dialog.show() + self._simulation_run_editor_dialog.finished.connect(self.handle_simulation_run_editor_dialog_close) + self._simulation_run_editor_dialog.show() @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_run_editor_dialog_close(self, result: int) -> None: # This should not happen but is checked nevertheless - if self.simulation_run_editor_dialog is None or result == QtWidgets.QDialog.DialogCode.Rejected: + if self._simulation_run_editor_dialog is None or result == QtWidgets.QDialog.DialogCode.Rejected: return try: - self.simulation_runs_model.update_edited_simulation_run_model( - self.simulation_run_editor_dialog.simulation_run_model_index, - self.simulation_run_editor_dialog.edited_simulation_run_model, + self._simulation_runs_model.update_edited_simulation_run_model( + self._simulation_run_editor_dialog.simulation_run_model_index, + self._simulation_run_editor_dialog.edited_simulation_run_model, ) except ValueError as err: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Simulation run model update error!", - message_box_content=f"Update of simulation run model {self.simulation_run_editor_dialog.simulation_run_model_index.row()} failed due to an error!\nReason: {err}", + message_box_content=f"Update of simulation run model {self._simulation_run_editor_dialog.simulation_run_model_index.row()} failed due to an error!\nReason: {err}", is_cancellable=False, log_contents=False, ) finally: - self.simulation_run_editor_dialog = None + self._simulation_run_editor_dialog = None @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_sim_run_save_to_file_btn_click(self) -> None: - if self.simulation_run_export_to_file_dialog is not None: + if self._simulation_run_export_to_file_dialog is not None: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -499,24 +499,24 @@ def handle_sim_run_save_to_file_btn_click(self) -> None: if not filename: return - self.simulation_run_export_to_file_dialog = SimulationRunJsonExportDialog( - parent=self, shared_simulation_runs_model=self.simulation_runs_model + self._simulation_run_export_to_file_dialog = SimulationRunJsonExportDialog( + parent=self, shared_simulation_runs_model=self._simulation_runs_model ) - self.simulation_run_export_to_file_dialog.finished.connect(self.handle_sim_run_export_to_file_dialog_close) - self.simulation_run_export_to_file_dialog.start_export( + self._simulation_run_export_to_file_dialog.finished.connect(self.handle_sim_run_export_to_file_dialog_close) + self._simulation_run_export_to_file_dialog.start_export( Path(filename), - self.associated_stringified_syrec_program, - self.simulation_runs_model.rowCount(QtCore.QModelIndex()), + self._associated_stringified_syrec_program, + self._simulation_runs_model.rowCount(QtCore.QModelIndex()), ) - self.simulation_run_export_to_file_dialog.show() + self._simulation_run_export_to_file_dialog.show() @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_sim_run_export_to_file_dialog_close(self, _: int) -> None: - self.simulation_run_export_to_file_dialog = None + self._simulation_run_export_to_file_dialog = None @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_open_and_start_all_input_states_generator_dialog(self, input_state_size: int) -> None: - if self.all_input_states_generator_dialog is not None: + if self._all_input_states_generator_dialog is not None: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -527,18 +527,18 @@ def handle_open_and_start_all_input_states_generator_dialog(self, input_state_si ) return - self.all_input_states_generator_dialog = AllInputStatesGeneratorDialog( - parent=self, shared_simulation_runs_model=self.simulation_runs_model + self._all_input_states_generator_dialog = AllInputStatesGeneratorDialog( + parent=self, shared_simulation_runs_model=self._simulation_runs_model ) - self.all_input_states_generator_dialog.finished.connect(self.handle_input_states_generator_dialog_close) - self.all_input_states_generator_dialog.show() - self.all_input_states_generator_dialog.start_generation(input_state_size) + self._all_input_states_generator_dialog.finished.connect(self.handle_input_states_generator_dialog_close) + self._all_input_states_generator_dialog.show() + self._all_input_states_generator_dialog.start_generation(input_state_size) @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_input_states_generator_dialog_close(self, result: int) -> None: - self.all_input_states_generator_dialog = None + self._all_input_states_generator_dialog = None - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.currentWidget() if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, required_widgets=[optional_curr_active_tab_widget], @@ -553,7 +553,7 @@ def handle_input_states_generator_dialog_close(self, result: int) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def handle_simulation_run_delete_btn_click(self) -> None: - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.currentWidget() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.currentWidget() optional_simulation_runs_list_view: QtWidgets.QListView | None = ( optional_curr_active_tab_widget.findChild(QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME) if optional_curr_active_tab_widget is not None @@ -572,7 +572,7 @@ def handle_simulation_run_delete_btn_click(self) -> None: "QtWidgets.QListView", optional_simulation_runs_list_view ) - if not self.simulation_runs_model.delete_simulation_run_model(simulation_runs_list_view.currentIndex()): + if not self._simulation_runs_model.delete_simulation_run_model(simulation_runs_list_view.currentIndex()): return # Deletion of an element should only be enabled when an item in the QListView is selected. After the deletion @@ -656,13 +656,13 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i if switched_to_tab_index == -1: return - if switched_to_tab_index == self.prev_active_simulation_runs_tab_idx: + if switched_to_tab_index == self._prev_active_simulation_runs_tab_idx: return - optional_prev_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.prev_active_simulation_runs_tab_idx + optional_prev_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._prev_active_simulation_runs_tab_idx ) - optional_to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( + optional_to_be_switched_to_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( switched_to_tab_index ) optional_sim_run_exec_mode_dropdown_in_switched_to_tab: QtWidgets.QComboBox | None = ( @@ -681,7 +681,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i error_dialog_content="Failed to locate previous/current active tab widget in simulation run tab change handler", ): if optional_to_be_switched_to_tab_widget is None: - self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) + self._simulation_runs_tab_widget.setCurrentIndex(self._prev_active_simulation_runs_tab_idx) return prev_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_prev_active_tab_widget) @@ -689,7 +689,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i "QtWidgets.QWidget", optional_to_be_switched_to_tab_widget ) - if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: + if self._simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: if not show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=self, @@ -698,14 +698,14 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i is_cancellable=True, log_contents=False, ): - self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) + self._simulation_runs_tab_widget.setCurrentIndex(self._prev_active_simulation_runs_tab_idx) return - self.simulation_runs_model.delete_all_simulation_run_models() + self._simulation_runs_model.delete_all_simulation_run_models() self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(to_be_switched_to_tab_widget, False) - if to_be_switched_to_tab_widget.objectName() == self.all_sim_runs_tab_widget_name: - n_input_state_combinations: int = 2**self.annotatable_quantum_computation.num_data_qubits + if to_be_switched_to_tab_widget.objectName() == ALL_SIM_RUNS_TAB_WIDGET_NAME: + n_input_state_combinations: int = 2**self._annotatable_quantum_computation.num_data_qubits if not show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=self, @@ -714,26 +714,26 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i is_cancellable=True, log_contents=False, ): - self.simulation_runs_tab_widget.setCurrentIndex(self.prev_active_simulation_runs_tab_idx) + self._simulation_runs_tab_widget.setCurrentIndex(self._prev_active_simulation_runs_tab_idx) self.set_default_simulation_run_modification_buttons_enabled_state() return self.handle_open_and_start_all_input_states_generator_dialog( - self.annotatable_quantum_computation.num_data_qubits + self._annotatable_quantum_computation.num_data_qubits ) sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown_in_switched_to_tab ) # TODO: Error handling - sim_run_exec_mode_dropdown.setCurrentIndex(self.shared_selected_sim_run_execution_mode_dropdown_index) + sim_run_exec_mode_dropdown.setCurrentIndex(self._shared_selected_sim_run_execution_mode_dropdown_index) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) - self.prev_active_simulation_runs_tab_idx = switched_to_tab_index + self._prev_active_simulation_runs_tab_idx = switched_to_tab_index @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _open_simulation_runs_execution_dialog(self) -> None: - if self.simulation_run_dialog is not None: + if self._simulation_run_dialog is not None: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -744,8 +744,8 @@ def _open_simulation_runs_execution_dialog(self) -> None: ) return - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._simulation_runs_tab_widget.currentIndex() ) optional_simulation_runs_list_view: QtWidgets.QWidget | None = ( @@ -798,20 +798,20 @@ def _open_simulation_runs_execution_dialog(self) -> None: ) return - self.simulation_run_dialog = SimulationRunDialog( + self._simulation_run_dialog = SimulationRunDialog( parent=self, - shared_simulation_runs_model=self.simulation_runs_model, - annotatable_quantum_computation=self.annotatable_quantum_computation, + shared_simulation_runs_model=self._simulation_runs_model, + annotatable_quantum_computation=self._annotatable_quantum_computation, ) - self.simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) - self.simulation_run_dialog.show() + self._simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) + self._simulation_run_dialog.show() match curr_sim_run_exec_mode: case SimulationRunExecutionMode.RUN_SINGLE: assert selected_sim_run_model_idx is not None - self.simulation_run_dialog.start_simulation(selected_sim_run_model_idx) + self._simulation_run_dialog.start_simulation(selected_sim_run_model_idx) case SimulationRunExecutionMode.RUN_ALL | SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE: - self.simulation_run_dialog.start_simulations( + self._simulation_run_dialog.start_simulations( curr_sim_run_exec_mode == SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE ) case _: @@ -819,7 +819,7 @@ def _open_simulation_runs_execution_dialog(self) -> None: @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_runs_dialog_close(self, _: int) -> None: - self.simulation_run_dialog = None + self._simulation_run_dialog = None @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def open_import_file_selector(self) -> None: @@ -830,8 +830,8 @@ def open_import_file_selector(self) -> None: if not filename: return - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._simulation_runs_tab_widget.currentIndex() ) optional_selected_filename_lbl: QtWidgets.QWidget | None = ( optional_curr_active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) @@ -863,7 +863,7 @@ def open_import_file_selector(self) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def open_import_from_file_dialog(self) -> None: - if self.simulation_run_import_from_file_dialog is not None: + if self._simulation_run_import_from_file_dialog is not None: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -874,8 +874,8 @@ def open_import_from_file_dialog(self) -> None: ) return - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._simulation_runs_tab_widget.currentIndex() ) optional_selected_filename_lbl: QtWidgets.QWidget | None = ( optional_curr_active_tab_widget.findChild(QtWidgets.QLabel, LOADED_FROM_FILE_INPUT_FIELD_NAME) @@ -891,7 +891,7 @@ def open_import_from_file_dialog(self) -> None: return selected_filename_lbl: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_selected_filename_lbl) - if self.simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: + if self._simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: if not show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=self, @@ -901,24 +901,24 @@ def open_import_from_file_dialog(self) -> None: log_contents=False, ): return - self.simulation_runs_model.delete_all_simulation_run_models() + self._simulation_runs_model.delete_all_simulation_run_models() - self.simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog( - parent=self, shared_simulation_runs_model=self.simulation_runs_model + self._simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog( + parent=self, shared_simulation_runs_model=self._simulation_runs_model ) - self.simulation_run_import_from_file_dialog.finished.connect(self.handle_import_from_file_dialog_close) - self.simulation_run_import_from_file_dialog.show() - self.simulation_run_import_from_file_dialog.start_import( + self._simulation_run_import_from_file_dialog.finished.connect(self.handle_import_from_file_dialog_close) + self._simulation_run_import_from_file_dialog.show() + self._simulation_run_import_from_file_dialog.start_import( Path(selected_filename_lbl.text()), - expected_input_state_size=self.annotatable_quantum_computation.num_data_qubits, + expected_input_state_size=self._annotatable_quantum_computation.num_data_qubits, ) @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_import_from_file_dialog_close(self, result: int) -> None: - self.simulation_run_import_from_file_dialog = None + self._simulation_run_import_from_file_dialog = None - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._simulation_runs_tab_widget.currentIndex() ) if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, @@ -1039,8 +1039,8 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_o assert_never(curr_sim_run_exec_mode) def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._simulation_runs_tab_widget.currentIndex() ) optional_add_sim_run_btn: QtWidgets.QWidget | None = ( @@ -1083,9 +1083,9 @@ def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] def handle_simulation_run_execution_mode_selection_change(self, selected_sim_run_index: int) -> None: - self.shared_selected_sim_run_execution_mode_dropdown_index = selected_sim_run_index - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self.simulation_runs_tab_widget.widget( - self.simulation_runs_tab_widget.currentIndex() + self._shared_selected_sim_run_execution_mode_dropdown_index = selected_sim_run_index + optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( + self._simulation_runs_tab_widget.currentIndex() ) optional_simulation_runs_list_view: QtWidgets.QWidget | None = ( diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index 312cd14c..c6920a3a 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -39,12 +39,12 @@ def __init__(self, parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSi dialog_title="Generating simulation runs...", optional_progress_bar_text_format="Generated %v out of %m input states", ) - self.worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() - self.worker_send_queue_batch_size: int = 0 - self.num_generated_input_states: int = 0 + self._worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() + self._worker_send_queue_batch_size: int = 0 + self._num_generated_input_states: int = 0 - self.dialog_button_box.accepted.connect(self.accept) - self.dialog_button_box.rejected.connect(self._handle_input_state_generation_cancel_button_click) + self._dialog_button_box.accepted.connect(self.accept) + self._dialog_button_box.rejected.connect(self._handle_input_state_generation_cancel_button_click) def start_generation( self, @@ -65,15 +65,15 @@ def start_generation( # Since the operands of the exponentiation operator are casted to floats, the result (a float) could exceed the maximum storable value in an integer so we need to check this error case since # otherwise setting the maximum value of the QtWidgets.QProgressBar would raise an exception/no matching overload will be found since said function expects an integer parameter. n_expected_sim_runs_to_generate: Final[float] = 2**expected_input_state_size - if self.progress_bar is not None: + if self._progress_bar is not None: if not self._can_value_can_be_used_as_progress_bar_max_value(int(n_expected_sim_runs_to_generate)): # We do not ask for confirmation to close the dialog since we faulted before the input state generation started. super().reject() return - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(int(n_expected_sim_runs_to_generate)) - self.progress_bar.setValue(0) + self._progress_bar.setMinimum(0) + self._progress_bar.setMaximum(int(n_expected_sim_runs_to_generate)) + self._progress_bar.setValue(0) else: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -83,39 +83,39 @@ def start_generation( is_cancellable=False, ) - self.title_lbl.setText(f"Generating simulation runs with batch size {worker_send_queue_batch_size}!") + self._title_lbl.setText(f"Generating simulation runs with batch size {worker_send_queue_batch_size}!") # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker_send_queue_batch_size = worker_send_queue_batch_size - self.worker = AllInputStatesGeneratorWorker( + self._worker_send_queue_batch_size = worker_send_queue_batch_size + self._worker = AllInputStatesGeneratorWorker( expected_input_state_size, worker_send_queue_config=QueueConfig( - queue_instance=self.worker_send_queue, queue_batch_size=self.worker_send_queue_batch_size + queue_instance=self._worker_send_queue, queue_batch_size=self._worker_send_queue_batch_size ), ) - self.worker_thread = QtCore.QThread() - self.worker.moveToThread(self.worker_thread) - self.worker.batchCompleted.connect( + self._worker_thread = QtCore.QThread() + self._worker.moveToThread(self._worker_thread) + self._worker.batchCompleted.connect( self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.finished.connect( + self._worker.finished.connect( self._handle_input_state_generator_finished, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.failed.connect( + self._worker.failed.connect( self._handle_input_state_generator_failure, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker_thread.started.connect(self.worker.start_generation, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker_thread.finished.connect(self.worker_thread.deleteLater) - self.worker_thread.finished.connect(self._reset_workers) - self.worker_thread.start(QtCore.QThread.Priority.LowPriority) + self._worker_thread.started.connect(self._worker.start_generation, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker_thread.finished.connect(self._worker_thread.deleteLater) + self._worker_thread.finished.connect(self._reset_workers) + self._worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) @override def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_input_state_generation_cancel_button_click(): - if not self.error_text_lbl.text(): + if not self._error_text_lbl.text(): self.accept() else: # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. @@ -135,13 +135,13 @@ def _handle_input_state_generator_failure(self, err: Exception) -> None: @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] def _handle_generated_input_state_batch(self, batch_generation_duration_in_seconds: float) -> None: - if self.stop_processing_recv_batches: + if self._stop_processing_recv_batches: return n_dequeued_batch_elems: int = 0 try: - for _ in range(self.worker_send_queue_batch_size): - self.shared_simulation_runs_model.add_simulation_run_model(self.worker_send_queue.get_nowait()) + for _ in range(self._worker_send_queue_batch_size): + self._shared_simulation_runs_model.add_simulation_run_model(self._worker_send_queue.get_nowait()) n_dequeued_batch_elems += 1 except queue.Empty: # The last batch generated by the worker could contain less than the expected batch size elements thus an empty queue should not be treated as an error @@ -152,20 +152,20 @@ def _handle_generated_input_state_batch(self, batch_generation_duration_in_secon self._update_progress_text_with_batch_info(n_dequeued_batch_elems, batch_generation_duration_in_seconds) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_generated_input_states += n_dequeued_batch_elems - if self.progress_bar is not None: - self.progress_bar.setValue(self.num_generated_input_states) - self.progress_info_text_lbl.setText("") + self._num_generated_input_states += n_dequeued_batch_elems + if self._progress_bar is not None: + self._progress_bar.setValue(self._num_generated_input_states) + self._progress_info_text_lbl.setText("") QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_input_state_generator_finished(self, was_cancellation_requested: bool) -> None: info_msg: Final[str] = "Input state generator finished!" - self.progress_info_text_lbl.setText(info_msg) + self._progress_info_text_lbl.setText(info_msg) log_info_to_console(info_msg) - if self.progress_bar is not None: - self.progress_bar.setVisible(False) + if self._progress_bar is not None: + self._progress_bar.setVisible(False) # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation @@ -179,7 +179,7 @@ def _handle_input_state_generator_finished(self, was_cancellation_requested: boo @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_input_state_generation_cancel_button_click(self) -> bool: - if self.worker is None: + if self._worker is None: return True if show_and_request_ok_in_optionally_cancellable_notification( @@ -196,7 +196,7 @@ def _handle_input_state_generation_cancel_button_click(self) -> bool: return False def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: - self.progress_info_text_lbl.setText("") + self._progress_info_text_lbl.setText("") if err is not None: # We want to log the source of the error as close as possible to the origin of the actual error thus we need to skip a few stack frames # to determine the "source" stack frame. The skip stack frames would be (read from left to right with the leftmost stackframe being at the @@ -204,7 +204,7 @@ def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) try: - self.shared_simulation_runs_model.delete_all_simulation_run_models() + self._shared_simulation_runs_model.delete_all_simulation_run_models() except Exception: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 79a165c8..3c048b38 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -66,12 +66,12 @@ def __init__( ) -> None: super().__init__(parent) - self.worker_thread: QtCore.QThread | None = None - self.worker: WorkerType | None = None - self.shared_simulation_runs_model: QtSimulationRunModel = shared_simulation_runs_model + self._worker_thread: QtCore.QThread | None = None + self._worker: WorkerType | None = None + self._shared_simulation_runs_model: QtSimulationRunModel = shared_simulation_runs_model - self.stop_processing_recv_batches: bool = False - self.total_runtime_in_seconds: float = 0 + self._stop_processing_recv_batches: bool = False + self._total_runtime_in_seconds: float = 0 # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) @@ -96,45 +96,46 @@ def __init__( self.setGeometry(dialog_x_pos, dialog_y_pos, to_be_used_dialog_size.width(), to_be_used_dialog_size.height()) layout = QtWidgets.QVBoxLayout() - self.title_lbl = QtWidgets.QLabel() - self.title_lbl.setStyleSheet("QLabel { font-size : 16px; font-weight: bold; }") - self.title_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._title_lbl = QtWidgets.QLabel() + self._title_lbl.setStyleSheet("QLabel { font-size : 16px; font-weight: bold; }") + self._title_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.progress_info_text_lbl = QtWidgets.QLabel() - self.progress_info_text_lbl.setStyleSheet("QLabel { color : gray; }") - self.progress_info_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._progress_info_text_lbl = QtWidgets.QLabel() + self._progress_info_text_lbl.setStyleSheet("QLabel { color : gray; }") + self._progress_info_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.error_text_lbl = QtWidgets.QLabel() - self.error_text_lbl.setStyleSheet("QLabel { color : red; }") - self.error_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._error_text_lbl = QtWidgets.QLabel() + self._error_text_lbl.setStyleSheet("QLabel { color : red; }") + self._error_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.progress_bar: QtWidgets.QProgressBar | None = None + self._progress_bar: QtWidgets.QProgressBar | None = None if optional_progress_bar_text_format is not None: - self.progress_bar = QtWidgets.QProgressBar() + self._progress_bar = QtWidgets.QProgressBar() # For placeholder values see: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QProgressBar.html#PySide6.QtWidgets.QProgressBar.format - # self.progress_bar.setFormat("Generated %v out of %m input states") - self.progress_bar.setFormat(optional_progress_bar_text_format) - self.progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + # self._progress_bar.setFormat("Generated %v out of %m input states") + # An invalid pattern (e.g. unknown placeholders, etc.) will not cause an error. + self._progress_bar.setFormat(optional_progress_bar_text_format) + self._progress_bar.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.total_runtime_info_text_lbl = QtWidgets.QLabel() - self.total_runtime_info_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._total_runtime_info_text_lbl = QtWidgets.QLabel() + self._total_runtime_info_text_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.dialog_button_box = QtWidgets.QDialogButtonBox( + self._dialog_button_box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel ) - self.dialog_button_box.setCenterButtons(True) + self._dialog_button_box.setCenterButtons(True) self._change_dialog_ok_button_enable_state(False) self._change_dialog_cancel_button_enable_state(False) if create_default_layout: - layout.addWidget(self.title_lbl) - layout.addWidget(self.progress_info_text_lbl) - layout.addWidget(self.error_text_lbl) + layout.addWidget(self._title_lbl) + layout.addWidget(self._progress_info_text_lbl) + layout.addWidget(self._error_text_lbl) layout.addStretch() if optional_progress_bar_text_format is not None: - layout.addWidget(self.progress_bar) - layout.addWidget(self.total_runtime_info_text_lbl) - layout.addWidget(self.dialog_button_box) + layout.addWidget(self._progress_bar) + layout.addWidget(self._total_runtime_info_text_lbl) + layout.addWidget(self._dialog_button_box) self.setLayout(layout) @staticmethod @@ -165,18 +166,19 @@ def get_center_screen_position_for_size(dialog_size: QtCore.QSize) -> QtCore.QPo @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _allow_worker_to_continue(self) -> None: - if self.worker is None: + """Signal to the worker that it can continue with its long running operation.""" + if self._worker is None: return try: - self.worker.notify_to_continue_processing() + self._worker.notify_to_continue_processing() except Exception as err: self._handle_non_recoverable_error( f"Error while trying to notify simulation run execution worker about new batch data being available, reason: {BaseProgressDialog._stringify_error(err)}" ) def _update_progress_text_with_batch_info(self, n_batch_elements: int, batch_duration_in_seconds: float) -> None: - self.progress_info_text_lbl.setText( + self._progress_info_text_lbl.setText( DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT.format( n_batch_elements=n_batch_elements, batch_duration_in_ms=batch_duration_in_seconds * 1000 ) @@ -186,45 +188,57 @@ def _accumulate_and_update_total_runtime(self, batch_runtime_in_seconds: float) if batch_runtime_in_seconds < 0: return - self.total_runtime_in_seconds += batch_runtime_in_seconds - self.total_runtime_info_text_lbl.setText( - DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT.format(total_runtime_in_seconds=self.total_runtime_in_seconds) + self._total_runtime_in_seconds += batch_runtime_in_seconds + self._total_runtime_info_text_lbl.setText( + DEFAULT_TOTAL_RUNTIME_INFO_TEXT_FORMAT.format(total_runtime_in_seconds=self._total_runtime_in_seconds) ) def _shutdown_worker_thread_and_await_completion(self) -> None: - if self.worker_thread is None: + """ + Stop the work threads event queue (QThread) and await the completion of the associated system thread of the worker. + + Note: This call will block until the system thread of the worker finishes execution. + """ + if self._worker_thread is None: return log_info_to_console( "Shutting down worker thread!", num_additionally_skipped_stack_frames_starting_from_caller_function=1 ) - self.worker_thread.quit() + self._worker_thread.quit() log_info_to_console( "Waiting on worker thread completion...", num_additionally_skipped_stack_frames_starting_from_caller_function=1, ) - self.worker_thread.wait() + self._worker_thread.wait() log_info_to_console( "Worker thread finished!", num_additionally_skipped_stack_frames_starting_from_caller_function=1 ) - self.progress_info_text_lbl.setText("Worker thread finished!") + self._progress_info_text_lbl.setText("Worker thread finished!") def _request_worker_cancellation(self) -> None: - if self.worker is None: + """ + Request the cancellation of the long running operation of the worker. + + Note: It is the responsibility of the worker to support cooperative cancellation with this function giving + no guarantee whether the worker supports such behaviour. Depending on the implementation of the worker, this call might block + the calling thread. + """ + if self._worker is None: return - self.stop_processing_recv_batches = True - self.progress_info_text_lbl.setText("Requesting cancellation of long running worker!") + self._stop_processing_recv_batches = True + self._progress_info_text_lbl.setText("Requesting cancellation of long running worker!") log_info_to_console( "Requesting cancellation of long running worker", num_additionally_skipped_stack_frames_starting_from_caller_function=1, ) - self.worker.request_cancellation() + self._worker.request_cancellation() self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) def _change_dialog_cancel_button_enable_state(self, should_button_be_enabled: bool) -> None: BaseProgressDialog._change_dialog_button_enable_state( - self.dialog_button_box, + self._dialog_button_box, QtWidgets.QDialogButtonBox.StandardButton.Cancel, should_button_be_enabled, btn_not_found_notification_parent=self, @@ -232,7 +246,7 @@ def _change_dialog_cancel_button_enable_state(self, should_button_be_enabled: bo def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: BaseProgressDialog._change_dialog_button_enable_state( - self.dialog_button_box, + self._dialog_button_box, QtWidgets.QDialogButtonBox.StandardButton.Ok, should_button_be_enabled, btn_not_found_notification_parent=self, @@ -250,11 +264,12 @@ def _update_displayed_error_text( err_msg, num_additionally_skipped_stack_frames_starting_from_caller_function=num_additionally_skipped_stack_frames_starting_from_this_function, ) - self.error_text_lbl.setText(err_msg) + self._error_text_lbl.setText(err_msg) def _reset_workers(self) -> None: - self.worker_thread = None - self.worker = None + """Reset the worker and worker thread instances by setting them to None.""" + self._worker_thread = None + self._worker = None @staticmethod def _change_dialog_button_enable_state( diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index f64e8d2e..8c205a65 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -73,69 +73,71 @@ def __init__( create_default_layout=False, user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) - self.annotatable_quantum_computation: syrec.annotatable_quantum_computation = annotatable_quantum_computation - self.optional_filtered_shared_sim_run_model: SimulationRunFilterModel | None = None - self.stop_at_first_output_state_mismatch: bool = False - self.num_executed_simulation_runs: int = 0 - self.last_fetched_simulation_run_idx: int = 0 - self.total_model_update_runtime_in_seconds: float = 0 + self._annotatable_quantum_computation: Final[syrec.annotatable_quantum_computation] = ( + annotatable_quantum_computation + ) + self._optional_filtered_shared_sim_run_model: SimulationRunFilterModel | None = None + self._stop_at_first_output_state_mismatch: bool = False + self._num_executed_simulation_runs: int = 0 + self._last_fetched_simulation_run_idx: int = 0 + self._total_model_update_runtime_in_seconds: float = 0 - self.sim_run_model_queue_batch_size: int = 0 - self.sim_run_model_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() + self._sim_run_model_queue_batch_size: int = 0 + self._sim_run_model_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() - self.sim_run_result_queue_batch_size: int = 0 - self.sim_run_result_queue: queue.SimpleQueue[SimulationRunResult] = queue.SimpleQueue() + self._sim_run_result_queue_batch_size: int = 0 + self._sim_run_result_queue: queue.SimpleQueue[SimulationRunResult] = queue.SimpleQueue() - self.dialog_button_box.accepted.connect(self.accept) - self.dialog_button_box.rejected.connect(self._handle_simulation_runs_cancel_button_click) + self._dialog_button_box.accepted.connect(self.accept) + self._dialog_button_box.rejected.connect(self._handle_simulation_runs_cancel_button_click) layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.title_lbl) - layout.addWidget(self.progress_info_text_lbl) - layout.addWidget(self.error_text_lbl) + layout.addWidget(self._title_lbl) + layout.addWidget(self._progress_info_text_lbl) + layout.addWidget(self._error_text_lbl) simulation_runs_list_layout = QtWidgets.QHBoxLayout() - self.simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() - self.simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) - self.simulation_runs_list_view.setUniformItemSizes(True) - self.simulation_runs_list_view.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust) - self.simulation_runs_list_view.setAutoFillBackground(False) - self.simulation_runs_list_view.setSpacing(5) - self.simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) + self._simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView() + self._simulation_runs_list_view.setItemDelegate(SimulationRunExecutionStyledItemDelegate()) + self._simulation_runs_list_view.setUniformItemSizes(True) + self._simulation_runs_list_view.setResizeMode(QtWidgets.QListView.ResizeMode.Adjust) + self._simulation_runs_list_view.setAutoFillBackground(False) + self._simulation_runs_list_view.setSpacing(5) + self._simulation_runs_list_view.setFlow(QtWidgets.QListView.Flow.TopToBottom) # By default the vertical scroll mode is set to ScrollPerItem which will prevent the user to view not displayed if the vertical viewport size is larger than the required height of the list view item. - self.simulation_runs_list_view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) + self._simulation_runs_list_view.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) # Select with click on item, unselect with Ctrl+Click on already selected item (see https://doc.qt.io/qt-6/qabstractitemview.html#SelectionMode-enum) - self.simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + self._simulation_runs_list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) simulation_runs_list_scrollarea = QtWidgets.QScrollArea() simulation_runs_list_scrollarea.setAutoFillBackground(False) - simulation_runs_list_scrollarea.setWidget(self.simulation_runs_list_view) + simulation_runs_list_scrollarea.setWidget(self._simulation_runs_list_view) simulation_runs_list_scrollarea.setWidgetResizable(True) simulation_runs_list_layout.addWidget(simulation_runs_list_scrollarea) layout.addLayout(simulation_runs_list_layout) - layout.addWidget(self.progress_bar) - layout.addWidget(self.total_runtime_info_text_lbl) + layout.addWidget(self._progress_bar) + layout.addWidget(self._total_runtime_info_text_lbl) - self.total_model_update_runtime_lbl = QtWidgets.QLabel() - self.total_model_update_runtime_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.total_model_update_runtime_lbl) + self._total_model_update_runtime_lbl = QtWidgets.QLabel() + self._total_model_update_runtime_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self._total_model_update_runtime_lbl) - layout.addWidget(self.dialog_button_box) + layout.addWidget(self._dialog_button_box) self.setLayout(layout) def start_simulation(self, idx_of_sim_run_to_execute: QtCore.QModelIndex) -> None: - self.sim_run_model_queue_batch_size = 1 - self.sim_run_result_queue_batch_size = 1 + self._sim_run_model_queue_batch_size = 1 + self._sim_run_result_queue_batch_size = 1 - if self.progress_bar is not None: - self.progress_bar.setVisible(False) + if self._progress_bar is not None: + self._progress_bar.setVisible(False) - self.optional_filtered_shared_sim_run_model = SimulationRunFilterModel(self, idx_of_sim_run_to_execute) - self.optional_filtered_shared_sim_run_model.setSourceModel(self.shared_simulation_runs_model) - # self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) - self.simulation_runs_list_view.setModel(self.optional_filtered_shared_sim_run_model) + self._optional_filtered_shared_sim_run_model = SimulationRunFilterModel(self, idx_of_sim_run_to_execute) + self._optional_filtered_shared_sim_run_model.setSourceModel(self._shared_simulation_runs_model) + # self._simulation_runs_list_view.setModel(self._shared_simulation_runs_model) + self._simulation_runs_list_view.setModel(self._optional_filtered_shared_sim_run_model) log_info_to_console(f"Starting execution of simulation run (index: {idx_of_sim_run_to_execute.row()})") - self.title_lbl.setText(f"Executing simulation run {idx_of_sim_run_to_execute.row()}!") + self._title_lbl.setText(f"Executing simulation run {idx_of_sim_run_to_execute.row()}!") self._perform_single_sim_run_execution(idx_of_sim_run_to_execute) self._change_dialog_ok_button_enable_state(True) @@ -145,7 +147,7 @@ def start_simulations( sim_run_model_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, sim_run_result_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: - expected_input_state_size: Final[int] = self.annotatable_quantum_computation.num_data_qubits + expected_input_state_size: Final[int] = self._annotatable_quantum_computation.num_data_qubits if sim_run_model_queue_batch_size < 1 or sim_run_result_queue_batch_size < 1 or expected_input_state_size < 1: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -157,30 +159,30 @@ def start_simulations( super().reject() return - self.sim_run_model_queue_batch_size = sim_run_model_queue_batch_size - self.sim_run_result_queue_batch_size = sim_run_result_queue_batch_size + self._sim_run_model_queue_batch_size = sim_run_model_queue_batch_size + self._sim_run_result_queue_batch_size = sim_run_result_queue_batch_size - self.simulation_runs_list_view.setModel(self.shared_simulation_runs_model) - self.stop_at_first_output_state_mismatch = stop_at_first_output_state_mismatch + self._simulation_runs_list_view.setModel(self._shared_simulation_runs_model) + self._stop_at_first_output_state_mismatch = stop_at_first_output_state_mismatch log_info_to_console( - f"Starting execution of simulation runs, stopping after first output mismatch flag is set to {self.stop_at_first_output_state_mismatch}" + f"Starting execution of simulation runs, stopping after first output mismatch flag is set to {self._stop_at_first_output_state_mismatch}" ) - expected_total_num_simulation_runs: Final[int] = self.shared_simulation_runs_model.rowCount( + expected_total_num_simulation_runs: Final[int] = self._shared_simulation_runs_model.rowCount( QtCore.QModelIndex() ) - self.title_lbl.setText( + self._title_lbl.setText( f"Executing {expected_total_num_simulation_runs} simulation runs with batch sizes (Sim. run model queue={sim_run_model_queue_batch_size}, Sim. run result queue={sim_run_result_queue_batch_size})!" ) - if self.progress_bar is not None: + if self._progress_bar is not None: if not self._can_value_can_be_used_as_progress_bar_max_value(expected_total_num_simulation_runs): # We do not ask for confirmation to close the dialog since we faulted before the simulation run execution started. super().reject() return - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(expected_total_num_simulation_runs) - self.progress_bar.setValue(0) + self._progress_bar.setMinimum(0) + self._progress_bar.setMaximum(expected_total_num_simulation_runs) + self._progress_bar.setValue(0) else: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -194,36 +196,36 @@ def start_simulations( return # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker = SimulationRunWorker( - self.annotatable_quantum_computation, + self._worker = SimulationRunWorker( + self._annotatable_quantum_computation, expected_input_state_size, - self.stop_at_first_output_state_mismatch, + self._stop_at_first_output_state_mismatch, worker_recv_queue_config=QueueConfig( - queue_instance=self.sim_run_model_queue, queue_batch_size=sim_run_model_queue_batch_size + queue_instance=self._sim_run_model_queue, queue_batch_size=sim_run_model_queue_batch_size ), worker_send_queue_config=QueueConfig( - queue_instance=self.sim_run_result_queue, queue_batch_size=sim_run_result_queue_batch_size + queue_instance=self._sim_run_result_queue, queue_batch_size=sim_run_result_queue_batch_size ), ) - self.worker_thread = QtCore.QThread() - self.worker.moveToThread(self.worker_thread) - self.worker_thread.started.connect(self.worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.finished.connect( + self._worker_thread = QtCore.QThread() + self._worker.moveToThread(self._worker_thread) + self._worker_thread.started.connect(self._worker.start_simulations, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.finished.connect( self._handle_all_simulation_run_executions_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.batchCompleted.connect( + self._worker.batchCompleted.connect( self._handle_simulation_run_execution_batch_done, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.requestingData.connect( + self._worker.requestingData.connect( self._enqueue_next_simulation_runs, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.failed.connect(self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.failed.connect(self._handle_simulation_runs_failure, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker_thread.finished.connect(self.worker_thread.deleteLater) - self.worker_thread.finished.connect(self._reset_workers) + self._worker_thread.finished.connect(self._worker_thread.deleteLater) + self._worker_thread.finished.connect(self._reset_workers) - self.worker_thread.start(QtCore.QThread.Priority.LowPriority) + self._worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) self._enqueue_next_simulation_runs() @@ -237,7 +239,7 @@ def reject(self) -> None: def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_simulation_runs_cancel_button_click(): - if not self.error_text_lbl.text(): + if not self._error_text_lbl.text(): self.accept() else: # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. @@ -247,11 +249,11 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_all_simulation_run_executions_done(self, was_cancellation_requested: bool) -> None: - self.progress_info_text_lbl.setText("Simulation run execution finished!") + self._progress_info_text_lbl.setText("Simulation run execution finished!") log_info_to_console("Simulation run execution finished!") - if self.progress_bar is not None: - self.progress_bar.setVisible(False) + if self._progress_bar is not None: + self._progress_bar.setVisible(False) # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation @@ -269,7 +271,7 @@ def _handle_simulation_runs_failure(self, err: Exception) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_simulation_runs_cancel_button_click(self) -> bool: - if self.worker is None: + if self._worker is None: return True if show_and_request_ok_in_optionally_cancellable_notification( @@ -287,7 +289,7 @@ def _handle_simulation_runs_cancel_button_click(self) -> bool: @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_in_seconds: float) -> None: - if self.stop_processing_recv_batches: + if self._stop_processing_recv_batches: return n_received_sim_run_execution_results: int = 0 @@ -295,11 +297,11 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ batch_results_processing_start_timestamp: Final[float] = SimulationRunWorker.get_timestamp() try: - for _ in range(self.sim_run_result_queue_batch_size): - simulation_run_result: SimulationRunResult = self.sim_run_result_queue.get_nowait() + for _ in range(self._sim_run_result_queue_batch_size): + simulation_run_result: SimulationRunResult = self._sim_run_result_queue.get_nowait() to_be_updated_sim_run_number = simulation_run_result.simulation_run_number - self.shared_simulation_runs_model.update_model_using_simulation_run_result( - self.shared_simulation_runs_model.index(to_be_updated_sim_run_number), + self._shared_simulation_runs_model.update_model_using_simulation_run_result( + self._shared_simulation_runs_model.index(to_be_updated_sim_run_number), simulation_run_result.actual_output_state, simulation_run_result.do_expected_and_actual_outputs_match, simulation_run_result.sim_runtime_in_ms, @@ -325,22 +327,22 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ ) self._update_total_model_runtime_and_label(batch_results_processing_duration_in_seconds) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_executed_simulation_runs += n_received_sim_run_execution_results - if self.progress_bar is not None: - self.progress_bar.setValue(self.num_executed_simulation_runs) + self._num_executed_simulation_runs += n_received_sim_run_execution_results + if self._progress_bar is not None: + self._progress_bar.setValue(self._num_executed_simulation_runs) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _enqueue_next_simulation_runs(self) -> None: try: for i in range( - self.last_fetched_simulation_run_idx, - self.last_fetched_simulation_run_idx + self.sim_run_model_queue_batch_size, + self._last_fetched_simulation_run_idx, + self._last_fetched_simulation_run_idx + self._sim_run_model_queue_batch_size, ): to_be_enqueued_sim_run_model: SimulationRunModel | None = ( - self.shared_simulation_runs_model.get_simulation_run_model(i) + self._shared_simulation_runs_model.get_simulation_run_model(i) ) - self.last_fetched_simulation_run_idx += 1 - self.sim_run_model_queue.put(to_be_enqueued_sim_run_model) + self._last_fetched_simulation_run_idx += 1 + self._sim_run_model_queue.put(to_be_enqueued_sim_run_model) if to_be_enqueued_sim_run_model is None: break # After having enqueued a new batch for the worker add a small delay before allowing the worker to produce new items @@ -353,20 +355,20 @@ def _enqueue_next_simulation_runs(self) -> None: ) def _update_total_model_runtime_and_label(self, batch_model_update_runtime_in_seconds: float) -> None: - self.total_model_update_runtime_in_seconds += batch_model_update_runtime_in_seconds - self.total_model_update_runtime_lbl.setText( + self._total_model_update_runtime_in_seconds += batch_model_update_runtime_in_seconds + self._total_model_update_runtime_lbl.setText( MODEL_UPDATE_RUNTIME_FORMAT.format( - total_model_update_runtime_in_seconds=self.total_model_update_runtime_in_seconds + total_model_update_runtime_in_seconds=self._total_model_update_runtime_in_seconds ) ) def _reset_previous_simulation_runs(self) -> bool: progress_info_msg: Final[str] = "Resetting previous simulation run results!" - self.progress_info_text_lbl.setText(progress_info_msg) + self._progress_info_text_lbl.setText(progress_info_msg) log_info_to_console(progress_info_msg) try: - self.shared_simulation_runs_model.reset_prev_simulation_run_execution_results() + self._shared_simulation_runs_model.reset_prev_simulation_run_execution_results() except Exception as err: self._handle_non_recoverable_error( f"Error during reset of previous simulation run execution results prior to new simulation, reason: {SimulationRunDialog._stringify_error(err)}" @@ -376,9 +378,9 @@ def _reset_previous_simulation_runs(self) -> bool: return True def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: - self.progress_info_text_lbl.setText("") - if self.progress_bar is not None: - self.progress_bar.setVisible(False) + self._progress_info_text_lbl.setText("") + if self._progress_bar is not None: + self._progress_bar.setVisible(False) if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) @@ -388,9 +390,9 @@ def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: def _perform_single_sim_run_execution(self, idx_of_sim_run_to_execute: QtCore.QModelIndex) -> None: try: - self.shared_simulation_runs_model.reset_prev_simulation_run_execution_result(idx_of_sim_run_to_execute) + self._shared_simulation_runs_model.reset_prev_simulation_run_execution_result(idx_of_sim_run_to_execute) - sim_run_for_idx: Final[SimulationRunModel | None] = self.shared_simulation_runs_model.data( + sim_run_for_idx: Final[SimulationRunModel | None] = self._shared_simulation_runs_model.data( idx_of_sim_run_to_execute, SIMULATION_RUN_IO_STATE_QT_ROLE ) if sim_run_for_idx is None: @@ -401,12 +403,12 @@ def _perform_single_sim_run_execution(self, idx_of_sim_run_to_execute: QtCore.QM return result: Final[SimulationRunResult] = SimulationRunWorker.perform_single_sim_run_execution( - self.annotatable_quantum_computation, + self._annotatable_quantum_computation, idx_of_sim_run_to_execute.row(), sim_run_for_idx.input_state, sim_run_for_idx.expected_output_state, ) - self.shared_simulation_runs_model.update_model_using_simulation_run_result( + self._shared_simulation_runs_model.update_model_using_simulation_run_result( idx_of_sim_run_to_execute, result.actual_output_state, result.do_expected_and_actual_outputs_match, diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 84cb8f07..c94545d2 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -62,7 +62,7 @@ class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = None): super().__init__(parent) - self.expected_max_num_characters = expected_max_num_characters + self._expected_max_num_characters = expected_max_num_characters self.setMaxLength(expected_max_num_characters) self.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) @@ -71,7 +71,7 @@ def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = def sizeHint(self) -> QtCore.QSize: # noqa: N802 sh = super().sizeHint() fm = QtGui.QFontMetrics(self.font()) - nominal = fm.boundingRect("W" * self.expected_max_num_characters).width() + nominal = fm.boundingRect("W" * self._expected_max_num_characters).width() # use the offered width preferred = max(nominal, self.width()) return QtCore.QSize(preferred, sh.height()) @@ -130,23 +130,22 @@ def __init__( parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) - self.failed_due_to_internal_error: bool = False - self.simulation_run_model_index: QtCore.QModelIndex = simulation_run_model_index - self.edited_simulation_run_model: SimulationRunModel = copy_of_reference_edit_sim_run_model + self._failed_due_to_internal_error: bool = False + self._edited_simulation_run_model: SimulationRunModel = copy_of_reference_edit_sim_run_model - self.qreg_layouts: list[QuantumRegisterLayout] = simulation_run_model_index.data( + self._qreg_layouts: list[QuantumRegisterLayout] = simulation_run_model_index.data( QUANTUM_REGISTER_LAYOUT_QT_ROLE ) - self.annotatable_quantum_computation: syrec.annotatable_quantum_computation = simulation_run_model_index.data( + self._annotatable_quantum_computation: syrec.annotatable_quantum_computation = simulation_run_model_index.data( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE ) - initial_input_state: syrec.n_bit_values_container = self.edited_simulation_run_model.input_state + initial_input_state: syrec.n_bit_values_container = self._edited_simulation_run_model.input_state initial_expected_output_state: syrec.n_bit_values_container | None = ( - self.edited_simulation_run_model.expected_output_state + self._edited_simulation_run_model.expected_output_state ) initial_actual_output_state: syrec.n_bit_values_container | None = ( - self.edited_simulation_run_model.actual_output_state + self._edited_simulation_run_model.actual_output_state ) # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice @@ -165,22 +164,19 @@ def __init__( main_layout = QtWidgets.QVBoxLayout() - self.simulation_run_wrapper_box = QtWidgets.QGroupBox( + self._simulation_run_wrapper_box = QtWidgets.QGroupBox( "Simulation run #" + str(simulation_run_model_index.row()) ) - self.are_qubits_values_readonly: bool = initial_input_state.size() == 0 - self.edit_of_qubit_values_enabled: bool = False - quantum_register_controls_grid_layout = QtWidgets.QGridLayout() - self.simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) + self._simulation_run_wrapper_box.setLayout(quantum_register_controls_grid_layout) quantum_register_controls_grid_layout.addLayout( self._create_qreg_search_controls(), 0, 0, alignment=QtCore.Qt.AlignmentFlag.AlignCenter ) init_expected_output_state_button = QtWidgets.QPushButton( "Init output state" - if self.edited_simulation_run_model.expected_output_state is None + if self._edited_simulation_run_model.expected_output_state is None else "Clear output state", objectName=QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, ) @@ -205,7 +201,7 @@ def __init__( ) qreg_controls_grid_row: int = 2 - for qreg_layout in self.qreg_layouts: + for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name quantum_register_label = QtWidgets.QLabel( @@ -316,7 +312,7 @@ def __init__( quantum_register_controls_grid_layout.setColumnStretch(4, 1) simulation_run_scroll_area = QtWidgets.QScrollArea() - simulation_run_scroll_area.setWidget(self.simulation_run_wrapper_box) + simulation_run_scroll_area.setWidget(self._simulation_run_wrapper_box) simulation_run_scroll_area.setWidgetResizable(True) simulation_run_scroll_area.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) main_layout.addWidget(simulation_run_scroll_area) @@ -326,19 +322,19 @@ def __init__( main_layout.addWidget(qreg_values_validation_error_label) # Add dialog control buttons and link signals to slots of dialog - self.dialog_button_box = QtWidgets.QDialogButtonBox( + self._dialog_button_box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.StandardButton.Save | QtWidgets.QDialogButtonBox.StandardButton.Cancel ) - self.dialog_button_box.setCenterButtons(True) - self.dialog_button_box.accepted.connect(self.accept) - self.dialog_button_box.rejected.connect(self.reject) - main_layout.addWidget(self.dialog_button_box) + self._dialog_button_box.setCenterButtons(True) + self._dialog_button_box.accepted.connect(self.accept) + self._dialog_button_box.rejected.connect(self.reject) + main_layout.addWidget(self._dialog_button_box) self.setLayout(main_layout) @override def reject(self) -> None: # Ask for confirmation before closing dialog - if self.failed_due_to_internal_error or show_and_request_ok_in_optionally_cancellable_notification( + if self._failed_due_to_internal_error or show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.QUESTION, message_box_parent=self, message_box_title="Confirm close", @@ -350,7 +346,7 @@ def reject(self) -> None: @override def closeEvent(self, event: QtGui.QCloseEvent) -> None: - if self.failed_due_to_internal_error: + if self._failed_due_to_internal_error: super().reject() return @@ -369,44 +365,44 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_quantum_register_name_search(self) -> None: - for qreg_layout in self.qreg_layouts: + for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name optional_qreg_name_search_input_field: QtWidgets.QLineEdit | None = ( - self.simulation_run_wrapper_box.findChild(QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME) + self._simulation_run_wrapper_box.findChild(QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME) ) - optional_qreg_name_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_name_label: QtWidgets.QLabel | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLabel, QREG_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) ) - optional_qreg_layout_info_label: QtWidgets.QLabel | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_layout_info_label: QtWidgets.QLabel | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLabel, QREG_LAYOUT_INFO_NAME_FORMAT.format(qreg_name=qreg_name) ) optional_qreg_input_state_input_field: QtWidgets.QLineEdit | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_expected_output_state_label: QtWidgets.QLabel | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLabel, EXPECTED_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_expected_output_state_input_field: QtWidgets.QLineEdit | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_actual_output_state_label: QtWidgets.QLabel | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLabel, ACTUAL_QREG_OUTPUT_STATE_PREFIX_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_actual_output_state_widget: QtWidgets.QLabel | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLabel, ACTUAL_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_edit_qubit_values_toggle_button: QtWidgets.QPushButton | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) ) ) @@ -462,13 +458,13 @@ def _handle_input_state_qubit_value_checkbox_state_change( update_associated_state_input_field: bool = False, ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, INPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), ) ) - optional_qreg_input_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_input_state_input_field: QtWidgets.QLineEdit | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) ) @@ -486,7 +482,7 @@ def _handle_input_state_qubit_value_checkbox_state_change( updated_qubit_value, return_as_high_low_state=True ) - if not self.edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): + if not self._edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -522,13 +518,13 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( update_associated_state_input_field: bool, ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=associated_qubit), ) ) - optional_qreg_output_state_input_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_output_state_input_field: QtWidgets.QLineEdit | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), ) @@ -547,7 +543,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( updated_qubit_value, return_as_high_low_state=True ) - if self.edited_simulation_run_model.expected_output_state is None: + if self._edited_simulation_run_model.expected_output_state is None: stringified_updated_qubit_value = SimulationRunEditorDialog._stringify_qubit_value( None, return_as_high_low_state=True ) @@ -556,7 +552,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( ) return - if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( + if not self._edited_simulation_run_model.update_expected_output_state_qubit_value( associated_qubit, updated_qubit_value ): show_and_request_ok_in_optionally_cancellable_notification( @@ -593,7 +589,7 @@ def _create_qreg_search_controls(self) -> QtWidgets.QLayout: qreg_name_validator = QtGui.QRegularExpressionValidator(qreg_name_regular_expr, self) qreg_search_input_field.setValidator(qreg_name_validator) - qreg_name_search_completer = QtWidgets.QCompleter([qreg_layout.qreg_name for qreg_layout in self.qreg_layouts]) + qreg_name_search_completer = QtWidgets.QCompleter([qreg_layout.qreg_name for qreg_layout in self._qreg_layouts]) qreg_name_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) qreg_search_input_field.setCompleter(qreg_name_search_completer) @@ -782,7 +778,7 @@ def _create_search_controls_for_qubits_of_qreg( qubit_search_completer = QtWidgets.QCompleter( SimulationRunEditorDialog._get_internal_qubit_labels_for_qreg( - self.annotatable_quantum_computation, first_qreg_qubit, last_qreg_qubit - first_qreg_qubit + self._annotatable_quantum_computation, first_qreg_qubit, last_qreg_qubit - first_qreg_qubit ) ) qubit_search_completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseSensitive) @@ -822,7 +818,7 @@ def _create_qubit_controls_groupbox( for qubit in range(first_qubit_of_qreg, first_qubit_of_qreg + associated_qreg_layout.qreg_size): # Due to first row (idx=0) of group box containing the qubit searc controls, the qubit value controls start in row 1. relative_qubit_index_in_qreg: int = qubit - first_qubit_of_qreg - fetched_internal_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( + fetched_internal_qubit_label: str | None = self._annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal ) qubit_label = QtWidgets.QLabel( @@ -923,13 +919,13 @@ def _create_qubit_controls_groupbox( @QtCore.pyqtSlot(str) # type: ignore[untyped-decorator] def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_name: str) -> None: associated_qreg_layout: Final[QuantumRegisterLayout | None] = next( - filter(lambda qreg_layout: qreg_layout.qreg_name == associated_quantum_register_name, self.qreg_layouts), + filter(lambda qreg_layout: qreg_layout.qreg_name == associated_quantum_register_name, self._qreg_layouts), None, ) if associated_qreg_layout is None: return - optional_qreg_qubits_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_qubits_groupbox: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1011,7 +1007,7 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ "QtWidgets.QCheckBox", optional_actual_output_state_qubit_checkbox ) - matched_with_qubit_label: str | None = self.annotatable_quantum_computation.get_qubit_label( + matched_with_qubit_label: str | None = self._annotatable_quantum_computation.get_qubit_label( qubit, syrec.qubit_label_type.internal ) does_qubit_label_match_search_text: bool = ( @@ -1030,18 +1026,18 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: is_any_qubit_values_groupbox_collapsed: bool = False optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - optional_qreg_search_input_field: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_search_input_field: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME ) - optional_qreg_search_trigger_btn: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + optional_qreg_search_trigger_btn: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_SEARCH_TRIGGER_BUTTON_NAME ) @@ -1055,26 +1051,26 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam ): return - for qreg_layout in self.qreg_layouts: + for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) optional_qreg_expected_output_state_input_field: QtWidgets.QtWidget | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) - optional_qubit_values_groupbox: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + optional_qubit_values_groupbox: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - optional_qubit_values_toggle_button: QtWidgets.QtWidget | None = self.simulation_run_wrapper_box.findChild( + optional_qubit_values_toggle_button: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1138,7 +1134,7 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_init_expected_output_state_button_click(self) -> None: optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1155,24 +1151,24 @@ def _handle_init_expected_output_state_button_click(self) -> None: "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button ) - should_reset_output_state: bool = self.edited_simulation_run_model.expected_output_state is not None + should_reset_output_state: bool = self._edited_simulation_run_model.expected_output_state is not None if should_reset_output_state: - self.edited_simulation_run_model.expected_output_state = None + self._edited_simulation_run_model.expected_output_state = None expected_output_state_value_toggle_button.setText("Init output state") else: - self.edited_simulation_run_model.initialize_expected_output_state_as_copy_of_input_state() + self._edited_simulation_run_model.initialize_expected_output_state_as_copy_of_input_state() expected_output_state_value_toggle_button.setText("Clear output state") - for qreg_layout in self.qreg_layouts: + for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) optional_qreg_output_state_input_field: QtWidgets.QtWidget | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) @@ -1194,7 +1190,7 @@ def _handle_init_expected_output_state_button_click(self) -> None: # and since no reset is requested, i.e. we initialized the expected output state member variable which in turn means it is not None at this point. qreg_output_state_input_field.setText( SimulationRunEditorDialog._stringify_some_qubits_of_n_bit_values_container( - self.edited_simulation_run_model.expected_output_state, # type: ignore[arg-type] + self._edited_simulation_run_model.expected_output_state, # type: ignore[arg-type] qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, ) @@ -1205,7 +1201,7 @@ def _handle_init_expected_output_state_button_click(self) -> None: qreg_layout.first_qubit_of_qreg, qreg_layout.first_qubit_of_qreg + qreg_layout.qreg_size ): optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) ) ) @@ -1218,7 +1214,7 @@ def _handle_init_expected_output_state_button_click(self) -> None: associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) qubit_value: bool | None = ( - self.edited_simulation_run_model.expected_output_state.test(qubit) # type: ignore[union-attr] + self._edited_simulation_run_model.expected_output_state.test(qubit) # type: ignore[union-attr] if not should_reset_output_state else None ) @@ -1236,20 +1232,20 @@ def _handle_init_expected_output_state_button_click(self) -> None: def _handle_input_or_output_state_text_change( self, associated_qreg_name: str, expected_qreg_size: int, is_editing_input_state: bool ) -> None: - optional_input_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + optional_input_state_text_field: QtWidgets.QLineEdit | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - optional_output_state_text_field: QtWidgets.QLineEdit | None = self.simulation_run_wrapper_box.findChild( + optional_output_state_text_field: QtWidgets.QLineEdit | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) optional_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=associated_qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1257,14 +1253,14 @@ def _handle_input_or_output_state_text_change( ) optional_expected_output_state_init_button: QtWidgets.QPushButton | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - optional_dialog_save_button: QtWidgets.QPushButton | None = self.dialog_button_box.button( + optional_dialog_save_button: QtWidgets.QPushButton | None = self._dialog_button_box.button( QtWidgets.QDialogButtonBox.StandardButton.Save ) optional_qreg_values_validation_error_lbl: QtWidgets.QLabel | None = self.findChild( @@ -1320,15 +1316,15 @@ def _handle_input_or_output_state_text_change( ) curr_not_edited_input_text_field.setEnabled( are_stringified_qreg_contents_valid - and (self.edited_simulation_run_model.expected_output_state is not None or not is_editing_input_state) + and (self._edited_simulation_run_model.expected_output_state is not None or not is_editing_input_state) ) if are_stringified_qreg_contents_valid: edited_qreg_layout: QuantumRegisterLayout | None = next( - filter(lambda qreg_layout: qreg_layout.qreg_name == associated_qreg_name, self.qreg_layouts), None + filter(lambda qreg_layout: qreg_layout.qreg_name == associated_qreg_name, self._qreg_layouts), None ) if edited_qreg_layout is None: - self.failed_due_to_internal_error = True + self._failed_due_to_internal_error = True show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -1341,7 +1337,7 @@ def _handle_input_or_output_state_text_change( return optional_effected_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1384,12 +1380,14 @@ def _handle_input_or_output_state_text_change( update_associated_state_input_field=False, ) - for qreg_layout in self.qreg_layouts: + for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name - optional_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = self.simulation_run_wrapper_box.findChild( - QtWidgets.QGroupBox, - QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), - QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + optional_qreg_qubit_values_groupbox: QtWidgets.QGroupBox | None = ( + self._simulation_run_wrapper_box.findChild( + QtWidgets.QGroupBox, + QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) ) if not self._assert_all_required_widgets_found_or_close_dialog( @@ -1402,7 +1400,7 @@ def _handle_input_or_output_state_text_change( if qreg_name != associated_qreg_name: optional_not_edited_input_state_text_field: QtWidgets.QLineEdit | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1410,7 +1408,7 @@ def _handle_input_or_output_state_text_change( ) optional_not_edited_output_state_text_field: QtWidgets.QLineEdit | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1418,7 +1416,7 @@ def _handle_input_or_output_state_text_change( ) optional_not_edited_qreg_qubit_values_edit_toggle_button: QtWidgets.QPushButton | None = ( - self.simulation_run_wrapper_box.findChild( + self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1451,7 +1449,7 @@ def _handle_input_or_output_state_text_change( not_edited_input_state_text_field.setEnabled(should_state_controls_be_visible) not_edited_output_state_text_field.setEnabled( should_state_controls_be_visible - and self.edited_simulation_run_model.expected_output_state is not None + and self._edited_simulation_run_model.expected_output_state is not None ) not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(should_state_controls_be_visible) @@ -1490,7 +1488,7 @@ def _handle_input_or_output_state_text_change( not_edited_input_state_qubit_checkbox.setEnabled(are_stringified_qreg_contents_valid) not_edited_output_state_qubit_checkbox.setEnabled( are_stringified_qreg_contents_valid - and self.edited_simulation_run_model.expected_output_state is not None + and self._edited_simulation_run_model.expected_output_state is not None ) @staticmethod @@ -1542,6 +1540,6 @@ def _assert_all_required_widgets_found_or_close_dialog( ): return True - self.failed_due_to_internal_error = True + self._failed_due_to_internal_error = True self.reject() return False diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index 41ef8c65..3eb03260 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -45,35 +45,35 @@ def __init__(self, parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSi optional_progress_bar_text_format="Processed simulation run %v of %m", create_default_layout=False, ) - self.num_processed_sim_runs: int = 0 - self.total_num_exported_sim_runs: int = 0 - self.total_num_skipped_sim_runs: int = 0 - self.last_exported_sim_run_num: int = 0 + self._num_processed_sim_runs: int = 0 + self._total_num_exported_sim_runs: int = 0 + self._total_num_skipped_sim_runs: int = 0 + self._last_exported_sim_run_num: int = 0 - self.worker_recv_queue_batch_size: int = 0 - self.worker_send_queue: queue.SimpleQueue[ExportedBatchData] = queue.SimpleQueue() - self.worker_recv_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() + self._worker_recv_queue_batch_size: int = 0 + self._worker_send_queue: queue.SimpleQueue[ExportedBatchData] = queue.SimpleQueue() + self._worker_recv_queue: queue.SimpleQueue[SimulationRunModel | None] = queue.SimpleQueue() - self.dialog_button_box.accepted.connect(self.accept) - self.dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) + self._dialog_button_box.accepted.connect(self.accept) + self._dialog_button_box.rejected.connect(self._handle_export_to_file_cancel_button_click) - self.export_location_info_lbl = QtWidgets.QLabel("") - self.export_location_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._export_location_info_lbl = QtWidgets.QLabel("") + self._export_location_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.exported_sim_runs_data_lbl = QtWidgets.QLabel("") - self.exported_sim_runs_data_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.exported_sim_runs_data_lbl.setStyleSheet("QLabel { color : gray; }") + self._exported_sim_runs_data_lbl = QtWidgets.QLabel("") + self._exported_sim_runs_data_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._exported_sim_runs_data_lbl.setStyleSheet("QLabel { color : gray; }") layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.title_lbl) - layout.addWidget(self.export_location_info_lbl) - layout.addWidget(self.progress_info_text_lbl) - layout.addWidget(self.error_text_lbl) + layout.addWidget(self._title_lbl) + layout.addWidget(self._export_location_info_lbl) + layout.addWidget(self._progress_info_text_lbl) + layout.addWidget(self._error_text_lbl) layout.addStretch() - layout.addWidget(self.progress_bar) - layout.addWidget(self.total_runtime_info_text_lbl) - layout.addWidget(self.exported_sim_runs_data_lbl) - layout.addWidget(self.dialog_button_box) + layout.addWidget(self._progress_bar) + layout.addWidget(self._total_runtime_info_text_lbl) + layout.addWidget(self._exported_sim_runs_data_lbl) + layout.addWidget(self._dialog_button_box) self.setLayout(layout) def start_export( @@ -83,8 +83,8 @@ def start_export( num_sim_runs_to_export: int, worker_recv_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: - self.title_lbl.setText(f"Exporting simulation runs with batch size {worker_recv_queue_batch_size}!") - self.export_location_info_lbl.setText(f"Export destination: {export_location!s}") + self._title_lbl.setText(f"Exporting simulation runs with batch size {worker_recv_queue_batch_size}!") + self._export_location_info_lbl.setText(f"Export destination: {export_location!s}") if worker_recv_queue_batch_size < 1 or num_sim_runs_to_export < 1: show_and_request_ok_in_optionally_cancellable_notification( @@ -97,15 +97,15 @@ def start_export( super().reject() return - if self.progress_bar is not None: + if self._progress_bar is not None: if not self._can_value_can_be_used_as_progress_bar_max_value(num_sim_runs_to_export): # We do not ask for confirmation to close the dialog since we faulted before the export started. super().reject() return - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(num_sim_runs_to_export) - self.progress_bar.setValue(0) + self._progress_bar.setMinimum(0) + self._progress_bar.setMaximum(num_sim_runs_to_export) + self._progress_bar.setValue(0) else: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -115,32 +115,32 @@ def start_export( is_cancellable=False, ) - self.worker_recv_queue_batch_size = worker_recv_queue_batch_size + self._worker_recv_queue_batch_size = worker_recv_queue_batch_size # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation - self.worker = SimulationRunJsonExportWorker( + self._worker = SimulationRunJsonExportWorker( export_location, associated_stringified_syrec_program, - worker_send_queue_config=QueueConfig(queue_instance=self.worker_send_queue, queue_batch_size=1), + worker_send_queue_config=QueueConfig(queue_instance=self._worker_send_queue, queue_batch_size=1), worker_recv_queue_config=QueueConfig( - queue_instance=self.worker_recv_queue, queue_batch_size=self.worker_recv_queue_batch_size + queue_instance=self._worker_recv_queue, queue_batch_size=self._worker_recv_queue_batch_size ), ) - self.worker_thread = QtCore.QThread() - self.worker.moveToThread(self.worker_thread) - self.worker.batchCompleted.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.requestingData.connect( + self._worker_thread = QtCore.QThread() + self._worker.moveToThread(self._worker_thread) + self._worker.batchCompleted.connect(self._handle_batch_exported, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.requestingData.connect( self._enqueue_next_simulation_runs_to_export, QtCore.Qt.ConnectionType.QueuedConnection ) - self.worker.finished.connect(self._handle_export_completion, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker.failed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.finished.connect(self._handle_export_completion, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.failed.connect(self._handle_export_failure, QtCore.Qt.ConnectionType.QueuedConnection) - self.worker_thread.started.connect( - self.worker.start_export, + self._worker_thread.started.connect( + self._worker.start_export, QtCore.Qt.ConnectionType.QueuedConnection, ) - self.worker_thread.finished.connect(self.worker_thread.deleteLater) - self.worker_thread.finished.connect(self._reset_workers) - self.worker_thread.start(QtCore.QThread.Priority.LowPriority) + self._worker_thread.finished.connect(self._worker_thread.deleteLater) + self._worker_thread.finished.connect(self._reset_workers) + self._worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) self._enqueue_next_simulation_runs_to_export() @@ -154,7 +154,7 @@ def reject(self) -> None: def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_export_to_file_cancel_button_click(): - if not self.error_text_lbl.text(): + if not self._error_text_lbl.text(): self.accept() else: # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. @@ -170,7 +170,7 @@ def _handle_export_failure(self, err: Exception) -> None: def _handle_batch_exported(self, batch_generation_duration_in_seconds: float) -> None: batch_data: ExportedBatchData = ExportedBatchData(exported_sim_runs=0, skipped_sim_runs=0) try: - batch_data = self.worker_send_queue.get_nowait() + batch_data = self._worker_send_queue.get_nowait() except queue.Empty: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, @@ -186,20 +186,21 @@ def _handle_batch_exported(self, batch_generation_duration_in_seconds: float) -> self._handle_non_recoverable_error(err) return - self.progress_info_text_lbl.setText( + self._progress_info_text_lbl.setText( f"Batch completed! Exported {batch_data.exported_sim_runs} and skipping {batch_data.skipped_sim_runs} simulation runs. Runtime [in seconds]: {batch_generation_duration_in_seconds}" ) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_processed_sim_runs += batch_data.exported_sim_runs + batch_data.skipped_sim_runs + self._num_processed_sim_runs += batch_data.exported_sim_runs + batch_data.skipped_sim_runs - if self.progress_bar is not None: - self.progress_bar.setValue(self.num_processed_sim_runs) + if self._progress_bar is not None: + self._progress_bar.setValue(self._num_processed_sim_runs) - self.total_num_exported_sim_runs += batch_data.exported_sim_runs - self.total_num_skipped_sim_runs += batch_data.skipped_sim_runs - self.exported_sim_runs_data_lbl.setText( + self._total_num_exported_sim_runs += batch_data.exported_sim_runs + self._total_num_skipped_sim_runs += batch_data.skipped_sim_runs + self._exported_sim_runs_data_lbl.setText( EXPORTED_SIM_RUNS_DATA_LABEL.format( - n_exported_sim_runs=self.total_num_exported_sim_runs, n_skipped_sim_runs=self.total_num_skipped_sim_runs + n_exported_sim_runs=self._total_num_exported_sim_runs, + n_skipped_sim_runs=self._total_num_skipped_sim_runs, ) ) QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @@ -208,13 +209,13 @@ def _handle_batch_exported(self, batch_generation_duration_in_seconds: float) -> def _enqueue_next_simulation_runs_to_export(self) -> None: try: for i in range( - self.last_exported_sim_run_num, self.last_exported_sim_run_num + self.worker_recv_queue_batch_size + self._last_exported_sim_run_num, self._last_exported_sim_run_num + self._worker_recv_queue_batch_size ): to_be_enqueued_sim_run_model: SimulationRunModel | None = ( - self.shared_simulation_runs_model.get_simulation_run_model(i) + self._shared_simulation_runs_model.get_simulation_run_model(i) ) - self.last_exported_sim_run_num += 1 - self.worker_recv_queue.put_nowait(to_be_enqueued_sim_run_model) + self._last_exported_sim_run_num += 1 + self._worker_recv_queue.put_nowait(to_be_enqueued_sim_run_model) if to_be_enqueued_sim_run_model is None: break QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @@ -225,11 +226,11 @@ def _enqueue_next_simulation_runs_to_export(self) -> None: @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_export_completion(self, was_cancellation_requested: bool) -> None: - self.progress_info_text_lbl.setText("Simulation run export finished!") + self._progress_info_text_lbl.setText("Simulation run export finished!") log_info_to_console("Simulation run export finished!") - if self.progress_bar is not None: - self.progress_bar.setVisible(False) + if self._progress_bar is not None: + self._progress_bar.setVisible(False) # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker # and its associated thread but the same operation also needs to be execute when the worker completes successfully. However, when cancellation @@ -243,7 +244,7 @@ def _handle_export_completion(self, was_cancellation_requested: bool) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_export_to_file_cancel_button_click(self) -> bool: - if self.worker is None: + if self._worker is None: return True if show_and_request_ok_in_optionally_cancellable_notification( @@ -260,7 +261,7 @@ def _handle_export_to_file_cancel_button_click(self) -> bool: return False def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: - self.progress_info_text_lbl.setText("") + self._progress_info_text_lbl.setText("") if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 88110dec..5e78479f 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -42,33 +42,33 @@ def __init__(self, parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSi optional_progress_bar_text_format=None, create_default_layout=False, ) - self.num_imported_simulation_runs: int = 0 + self._num_imported_simulation_runs: int = 0 - self.worker_send_queue_batch_size: int = 0 - self.worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() + self._worker_send_queue_batch_size: int = 0 + self._worker_send_queue: queue.SimpleQueue[SimulationRunModel] = queue.SimpleQueue() - self.dialog_button_box.accepted.connect(self.accept) - self.dialog_button_box.rejected.connect(self._handle_import_from_file_cancel_button_click) + self._dialog_button_box.accepted.connect(self.accept) + self._dialog_button_box.rejected.connect(self._handle_import_from_file_cancel_button_click) - self.import_origin_info_lbl = QtWidgets.QLabel("") - self.import_origin_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._import_origin_info_lbl = QtWidgets.QLabel("") + self._import_origin_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.num_imported_simulation_runs_info_lbl = QtWidgets.QLabel("") - self.num_imported_simulation_runs_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self._num_imported_simulation_runs_info_lbl = QtWidgets.QLabel("") + self._num_imported_simulation_runs_info_lbl.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.title_lbl) - layout.addWidget(self.import_origin_info_lbl) - layout.addWidget(self.progress_info_text_lbl) - layout.addWidget(self.error_text_lbl) + layout.addWidget(self._title_lbl) + layout.addWidget(self._import_origin_info_lbl) + layout.addWidget(self._progress_info_text_lbl) + layout.addWidget(self._error_text_lbl) layout.addStretch() aggregate_stats_controls_layout = QtWidgets.QHBoxLayout() - aggregate_stats_controls_layout.addWidget(self.num_imported_simulation_runs_info_lbl) - aggregate_stats_controls_layout.addWidget(self.total_runtime_info_text_lbl) + aggregate_stats_controls_layout.addWidget(self._num_imported_simulation_runs_info_lbl) + aggregate_stats_controls_layout.addWidget(self._total_runtime_info_text_lbl) layout.addLayout(aggregate_stats_controls_layout) - layout.addWidget(self.dialog_button_box) + layout.addWidget(self._dialog_button_box) self.setLayout(layout) def start_import( @@ -77,10 +77,10 @@ def start_import( expected_input_state_size: int, worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, ) -> None: - self.title_lbl.setText( + self._title_lbl.setText( f"Importing simulation runs from .json file with batch size {worker_send_queue_batch_size}!" ) - self.import_origin_info_lbl.setText(f"Import source: {path_to_json_file!s}") + self._import_origin_info_lbl.setText(f"Import source: {path_to_json_file!s}") if worker_send_queue_batch_size < 1 or expected_input_state_size < 1: show_and_request_ok_in_optionally_cancellable_notification( @@ -93,26 +93,26 @@ def start_import( super().reject() return - self.worker_send_queue_batch_size = worker_send_queue_batch_size + self._worker_send_queue_batch_size = worker_send_queue_batch_size # Some helpful links are the official QThread documentation but some helpful explanaitions were also found in: # - https://www.haccks.com/posts/how-to-use-qthread-correctly-p1/ # We are creating a worker object that will perform a long running operation in the future with the worker # instance being a member of the dialog class/object. Since the worker will define slots for the cancellation of # the long running operation that it executes but also emits signals, said worker needs to be implemented as a QObject that will run its own event loop # instead of subclassing QThread which executes its slots in the thread in which the QThread was created and might not execute its own event loop. - self.worker = SimulationRunJsonImportWorker( + self._worker = SimulationRunJsonImportWorker( path_to_json_file, expected_input_state_size, worker_send_queue_config=QueueConfig( - queue_instance=self.worker_send_queue, queue_batch_size=self.worker_send_queue_batch_size + queue_instance=self._worker_send_queue, queue_batch_size=self._worker_send_queue_batch_size ), ) # Create a new QThread that manages one system thread WITHOUT BEING AN ACTUAL THREAD (see: https://doc.qt.io/qtforpython-6/PySide6/QtCore/QThread.html#detailed-description) - self.worker_thread = QtCore.QThread() + self._worker_thread = QtCore.QThread() # We are now modifying the thread affinity (https://doc.qt.io/qt-6/qobject.html#thread-affinity) of the worker object to the worker thread. # This will control in which thread the received events of the worker are processed. The worker instance is still available in the dialog # so the latter can still access member variables, etc. of the former. - self.worker.moveToThread(self.worker_thread) + self._worker.moveToThread(self._worker_thread) # If the worker has completed a batch in its long running operation then it will emit a corresponding signal that should be processed by the dialog instance. # Since we have changed the thread affinity of the worker, the worker signal -> dialog slot connection needs to be marked as a queued connection so that the # signal of the worker will enqueue an entry into the event queue of the dialog and then continue its long running operation in the worker thread. Since the @@ -120,27 +120,27 @@ def start_import( # # At runtime Qt could decide at runtime whether a direct or queued connection is required based on the thread affinity between the connected signal and slot but # we try to mark this behaviour explicitly by defining the signal-slot connection as a queued connection. - self.worker.batchCompleted.connect( + self._worker.batchCompleted.connect( self._handle_imported_sim_run_batch, QtCore.Qt.ConnectionType.QueuedConnection ) # The worker thread executing the long running worker operation will still continue running after the 'finished' signal of the worker was received thus # we need to manually handle the correct cancellation of the worker thread - self.worker.finished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.finished.connect(self._handle_import_completion, QtCore.Qt.ConnectionType.QueuedConnection) # Assuming that we are correctly catching all errors of the long running worker operation in the function execution said operation (executed in the worker thread) # the worker will emit a signal containing the caught error with the worker thread still running thus again we need to manually handle its cancellation - self.worker.failed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker.failed.connect(self._handle_importer_failure, QtCore.Qt.ConnectionType.QueuedConnection) # We initially tried to move the constructor parameters of the SimulationRunJsonImportWorker to the function executing the long running operation by using a lambda # that will trigger the latter but since python lambdas seemingly do not have thread affinity (https://stackoverflow.com/a/28626472) the lambda is executed in the main # thread and thus the long running operation would be executed in the main thread blocking the GUI and potentially causing thread starvation if generated batches need to # be acknowledged. - self.worker_thread.started.connect(self.worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) + self._worker_thread.started.connect(self._worker.start_import, QtCore.Qt.ConnectionType.QueuedConnection) # Since we are manually triggering the worker thread shutdown, after the worker thread has finished the associated QThread should be deleted - self.worker_thread.finished.connect(self.worker_thread.deleteLater) + self._worker_thread.finished.connect(self._worker_thread.deleteLater) # Additionally we 'clean' up the worker and worker thread instances (by setting them to None) after the worker thread has finished - self.worker_thread.finished.connect(self._reset_workers) + self._worker_thread.finished.connect(self._reset_workers) # Only this call will actually start a new thread - self.worker_thread.start(QtCore.QThread.Priority.LowPriority) + self._worker_thread.start(QtCore.QThread.Priority.LowPriority) self._change_dialog_cancel_button_enable_state(True) # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. @@ -153,7 +153,7 @@ def reject(self) -> None: def closeEvent(self, event: QtGui.QCloseEvent) -> None: # Ask for confirmation before closing if self._handle_import_from_file_cancel_button_click(): - if not self.error_text_lbl.text(): + if not self._error_text_lbl.text(): self.accept() else: # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. @@ -167,13 +167,13 @@ def _handle_importer_failure(self, err: Exception) -> None: @QtCore.pyqtSlot(float) # type: ignore[untyped-decorator] def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: float) -> None: - if self.stop_processing_recv_batches: + if self._stop_processing_recv_batches: return n_dequeued_batch_elems: int = 0 try: - for _ in range(self.worker_send_queue_batch_size): - self.shared_simulation_runs_model.add_simulation_run_model(self.worker_send_queue.get_nowait()) + for _ in range(self._worker_send_queue_batch_size): + self._shared_simulation_runs_model.add_simulation_run_model(self._worker_send_queue.get_nowait()) n_dequeued_batch_elems += 1 except queue.Empty: # The last batch generated by the worker could contain less than the expected batch size elements thus an empty queue should not be treated as an error @@ -184,19 +184,19 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f self._update_progress_text_with_batch_info(n_dequeued_batch_elems, batch_generation_duration_in_seconds) self._accumulate_and_update_total_runtime(batch_generation_duration_in_seconds) - self.num_imported_simulation_runs += n_dequeued_batch_elems - self.num_imported_simulation_runs_info_lbl.setText( - f"Num. imported simulation runs: {self.num_imported_simulation_runs}" + self._num_imported_simulation_runs += n_dequeued_batch_elems + self._num_imported_simulation_runs_info_lbl.setText( + f"Num. imported simulation runs: {self._num_imported_simulation_runs}" ) - if self.progress_bar is not None: - self.progress_bar.setValue(self.num_generated_input_states) - self.progress_info_text_lbl.setText("") + if self._progress_bar is not None: + self._progress_bar.setValue(self.num_generated_input_states) + self._progress_info_text_lbl.setText("") QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] def _handle_import_completion(self, was_cancellation_requested: bool) -> None: - self.progress_info_text_lbl.setText("Simulation run import finished!") + self._progress_info_text_lbl.setText("Simulation run import finished!") log_info_to_console("Simulation run import finished!") # Cancelling the long running operation through a click on the cancel button of the dialog will already request a shutdown of the worker @@ -211,7 +211,7 @@ def _handle_import_completion(self, was_cancellation_requested: bool) -> None: @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_import_from_file_cancel_button_click(self) -> bool: - if self.worker is None: + if self._worker is None: return True if show_and_request_ok_in_optionally_cancellable_notification( @@ -228,12 +228,12 @@ def _handle_import_from_file_cancel_button_click(self) -> bool: return False def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: - self.progress_info_text_lbl.setText("") + self._progress_info_text_lbl.setText("") if err is not None: self._update_displayed_error_text(err, num_additionally_skipped_stack_frames_starting_from_this_function=2) try: - self.shared_simulation_runs_model.delete_all_simulation_run_models() + self._shared_simulation_runs_model.delete_all_simulation_run_models() except Exception: show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 3071d0e4..910791b7 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -208,27 +208,26 @@ def __init__( self, annotatable_quantum_computation: syrec.annotatable_quantum_computation, parent: QtCore.QObject = None ) -> None: super().__init__(parent) - self.n_data_qubits: int = annotatable_quantum_computation.num_data_qubits - self.simulation_run_models: list[SimulationRunModel] = [] - self.quantum_register_layouts: list[QuantumRegisterLayout] = ( + self._simulation_run_models: list[SimulationRunModel] = [] + self._quantum_register_layouts: list[QuantumRegisterLayout] = ( QtSimulationRunModel._record_quantum_register_layouts(annotatable_quantum_computation) ) - self.longest_quantum_register_name: str = "" - self.largest_quantum_register_size: int = 0 - self.largest_first_qubit_of_quantum_registers: int = 0 - self.annotatable_quantum_computation = annotatable_quantum_computation + self._longest_quantum_register_name: str = "" + self._largest_quantum_register_size: int = 0 + self._largest_first_qubit_of_quantum_registers: int = 0 + self._annotatable_quantum_computation = annotatable_quantum_computation - for qreg_layout in self.quantum_register_layouts: - self.longest_quantum_register_name = ( + for qreg_layout in self._quantum_register_layouts: + self._longest_quantum_register_name = ( qreg_layout.qreg_name - if len(qreg_layout.qreg_name) > len(self.longest_quantum_register_name) - else self.longest_quantum_register_name + if len(qreg_layout.qreg_name) > len(self._longest_quantum_register_name) + else self._longest_quantum_register_name ) - self.largest_quantum_register_size = max(qreg_layout.qreg_size, self.largest_quantum_register_size) + self._largest_quantum_register_size = max(qreg_layout.qreg_size, self._largest_quantum_register_size) - if len(self.quantum_register_layouts) > 0: - self.largest_first_qubit_of_quantum_registers = self.quantum_register_layouts[ - len(self.quantum_register_layouts) - 1 + if len(self._quantum_register_layouts) > 0: + self._largest_first_qubit_of_quantum_registers = self._quantum_register_layouts[ + len(self._quantum_register_layouts) - 1 ].first_qubit_of_qreg @staticmethod @@ -256,7 +255,7 @@ def _record_quantum_register_layouts( @override def rowCount(self, parent: QtCore.QModelIndex) -> int: - return 0 if parent.isValid() else len(self.simulation_run_models) + return 0 if parent.isValid() else len(self._simulation_run_models) @override def data(self, index: QtCore.QModelIndex, role: int) -> Any | None: @@ -264,22 +263,22 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any | None: return None if role == SIMULATION_RUN_IO_STATE_QT_ROLE: - return self.simulation_run_models[index.row()] + return self._simulation_run_models[index.row()] if role == QUANTUM_REGISTER_LAYOUT_QT_ROLE: - return self.quantum_register_layouts + return self._quantum_register_layouts if role == LONGEST_QUANTUM_REGISTER_NAME_QT_ROLE: - return self.longest_quantum_register_name + return self._longest_quantum_register_name if role == LARGEST_QUANTUM_REGISTER_SIZE_QT_ROLE: - return self.largest_quantum_register_size + return self._largest_quantum_register_size if role == LARGEST_FIRST_QUBIT_OF_QUANTUM_REGISTER_QT_ROLE: - return self.largest_first_qubit_of_quantum_registers + return self._largest_first_qubit_of_quantum_registers if role == ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE: - return self.annotatable_quantum_computation + return self._annotatable_quantum_computation if role == LARGEST_SIM_RUN_NUMBER_QT_ROLE: return self.rowCount(QtCore.QModelIndex()) @@ -287,14 +286,14 @@ def data(self, index: QtCore.QModelIndex, role: int) -> Any | None: return None def get_simulation_run_model(self, index: int) -> SimulationRunModel | None: - if 0 <= index < len(self.simulation_run_models): - return self.simulation_run_models[index] + if 0 <= index < len(self._simulation_run_models): + return self._simulation_run_models[index] return None def add_simulation_run_model(self, simulation_run_model: SimulationRunModel) -> None: n_simulation_runs: Final[int] = self.rowCount(QtCore.QModelIndex()) self.beginInsertRows(QtCore.QModelIndex(), n_simulation_runs, n_simulation_runs) - self.simulation_run_models.append(simulation_run_model) + self._simulation_run_models.append(simulation_run_model) self.endInsertRows() def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: @@ -302,22 +301,22 @@ def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: return False self.beginRemoveRows(QtCore.QModelIndex(), index.row(), index.row()) - self.simulation_run_models.pop(index.row()) + self._simulation_run_models.pop(index.row()) self.endRemoveRows() return True def delete_all_simulation_run_models(self) -> None: self.beginResetModel() - self.simulation_run_models.clear() + self._simulation_run_models.clear() self.endResetModel() def reset_prev_simulation_run_execution_results(self) -> None: if self.rowCount(QtCore.QModelIndex()) == 0: return - for sim_run_model in self.simulation_run_models: + for sim_run_model in self._simulation_run_models: sim_run_model.reset_result_of_execution() - self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(len(self.simulation_run_models) - 1, 0)) + self.dataChanged.emit(self.createIndex(0, 0), self.createIndex(len(self._simulation_run_models) - 1, 0)) def reset_prev_simulation_run_execution_result(self, idx_of_sim_run_to_reset: QtCore.QModelIndex) -> None: if not self.is_model_index_valid(idx_of_sim_run_to_reset): @@ -325,7 +324,7 @@ def reset_prev_simulation_run_execution_result(self, idx_of_sim_run_to_reset: Qt log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) - self.simulation_run_models[idx_of_sim_run_to_reset.row()].reset_result_of_execution() + self._simulation_run_models[idx_of_sim_run_to_reset.row()].reset_result_of_execution() self.dataChanged.emit(idx_of_sim_run_to_reset, idx_of_sim_run_to_reset) def update_edited_simulation_run_model( @@ -336,7 +335,7 @@ def update_edited_simulation_run_model( log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) - self.simulation_run_models[index.row()].update_user_editable_data( + self._simulation_run_models[index.row()].update_user_editable_data( updated_simulation_run_data.input_state, updated_simulation_run_data.expected_output_state ) self.dataChanged.emit(index, index) @@ -353,10 +352,10 @@ def update_model_using_simulation_run_result( log_error_to_console(msg, num_additionally_skipped_stack_frames_starting_from_caller_function=1) raise ValueError(msg) - self.simulation_run_models[index.row()].set_result_of_simulation_execution( + self._simulation_run_models[index.row()].set_result_of_simulation_execution( actual_output_state, do_expected_and_actual_output_states_match, execution_runtime_in_ms ) self.dataChanged.emit(index, index) def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: - return index.isValid() and index.row() >= 0 and index.row() < len(self.simulation_run_models) # type: ignore[no-any-return] + return index.isValid() and index.row() >= 0 and index.row() < len(self._simulation_run_models) # type: ignore[no-any-return] diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index 92e494be..0db313ea 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -33,7 +33,7 @@ def __init__( self, expected_input_state_size: int, worker_send_queue_config: QueueConfig[SimulationRunModel] ) -> None: super().__init__(worker_send_queue_config) - self.expected_input_state_size: Final[int] = expected_input_state_size + self._expected_input_state_size: Final[int] = expected_input_state_size @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_generation(self) -> None: @@ -45,7 +45,7 @@ def start_generation(self) -> None: self._assert_valid_user_provided_parameter_values() # We are assuming that the caller has validated that the 2^x operation will not overflow the maximum value of a 32 bit integer. - n_states_to_generate: Final[int] = 2**self.expected_input_state_size + n_states_to_generate: Final[int] = 2**self._expected_input_state_size batch_start_timestamp = AllInputStatesGeneratorWorker.get_timestamp() while ( not self.is_cancellation_requested() @@ -63,7 +63,7 @@ def start_generation(self) -> None: self.send_queue.put_nowait( AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( - self.expected_input_state_size, integer_encoding_input_state + self._expected_input_state_size, integer_encoding_input_state ) ) @@ -90,8 +90,8 @@ def start_generation(self) -> None: @override def _assert_valid_user_provided_parameter_values(self) -> None: super()._assert_valid_user_provided_parameter_values() - if self.expected_input_state_size < 1: - msg = f"Expected input state size must be a positive integer but was actually {self.expected_input_state_size}!" + if self._expected_input_state_size < 1: + msg = f"Expected input state size must be a positive integer but was actually {self._expected_input_state_size}!" raise ValueError(msg) @staticmethod diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index b2ceb5ce..84face33 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -45,8 +45,8 @@ def __init__( worker_recv_queue_config=worker_recv_queue_config, ) - self.associated_stringified_syrec_program = associated_stringified_syrec_program - self.path_to_json_file: Final[Path] = path_to_json_file + self._associated_stringified_syrec_program: Final[str] = associated_stringified_syrec_program + self._path_to_json_file: Final[Path] = path_to_json_file @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_export(self) -> None: @@ -65,9 +65,9 @@ def start_export(self) -> None: self._assert_valid_user_provided_parameter_values() batch_start_timestamp = SimulationRunJsonExportWorker.get_timestamp() - with self.path_to_json_file.open("w") as file: + with self._path_to_json_file.open("w") as file: file.write( - f'{{"inputCircuit":"{SimulationRunJsonExportWorker.convert_to_single_line_string(self.associated_stringified_syrec_program)}", "simulationRuns":[' + f'{{"inputCircuit":"{SimulationRunJsonExportWorker.convert_to_single_line_string(self._associated_stringified_syrec_program)}", "simulationRuns":[' ) while ( diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 455a8e21..f07f33d6 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -54,8 +54,8 @@ def __init__( worker_send_queue_config: QueueConfig[SimulationRunModel], ) -> None: super().__init__(worker_send_queue_config) - self.path_to_json_file = path_to_json_file - self.expected_input_state_size = expected_input_state_size + self._path_to_json_file: Final[Path] = path_to_json_file + self._expected_input_state_size: Final[int] = expected_input_state_size @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_import(self) -> None: @@ -67,7 +67,7 @@ def start_import(self) -> None: n_remaining_input_states_to_import_in_batch: int = self._send_queue_batch_size # Reading bytes instead of strings leads to better parser performance - with self.path_to_json_file.open("rb") as file: + with self._path_to_json_file.open("rb") as file: # The json parser starts at the first element matching the prefix which in our case starts at an element with key 'simulationRuns' that is expected # to be a property of the top level element (i.e. the path to the 'simulationRuns' element is relative to the top level element). # Additionally, with the postfix '.item', only the entries of a JSON array are processed. If the 'simulationRuns' entry value is no @@ -85,7 +85,7 @@ def start_import(self) -> None: self.send_queue.put_nowait( SimulationRunJsonImportWorker._try_deserialize_simulation_run( - self.expected_input_state_size, arr_elem + self._expected_input_state_size, arr_elem ) ) n_remaining_input_states_to_import_in_batch -= 1 @@ -124,8 +124,8 @@ def start_import(self) -> None: @override def _assert_valid_user_provided_parameter_values(self) -> None: super()._assert_valid_user_provided_parameter_values() - if self.expected_input_state_size < 1: - msg = f"Expected input state size must be a positive integer but was actually {self.expected_input_state_size}!" + if self._expected_input_state_size < 1: + msg = f"Expected input state size must be a positive integer but was actually {self._expected_input_state_size}!" raise ValueError(msg) @staticmethod diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index c15276b6..82c548ae 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -45,9 +45,11 @@ def __init__( worker_send_queue_config=worker_send_queue_config, worker_recv_queue_config=worker_recv_queue_config, ) - self._expected_input_state_size = _expected_input_state_size - self._annotatable_quantum_computation = annotatable_quantum_computation - self._should_stop_at_first_output_state_mismatch: bool = stop_at_first_output_state_mismatch + self._expected_input_state_size: Final[int] = _expected_input_state_size + self._annotatable_quantum_computation: Final[syrec.annotatable_quantum_computation] = ( + annotatable_quantum_computation + ) + self._should_stop_at_first_output_state_mismatch: Final[bool] = stop_at_first_output_state_mismatch @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def start_simulations(self) -> None: From 8ea4637fced70b0271a5ab705b7feb258cd1d362 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 19:15:10 +0100 Subject: [PATCH 74/88] Fixed various smaller selection change bugs that effected the simulation run execution and edit controls --- .../quantum_circuit_simulation_dialog.py | 84 ++++++++++++++++--- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index e3300f04..175ffef4 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -59,7 +59,7 @@ SOME_SIM_RUNS_TAB_WIDGET_NAME: Final[str] = "some_sim_runs_tab" ALL_SIM_RUNS_TAB_WIDGET_NAME: Final[str] = "all_sim_runs_tab" -LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME: Final[str] = "LOAD_SIM_RUNS_FROM_FILE_TAB" +LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME: Final[str] = "load_sim_runs_from_file_tab" # TODO: Add simulation runs button is sometimes not enabled? @@ -313,14 +313,21 @@ def closeEvent(self, event: QtGui.QCloseEvent) -> None: @QtCore.pyqtSlot(QtCore.QItemSelection, QtCore.QItemSelection) # type: ignore[untyped-decorator] def handle_simulation_run_selection_change( - self, selected: QtCore.QItemSelection, deselected: QtCore.QItemSelection + self, + selected: QtCore.QItemSelection, + deselected: QtCore.QItemSelection, + optional_tab_widget_to_apply_selection_change_to: QtWidgets.QTabWidget | None = None, ) -> None: # We want to only update the simulation run execution controls in case that a selected simulation run was deselected without selecting a new simulation run or vice versa. # In all other cases leave the enabled state of the simulation run controls the same by simply returning from this function. if selected.isEmpty() == deselected.isEmpty(): return - optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.currentWidget() + optional_curr_active_tab_widget: QtWidgets.QWidget | None = ( + self._simulation_runs_tab_widget.currentWidget() + if optional_tab_widget_to_apply_selection_change_to is None + else optional_tab_widget_to_apply_selection_change_to + ) if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, required_widgets=[optional_curr_active_tab_widget], @@ -700,8 +707,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i ): self._simulation_runs_tab_widget.setCurrentIndex(self._prev_active_simulation_runs_tab_idx) return - self._simulation_runs_model.delete_all_simulation_run_models() - + self._clear_simulation_run_list_and_backing_model(prev_active_tab_widget) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(to_be_switched_to_tab_widget, False) if to_be_switched_to_tab_widget.objectName() == ALL_SIM_RUNS_TAB_WIDGET_NAME: @@ -715,7 +721,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i log_contents=False, ): self._simulation_runs_tab_widget.setCurrentIndex(self._prev_active_simulation_runs_tab_idx) - self.set_default_simulation_run_modification_buttons_enabled_state() + self.set_default_simulation_run_modification_buttons_enabled_state(prev_active_tab_widget) return self.handle_open_and_start_all_input_states_generator_dialog( @@ -727,7 +733,6 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i ) # TODO: Error handling sim_run_exec_mode_dropdown.setCurrentIndex(self._shared_selected_sim_run_execution_mode_dropdown_index) - self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) self._prev_active_simulation_runs_tab_idx = switched_to_tab_index @@ -776,7 +781,7 @@ def _open_simulation_runs_execution_dialog(self) -> None: "QtWidgets.QListView", optional_simulation_runs_list_view ) sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( - "QtWidgets.QListView", optional_sim_run_exec_mode_dropdown + "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown ) selected_sim_run_model_idx: QtCore.QModelIndex | None = None @@ -891,6 +896,9 @@ def open_import_from_file_dialog(self) -> None: return selected_filename_lbl: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_selected_filename_lbl) + curr_active_tab_widget: Final[QtWidgets.QTabWidget] = cast( + "QtWidgets.QTabWidget", optional_curr_active_tab_widget + ) if self._simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: if not show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, @@ -901,7 +909,7 @@ def open_import_from_file_dialog(self) -> None: log_contents=False, ): return - self._simulation_runs_model.delete_all_simulation_run_models() + self._clear_simulation_run_list_and_backing_model(curr_active_tab_widget) self._simulation_run_import_from_file_dialog = SimulationRunJsonImportDialog( parent=self, shared_simulation_runs_model=self._simulation_runs_model @@ -982,7 +990,9 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( run_simulation_runs_btn.setEnabled(should_controls_be_enabled) sim_run_exec_mode_dropdown.setEnabled(should_controls_be_enabled) - save_simulation_runs_to_file_btn.setEnabled(should_controls_be_enabled) + save_simulation_runs_to_file_btn.setEnabled( + should_controls_be_enabled and not self._did_syrec_program_contain_comments + ) def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( self, tab_widget: QtWidgets.QWidget, is_simulation_run_selected: bool @@ -1015,7 +1025,9 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_o sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown ) - cast("QtWidgets.QPushButton", optional_save_simulation_runs_to_file_btn) + save_simulation_runs_to_file_btn: Final[QtWidgets.QPushButton] = cast( + "QtWidgets.QPushButton", optional_save_simulation_runs_to_file_btn + ) curr_sim_run_exec_mode: Final[SimulationRunExecutionMode | None] = sim_run_exec_mode_dropdown.currentData() if curr_sim_run_exec_mode is None: @@ -1038,7 +1050,14 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_o # Added guard to handle new simulation run execution modes assert_never(curr_sim_run_exec_mode) - def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: + sim_run_exec_mode_dropdown.setEnabled(not is_simulation_run_selected) + save_simulation_runs_to_file_btn.setEnabled( + not self._did_syrec_program_contain_comments and not is_simulation_run_selected + ) + + def set_default_simulation_run_modification_buttons_enabled_state( + self, associated_tab_widget: QtWidgets.QTabWidget + ) -> None: optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( self._simulation_runs_tab_widget.currentIndex() ) @@ -1077,7 +1096,7 @@ def set_default_simulation_run_modification_buttons_enabled_state(self) -> None: edit_sim_run_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_edit_sim_run_btn) delete_sim_run_btn: Final[QtWidgets.QPushButton] = cast("QtWidgets.QPushButton", optional_delete_sim_run_btn) - add_sim_run_btn.setEnabled(True) + add_sim_run_btn.setEnabled(associated_tab_widget.objectName() != LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME) edit_sim_run_btn.setEnabled(False) delete_sim_run_btn.setEnabled(False) @@ -1108,3 +1127,42 @@ def handle_simulation_run_execution_mode_selection_change(self, selected_sim_run self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( curr_active_tab_widget, len(simulation_runs_list_view.selectedIndexes()) == 1 ) + + def _clear_simulation_run_list_and_backing_model( + self, tab_widget_containing_list_view: QtWidgets.QTabWidget + ) -> None: + optional_simulation_runs_list_view: QtWidgets.QWidget | None = tab_widget_containing_list_view.findChild( + QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME + ) + + if not assert_all_required_widgets_found_or_close_dialog( + error_notification_parent_widget=self, + required_widgets=[optional_simulation_runs_list_view], + error_dialog_content="Failed to locate all required simulation run list view widget during reset of simulation run list model", + ): + return + + simulation_runs_list_view: Final[QtWidgets.QListView] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view + ) + selection_prior_to_model_reset: Final[QtCore.QItemSelection] = ( + simulation_runs_list_view.selectionModel().selection() + ) + + # During the model reset the beginModelReset emitted by the simulation runs model will disconnect the associated QListView from the selection model, + # after which all elements in the model will be deleted followed by a reconnect of the QListView to the selection model (by the endModelReset signal of the model). + # When the model is reset, the views selection model throws away every index that became invalid (see https://doc.qt.io/qt-6/qabstractitemmodel.html#modelReset), so after endResetModel() the selection is already empty; + # Since the selection is already empty, manually resetting the selection of the QListView (via clearSelection() and setCurrentIndex(QtCore.QModelIndex())) will be silently ignored since the selection is already empty thus + # the connected slot for the selectionChanged signal of the QListView is not invoked and must be invoked manually. + self._simulation_runs_model.delete_all_simulation_run_models() + + selection_after_model_reset: Final[QtCore.QItemSelection] = ( + simulation_runs_list_view.selectionModel().selection() + ) + # To tab changed signal is emitted after the tab already changed thus the QTabWidget.currentWidget() will return the already switched to tab widget while we want to reset the selection + # in the switched from tab widget. + self.handle_simulation_run_selection_change( + selection_after_model_reset, + selection_prior_to_model_reset, + optional_tab_widget_to_apply_selection_change_to=tab_widget_containing_list_view, + ) From 859efae925ee01d2d2284758245b6c0d1a5c821e Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 19:58:04 +0100 Subject: [PATCH 75/88] Added back missing edited simulation run model index instance variable to simulation run editor dialog. Added sim run selection info label to quantum circuit simulation dialog --- .../quantum_circuit_simulation_dialog.py | 36 +++++++++++++++---- .../dialogs/simulation_run_editor_dialog.py | 34 +++++++++--------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 175ffef4..461f4dba 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -62,7 +62,6 @@ LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME: Final[str] = "load_sim_runs_from_file_tab" -# TODO: Add simulation runs button is sometimes not enabled? class SimulationRunExecutionMode(Enum): RUN_ALL = 0 RUN_ALL_STOP_AT_FIRST_FAILURE = 1 @@ -181,7 +180,7 @@ def initialize_simulation_runs_tab_widget( tab_wrapper_widget_layout.addSpacing(manual_y_space_size) # BEGIN: Create simulation runs list view Qt elements - simulation_runs_list_view: QtWidgets.QListView = QtWidgets.QListView(objectName=SIMULATION_RUNS_LIST_VIEW_NAME) + simulation_runs_list_view = QtWidgets.QListView(objectName=SIMULATION_RUNS_LIST_VIEW_NAME) simulation_runs_list_view.setModel(shared_simulation_runs_model) simulation_runs_list_view.setItemDelegate(SimulationRunOverviewStyledItemDelegate()) simulation_runs_list_view.setUniformItemSizes(True) @@ -200,6 +199,16 @@ def initialize_simulation_runs_tab_widget( simulation_runs_list_scrollarea.setWidget(simulation_runs_list_view) simulation_runs_list_scrollarea.setWidgetResizable(True) tab_wrapper_widget_layout.addWidget(simulation_runs_list_scrollarea) + + simulation_runs_list_selection_info_layout = QtWidgets.QHBoxLayout() + simulation_runs_list_selection_info_lbl: QtWidgets.QLabel = QtWidgets.QLabel( + "Select simulation runs with a left click while unselecting them with CTRL+left click" + ) + simulation_runs_list_selection_info_lbl.setStyleSheet("QLabel { color : gray; }") + simulation_runs_list_selection_info_layout.addStretch() + simulation_runs_list_selection_info_layout.addWidget(simulation_runs_list_selection_info_lbl) + simulation_runs_list_selection_info_layout.addStretch() + tab_wrapper_widget_layout.addLayout(simulation_runs_list_selection_info_layout) # END: Create simulation runs list view Qt elements # BEGIN: Create simulation runs list modification Qt elements @@ -731,7 +740,10 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown_in_switched_to_tab ) - # TODO: Error handling + # Setting an invalid current index will not throw an error but sets the current index to -1. + # With the assumption that the selectable simulation run execution modes in the ComboBox do not change at runtime + # and our override of the selection change slot of the ComboBox not changing the selected index then invalid dropdown indices + # can only stem from this setter call (assuming that no other sets are added to this class in the future). sim_run_exec_mode_dropdown.setCurrentIndex(self._shared_selected_sim_run_execution_mode_dropdown_index) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) self._prev_active_simulation_runs_tab_idx = switched_to_tab_index @@ -786,8 +798,16 @@ def _open_simulation_runs_execution_dialog(self) -> None: selected_sim_run_model_idx: QtCore.QModelIndex | None = None curr_sim_run_exec_mode: Final[SimulationRunExecutionMode | None] = sim_run_exec_mode_dropdown.currentData() - # TODO: - assert curr_sim_run_exec_mode is not None + if curr_sim_run_exec_mode is None: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=self, + message_box_title="Unknown selected simulation run execution mode!", + message_box_content="Failed to determine selected simulation run execution mode while initializing simulation run execution dialog!", + is_cancellable=False, + ) + return + if curr_sim_run_exec_mode == SimulationRunExecutionMode.RUN_SINGLE: curr_num_selected_simulation_runs: Final[int] = len(simulation_runs_list_view.selectedIndexes()) # We are assuming that the QListView only supports single item selection but QListView only offers fetch of all selected indices @@ -1035,7 +1055,7 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_o message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Failed to determine simulation run execution mode", - message_box_content="Failed to determine simulation run execution mode while changing anbled state of simulation run execution controls.", + message_box_content="Failed to determine simulation run execution mode while changing enabled state of simulation run execution controls.", is_cancellable=True, log_contents=False, ) @@ -1052,7 +1072,9 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_o sim_run_exec_mode_dropdown.setEnabled(not is_simulation_run_selected) save_simulation_runs_to_file_btn.setEnabled( - not self._did_syrec_program_contain_comments and not is_simulation_run_selected + not self._did_syrec_program_contain_comments + and not is_simulation_run_selected + and self._simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0 ) def set_default_simulation_run_modification_buttons_enabled_state( diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index c94545d2..ed57371a 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -130,8 +130,10 @@ def __init__( parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) + self.simulation_run_model_index: Final[QtCore.QModelIndex] = simulation_run_model_index + self._failed_due_to_internal_error: bool = False - self._edited_simulation_run_model: SimulationRunModel = copy_of_reference_edit_sim_run_model + self.edited_simulation_run_model: SimulationRunModel = copy_of_reference_edit_sim_run_model self._qreg_layouts: list[QuantumRegisterLayout] = simulation_run_model_index.data( QUANTUM_REGISTER_LAYOUT_QT_ROLE @@ -140,12 +142,12 @@ def __init__( ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE ) - initial_input_state: syrec.n_bit_values_container = self._edited_simulation_run_model.input_state + initial_input_state: syrec.n_bit_values_container = self.edited_simulation_run_model.input_state initial_expected_output_state: syrec.n_bit_values_container | None = ( - self._edited_simulation_run_model.expected_output_state + self.edited_simulation_run_model.expected_output_state ) initial_actual_output_state: syrec.n_bit_values_container | None = ( - self._edited_simulation_run_model.actual_output_state + self.edited_simulation_run_model.actual_output_state ) # Ensure the dialog is deleted when closed this may not be strictly necessary but seems to be a good cleanup practice @@ -176,7 +178,7 @@ def __init__( init_expected_output_state_button = QtWidgets.QPushButton( "Init output state" - if self._edited_simulation_run_model.expected_output_state is None + if self.edited_simulation_run_model.expected_output_state is None else "Clear output state", objectName=QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, ) @@ -482,7 +484,7 @@ def _handle_input_state_qubit_value_checkbox_state_change( updated_qubit_value, return_as_high_low_state=True ) - if not self._edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): + if not self.edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -543,7 +545,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( updated_qubit_value, return_as_high_low_state=True ) - if self._edited_simulation_run_model.expected_output_state is None: + if self.edited_simulation_run_model.expected_output_state is None: stringified_updated_qubit_value = SimulationRunEditorDialog._stringify_qubit_value( None, return_as_high_low_state=True ) @@ -552,7 +554,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( ) return - if not self._edited_simulation_run_model.update_expected_output_state_qubit_value( + if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( associated_qubit, updated_qubit_value ): show_and_request_ok_in_optionally_cancellable_notification( @@ -1151,12 +1153,12 @@ def _handle_init_expected_output_state_button_click(self) -> None: "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button ) - should_reset_output_state: bool = self._edited_simulation_run_model.expected_output_state is not None + should_reset_output_state: bool = self.edited_simulation_run_model.expected_output_state is not None if should_reset_output_state: - self._edited_simulation_run_model.expected_output_state = None + self.edited_simulation_run_model.expected_output_state = None expected_output_state_value_toggle_button.setText("Init output state") else: - self._edited_simulation_run_model.initialize_expected_output_state_as_copy_of_input_state() + self.edited_simulation_run_model.initialize_expected_output_state_as_copy_of_input_state() expected_output_state_value_toggle_button.setText("Clear output state") for qreg_layout in self._qreg_layouts: @@ -1190,7 +1192,7 @@ def _handle_init_expected_output_state_button_click(self) -> None: # and since no reset is requested, i.e. we initialized the expected output state member variable which in turn means it is not None at this point. qreg_output_state_input_field.setText( SimulationRunEditorDialog._stringify_some_qubits_of_n_bit_values_container( - self._edited_simulation_run_model.expected_output_state, # type: ignore[arg-type] + self.edited_simulation_run_model.expected_output_state, # type: ignore[arg-type] qreg_layout.first_qubit_of_qreg, qreg_layout.qreg_size, ) @@ -1214,7 +1216,7 @@ def _handle_init_expected_output_state_button_click(self) -> None: associated_qubit_value_checkbox = cast("QtWidgets.QCheckBox", optional_associated_qubit_value_checkbox) qubit_value: bool | None = ( - self._edited_simulation_run_model.expected_output_state.test(qubit) # type: ignore[union-attr] + self.edited_simulation_run_model.expected_output_state.test(qubit) # type: ignore[union-attr] if not should_reset_output_state else None ) @@ -1316,7 +1318,7 @@ def _handle_input_or_output_state_text_change( ) curr_not_edited_input_text_field.setEnabled( are_stringified_qreg_contents_valid - and (self._edited_simulation_run_model.expected_output_state is not None or not is_editing_input_state) + and (self.edited_simulation_run_model.expected_output_state is not None or not is_editing_input_state) ) if are_stringified_qreg_contents_valid: @@ -1449,7 +1451,7 @@ def _handle_input_or_output_state_text_change( not_edited_input_state_text_field.setEnabled(should_state_controls_be_visible) not_edited_output_state_text_field.setEnabled( should_state_controls_be_visible - and self._edited_simulation_run_model.expected_output_state is not None + and self.edited_simulation_run_model.expected_output_state is not None ) not_edited_qreg_qubit_values_edit_toggle_button.setEnabled(should_state_controls_be_visible) @@ -1488,7 +1490,7 @@ def _handle_input_or_output_state_text_change( not_edited_input_state_qubit_checkbox.setEnabled(are_stringified_qreg_contents_valid) not_edited_output_state_qubit_checkbox.setEnabled( are_stringified_qreg_contents_valid - and self._edited_simulation_run_model.expected_output_state is not None + and self.edited_simulation_run_model.expected_output_state is not None ) @staticmethod From 0fe9463d138b19f6f92dc12588a34b2a767fcbe2 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 21:45:23 +0100 Subject: [PATCH 76/88] Refactored some class variables to instance variable in SyReCEditor python class --- python/mqt/syrec/syrec_editor.py | 34 ++++++++++++++------------------ 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/python/mqt/syrec/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 7e1a6679..4345f20d 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -290,22 +290,20 @@ def wheelEvent(self, event): # noqa: N802 class SyReCEditor(QtWidgets.QWidget): # type: ignore[misc] - widget: CodeEditor | None = None - annotatable_quantum_computation: AnnotatableQuantumComputation | None = None - build_successful: Callable[[AnnotatableQuantumComputation], None] | None = None - build_failed: Callable[[str], None] | None = None - before_build: Callable[[], None] | None = None - parser_failed: Callable[[str], None] | None = None - synthesis_failed: Callable[[str], None] | None = None - - cost_aware_synthesis = 0 - line_aware_synthesis = 0 - def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__() - self.parent = parent - self.code_editor_widget: CodeEditor = CodeEditor(self.parent) + self.annotatable_quantum_computation: AnnotatableQuantumComputation | None = None + self.build_successful: Callable[[AnnotatableQuantumComputation], None] | None = None + self.build_failed: Callable[[str], None] | None = None + self.before_build: Callable[[], None] | None = None + self.parser_failed: Callable[[str], None] | None = None + self.synthesis_failed: Callable[[str], None] | None = None + + self.cost_aware_synthesis = 0 + self.line_aware_synthesis = 0 + + self.code_editor_widget: CodeEditor = CodeEditor(parent) self.code_editor_widget.setFont(QtGui.QFont("Monospace", 10, QtGui.QFont.Weight.Normal)) self.code_editor_widget.highlighter = SyReCHighlighter(self.code_editor_widget.document()) self.quantum_circuit_sim_runs_dialog: QuantumCircuitSimulationDialog | None = None @@ -329,12 +327,10 @@ def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: self.setLayout(self.layout) def setup_actions(self) -> None: - self.open_action = QtGui.QAction(QtGui.QIcon.fromTheme("document-open"), "&Open...", self.parent) - self.build_action = QtGui.QAction(QtGui.QIcon.fromTheme("media-playback-start"), "&Build...", self.parent) - self.sim_action = QtGui.QAction( - QtGui.QIcon.fromTheme("x-office-spreadsheet"), "&Sim...", self.parent - ) # system-run - self.stat_action = QtGui.QAction(QtGui.QIcon.fromTheme("applications-other"), "&Stats...", self.parent) + self.open_action = QtGui.QAction(QtGui.QIcon.fromTheme("document-open"), "&Open...", self) + self.build_action = QtGui.QAction(QtGui.QIcon.fromTheme("media-playback-start"), "&Build...", self) + self.sim_action = QtGui.QAction(QtGui.QIcon.fromTheme("x-office-spreadsheet"), "&Sim...", self) # system-run + self.stat_action = QtGui.QAction(QtGui.QIcon.fromTheme("applications-other"), "&Stats...", self) self.buttonCostAware = QtWidgets.QRadioButton("Cost-aware synthesis", self) self.buttonCostAware.toggled.connect(self.item_selected) From 5357109a6251849160e2b843ffcbe545bf6d390a Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 22:35:12 +0100 Subject: [PATCH 77/88] Added temporary workaround for quantum registers of ancillary variables not being considered as ancillary qubits --- .../quantum_circuit_simulation_dialog.py | 48 ++++++++++++++++--- .../dialogs/simulation_run_dialog.py | 13 +++-- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 2ef9f366..3c494dfb 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -26,7 +26,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets -from mqt.syrec import NBitValuesContainer +from mqt.syrec import NBitValuesContainer, QubitLabelType from .message_box_utils import MessageBoxType, show_and_request_ok_in_optionally_cancellable_notification from .simulation_view.dialogs.all_input_states_generator_dialog import AllInputStatesGeneratorDialog @@ -110,7 +110,11 @@ def __init__( self._simulation_run_export_to_file_dialog: SimulationRunJsonExportDialog | None = None self._simulation_run_dialog: SimulationRunDialog | None = None - self._expected_input_output_state_size: Final[int] = annotatable_quantum_computation.num_data_qubits + self._expected_input_output_state_size: Final[int] = ( + QuantumCircuitSimulationDialog._determine_num_non_ancillary_qubits( + self._annotatable_quantum_computation, potential_error_dialog_parent=self + ) + ) self._simulation_runs_model: QtSimulationRunModel = QtSimulationRunModel(annotatable_quantum_computation, self) self._shared_selected_sim_run_execution_mode_dropdown_index: int = 0 self._prev_active_simulation_runs_tab_idx: int = 0 @@ -721,7 +725,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(to_be_switched_to_tab_widget, False) if to_be_switched_to_tab_widget.objectName() == ALL_SIM_RUNS_TAB_WIDGET_NAME: - n_input_state_combinations: int = 2**self._annotatable_quantum_computation.num_data_qubits + n_input_state_combinations: int = 2**self._expected_input_output_state_size if not show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, message_box_parent=self, @@ -734,9 +738,7 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self.set_default_simulation_run_modification_buttons_enabled_state(prev_active_tab_widget) return - self.handle_open_and_start_all_input_states_generator_dialog( - self._annotatable_quantum_computation.num_data_qubits - ) + self.handle_open_and_start_all_input_states_generator_dialog(self._expected_input_output_state_size) sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown_in_switched_to_tab @@ -828,6 +830,7 @@ def _open_simulation_runs_execution_dialog(self) -> None: parent=self, shared_simulation_runs_model=self._simulation_runs_model, annotatable_quantum_computation=self._annotatable_quantum_computation, + expected_input_output_state_size=self._expected_input_output_state_size, ) self._simulation_run_dialog.finished.connect(self.handle_simulation_runs_dialog_close) self._simulation_run_dialog.show() @@ -939,7 +942,7 @@ def open_import_from_file_dialog(self) -> None: self._simulation_run_import_from_file_dialog.show() self._simulation_run_import_from_file_dialog.start_import( Path(selected_filename_lbl.text()), - expected_input_state_size=self._annotatable_quantum_computation.num_data_qubits, + expected_input_state_size=self._expected_input_output_state_size, ) @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] @@ -1189,3 +1192,34 @@ def _clear_simulation_run_list_and_backing_model( selection_prior_to_model_reset, optional_tab_widget_to_apply_selection_change_to=tab_widget_containing_list_view, ) + + # This method is only a temporary workaround for the quantum registers created for ancillary qubits not being marked as ancillary in the annotatable quantum computation (Date of comment 04.02.2026) + @staticmethod + def _determine_num_non_ancillary_qubits( + annotatable_quantum_computation: AnnotatableQuantumComputation, potential_error_dialog_parent: QtWidgets.QWidget + ) -> int: + num_ancillary_qubits: int = 0 + for qubit in range(annotatable_quantum_computation.num_data_qubits): + fetched_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( + qubit, QubitLabelType.internal + ) + if fetched_qubit_label is None: + show_and_request_ok_in_optionally_cancellable_notification( + message_box_type=MessageBoxType.ERROR, + message_box_parent=potential_error_dialog_parent, + message_box_title="Failed to determine qubit label", + message_box_content=f"Failed to determine internal qubit label for qubit {qubit}!", + is_cancellable=False, + ) + return 0 + num_ancillary_qubits += int( + not QuantumCircuitSimulationDialog._does_qubit_label_start_with_internal_qubit_label_prefix( + fetched_qubit_label + ) + ) + return num_ancillary_qubits + + # This method is only a temporary workaround for the quantum registers created for ancillary qubits not being marked as ancillary in the annotatable quantum computation (Date of comment 04.02.2026) + @staticmethod + def _does_qubit_label_start_with_internal_qubit_label_prefix(qubit_label: str) -> bool: + return qubit_label.startswith("__q") diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 660830c3..5f215dc1 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -64,6 +64,7 @@ def __init__( parent: QtWidgets.QWidget, shared_simulation_runs_model: QtSimulationRunModel, annotatable_quantum_computation: AnnotatableQuantumComputation, + expected_input_output_state_size: int, ) -> None: super().__init__( parent, @@ -74,6 +75,7 @@ def __init__( user_provided_dialog_size=SimulationRunDialog.get_default_big_dialog_size(), ) self._annotatable_quantum_computation: Final[AnnotatableQuantumComputation] = annotatable_quantum_computation + self._expected_input_state_size: Final[int] = expected_input_output_state_size self._optional_filtered_shared_sim_run_model: SimulationRunFilterModel | None = None self._stop_at_first_output_state_mismatch: bool = False self._num_executed_simulation_runs: int = 0 @@ -145,13 +147,16 @@ def start_simulations( sim_run_model_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, sim_run_result_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, ) -> None: - expected_input_state_size: Final[int] = self._annotatable_quantum_computation.num_data_qubits - if sim_run_model_queue_batch_size < 1 or sim_run_result_queue_batch_size < 1 or expected_input_state_size < 1: + if ( + sim_run_model_queue_batch_size < 1 + or sim_run_result_queue_batch_size < 1 + or self._expected_input_state_size < 1 + ): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, message_box_title="Invalid input parameters detected", - message_box_content=f"Expected simulation run model queue batch size (value={sim_run_model_queue_batch_size}), simulation run result queue batch size (value={sim_run_result_queue_batch_size}) as well as the expected input state size (value={expected_input_state_size}) to be positive integers!", + message_box_content=f"Expected simulation run model queue batch size (value={sim_run_model_queue_batch_size}), simulation run result queue batch size (value={sim_run_result_queue_batch_size}) as well as the expected input state size (value={self._expected_input_state_size}) to be positive integers!", is_cancellable=False, ) super().reject() @@ -196,7 +201,7 @@ def start_simulations( # To avoid redundant comments we refer to the SimulationRunJsonImportDialog.start_import(...) function for details regarding the worker-object to perform a long running operation self._worker = SimulationRunWorker( self._annotatable_quantum_computation, - expected_input_state_size, + self._expected_input_state_size, self._stop_at_first_output_state_mismatch, worker_recv_queue_config=QueueConfig( queue_instance=self._sim_run_model_queue, queue_batch_size=sim_run_model_queue_batch_size From 6c9fc032a96636f627a604ec68f3891e13511d92 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 23:52:57 +0100 Subject: [PATCH 78/88] CodeRabbit trivial or minor issue suggestions --- python/mqt/syrec/logger_utils.py | 25 ++++++++++++------- .../quantum_circuit_simulation_dialog.py | 7 +++--- .../dialogs/base_progress_dialog.py | 6 +++++ .../dialogs/simulation_run_dialog.py | 7 ++++-- .../simulation_run_json_import_dialog.py | 2 +- .../simulation_view/simulation_run_model.py | 2 +- ...ase_simulation_run_styled_item_delegate.py | 2 ++ ...ation_run_overview_styled_item_delegate.py | 2 ++ .../all_input_states_generator_worker.py | 4 ++- .../simulation_run_json_import_worker.py | 3 +-- .../workers/simulation_run_worker.py | 2 +- 11 files changed, 41 insertions(+), 21 deletions(-) diff --git a/python/mqt/syrec/logger_utils.py b/python/mqt/syrec/logger_utils.py index a925ee5b..30858c5b 100644 --- a/python/mqt/syrec/logger_utils.py +++ b/python/mqt/syrec/logger_utils.py @@ -14,12 +14,19 @@ def configure_default_console_logger() -> None: # For supported log message formats (see https://docs.python.org/3/library/logging.html#formatter-objects) - logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s-%(levelname)s-[%(filename)s:%(lineno)s - %(funcName)20s()]-%(message)s", - datefmt="%H:%M:%S", - force=True, + logger = logging.getLogger(DEFAULT_LOGGER_NAME) + if logger.handlers: + return + + handler = logging.StreamHandler() + handler.setFormatter( + logging.Formatter( + "%(asctime)s-%(levelname)s-[%(filename)s:%(lineno)s - %(funcName)20s()]-%(message)s", + datefmt="%H:%M:%S", + ) ) + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) def log_debug_to_console( @@ -27,7 +34,7 @@ def log_debug_to_console( ) -> None: logger = logging.getLogger(DEFAULT_LOGGER_NAME) # We do not want to log the origin of the helper function but of the caller of the function itself. - # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + # The origin of the log entry is set to the stack frame of the caller but can be advanced further up in the stack trace logger.debug(msg=info_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) @@ -36,7 +43,7 @@ def log_info_to_console( ) -> None: logger = logging.getLogger(DEFAULT_LOGGER_NAME) # We do not want to log the origin of the helper function but of the caller of the function itself. - # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + # The origin of the log entry is set to the stack frame of the caller but can be advanced further up in the stack trace logger.info(msg=info_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) @@ -45,7 +52,7 @@ def log_warning_to_console( ) -> None: logger = logging.getLogger(DEFAULT_LOGGER_NAME) # We do not want to log the origin of the helper function but of the caller of the function itself. - # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + # The origin of the log entry is set to the stack frame of the caller but can be advanced further up in the stack trace logger.warning(msg=warn_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) @@ -54,5 +61,5 @@ def log_error_to_console( ) -> None: logger = logging.getLogger(DEFAULT_LOGGER_NAME) # We do not want to log the origin of the helper function but of the caller of the function itself. - # The origin of the log entry is set to the stack frame of the caller but can be advances further up in the stack trace + # The origin of the log entry is set to the stack frame of the caller but can be advanced further up in the stack trace logger.error(msg=err_msg, stacklevel=(2 + num_additionally_skipped_stack_frames_starting_from_caller_function)) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 3c494dfb..1bcaccfc 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -791,7 +791,6 @@ def _open_simulation_runs_execution_dialog(self) -> None: ): return - cast("QtWidgets.QTabWidget", optional_curr_active_tab_widget) simulation_runs_list_view: Final[QtWidgets.QListView] = cast( "QtWidgets.QListView", optional_simulation_runs_list_view ) @@ -1198,7 +1197,7 @@ def _clear_simulation_run_list_and_backing_model( def _determine_num_non_ancillary_qubits( annotatable_quantum_computation: AnnotatableQuantumComputation, potential_error_dialog_parent: QtWidgets.QWidget ) -> int: - num_ancillary_qubits: int = 0 + num_non_ancillary_qubits: int = 0 for qubit in range(annotatable_quantum_computation.num_data_qubits): fetched_qubit_label: str | None = annotatable_quantum_computation.get_qubit_label( qubit, QubitLabelType.internal @@ -1212,12 +1211,12 @@ def _determine_num_non_ancillary_qubits( is_cancellable=False, ) return 0 - num_ancillary_qubits += int( + num_non_ancillary_qubits += int( not QuantumCircuitSimulationDialog._does_qubit_label_start_with_internal_qubit_label_prefix( fetched_qubit_label ) ) - return num_ancillary_qubits + return num_non_ancillary_qubits # This method is only a temporary workaround for the quantum registers created for ancillary qubits not being marked as ancillary in the annotatable quantum computation (Date of comment 04.02.2026) @staticmethod diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 3c048b38..2b82b58c 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -8,6 +8,7 @@ from __future__ import annotations +from abc import abstractmethod from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar from PyQt6 import QtCore, QtGui, QtWidgets @@ -164,6 +165,11 @@ def get_center_screen_position_for_size(dialog_size: QtCore.QSize) -> QtCore.QPo - ((dialog_size.height() // 2) if dialog_size.height() > 0 else 0), ) + @abstractmethod + def _handle_non_recoverable_error(self, err: Exception | str | None) -> None: + """Handle non-recoverable errors. Must be implemented by subclasses.""" + return + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _allow_worker_to_continue(self) -> None: """Signal to the worker that it can continue with its long running operation.""" diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 5f215dc1..dc90e26b 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -418,8 +418,11 @@ def _perform_single_sim_run_execution(self, idx_of_sim_run_to_execute: QtCore.QM result.sim_runtime_in_ms, ) - self._update_total_model_runtime_and_label(result.sim_runtime_in_ms) - self._accumulate_and_update_total_runtime(result.sim_runtime_in_ms) + sim_runtime_in_seconds: Final[float] = ( + result.sim_runtime_in_ms / 1000 if result.sim_runtime_in_ms != 0 else 0 + ) + self._update_total_model_runtime_and_label(sim_runtime_in_seconds) + self._accumulate_and_update_total_runtime(sim_runtime_in_seconds) except Exception as err: self._handle_non_recoverable_error( f"Error during reset of previous simulation run execution result of simulation run {idx_of_sim_run_to_execute.row()}, reason: {SimulationRunDialog._stringify_error(err)}" diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 5e78479f..2ee9f6ef 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -190,7 +190,7 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f ) if self._progress_bar is not None: - self._progress_bar.setValue(self.num_generated_input_states) + self._progress_bar.setValue(self._num_imported_simulation_runs) self._progress_info_text_lbl.setText("") QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 5d8362a7..61f78343 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -361,4 +361,4 @@ def update_model_using_simulation_run_result( self.dataChanged.emit(index, index) def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: - return index.isValid() and index.row() >= 0 and index.row() < len(self._simulation_run_models) # type: ignore[no-any-return] + return index.isValid() and index.row() < len(self._simulation_run_models) # type: ignore[no-any-return] diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py index a70d32dd..3a37aebf 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -85,6 +85,7 @@ def _get_estimated_quantum_register_contents_column_width( option: QtWidgets.QStyleOptionViewItem, largest_quantum_register_size_in_qubits: int, font_size: int, + *, does_content_include_prefix: bool = False, prefix_and_content_x_spacing: int = QREG_CONTENT_X_SPACING, ) -> int: @@ -136,6 +137,7 @@ def _draw_elided_text( text: str, text_rect: QtCore.QRect, font_size: int, + *, draw_bold_text: bool = False, text_alignment: QtCore.Qt.AlignmentFlag = QtCore.Qt.AlignmentFlag.AlignTop | QtCore.Qt.AlignmentFlag.AlignCenter, diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py index f5de0afa..50c4a49a 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -318,6 +318,7 @@ def _draw_card_border_and_header( simulation_run_number: int, card_content_rect: QtCore.QRect, available_header_width: int, + *, draw_rect_corners: bool = False, ) -> QtCore.QPoint: painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) @@ -353,6 +354,7 @@ def _draw_and_determine_column_headers( index: QtCore.QModelIndex, header_text_bottom_left_point: QtCore.QPoint, available_content_width: int, + *, draw_rect_corners: bool = False, ) -> list[QtCore.QRect]: qreg_name_and_layout_info_column_width: int = ( diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index c16ea38f..278a9bfe 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -44,7 +44,9 @@ def start_generation(self) -> None: try: self._assert_valid_user_provided_parameter_values() - # We are assuming that the caller has validated that the 2^x operation will not overflow the maximum value of a 32 bit integer. + # We are assuming that this worker will be used by the AllInputStatesGeneratorDialog which contains a progress bar which displays how many input state have been generated by this worker. + # However, the maximum value that the current value of the QProgressBar in the dialog must not exceed 2^31 but we do not validate this precondition here since it does not effect the worker but instead + # place the responsibility on the user of the worker to validate his required preconditions before calling this function. n_states_to_generate: Final[int] = 2**self._expected_input_state_size batch_start_timestamp = AllInputStatesGeneratorWorker.get_timestamp() while ( diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index 1ce5e696..b8b7ec83 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -16,7 +16,7 @@ else: from typing_extensions import override -from ...logger_utils import configure_default_console_logger, log_error_to_console, log_info_to_console +from ...logger_utils import log_error_to_console, log_info_to_console try: # The fastest of the supported parser backends according to the documentation (https://pypi.org/project/ijson/#toc-entry-15) @@ -24,7 +24,6 @@ # This should be the case for the majority of all platforms. import ijson.backends.yajl2_c as ijson except ImportError: - configure_default_console_logger() log_error_to_console("yajl2 C-extension not available, falling back to pure-Python parser!") # pure-Python fallback is always present but might not be the fastest import ijson diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index ba2abf2d..88b70284 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -80,7 +80,7 @@ def start_simulations(self) -> None: dequeued_sim_run_model: SimulationRunModel | None = None try: - dequeued_sim_run_model = self.recv_queue.get(block=False, timeout=0.2) + dequeued_sim_run_model = self.recv_queue.get(block=False) except queue.Empty: self.requestingData.emit() break From 3b49ad772ba007ef6ada1485908283defa39218a Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Wed, 4 Feb 2026 23:59:45 +0100 Subject: [PATCH 79/88] Enabling simulation run execution controls in load from file tab now correctly considers currently selected simulation run execution mode --- .../quantum_circuit_simulation_dialog.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 1bcaccfc..8363859a 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -951,16 +951,33 @@ def handle_import_from_file_dialog_close(self, result: int) -> None: optional_curr_active_tab_widget: QtWidgets.QWidget | None = self._simulation_runs_tab_widget.widget( self._simulation_runs_tab_widget.currentIndex() ) + + optional_sim_run_exec_mode_dropdown: QtWidgets.QComboBox | None = ( + optional_curr_active_tab_widget.findChild(QtWidgets.QComboBox, SIM_RUN_EXECUTION_MODE_DROPDOWN_NAME) + if optional_curr_active_tab_widget is not None + else None + ) + if not assert_all_required_widgets_found_or_close_dialog( error_notification_parent_widget=self, - required_widgets=[optional_curr_active_tab_widget], - error_dialog_content="Failed to locate active tab widget in import simulation runs from file dialog close handler", + required_widgets=[optional_curr_active_tab_widget, optional_sim_run_exec_mode_dropdown], + error_dialog_content="Failed to locate required QtWidgets in import simulation runs from file dialog close handler", ): return curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) + sim_run_exec_mode_dropdown: Final[QtWidgets.QComboBox] = cast( + "QtWidgets.QComboBox", optional_sim_run_exec_mode_dropdown + ) + curr_sim_run_exec_mode: Final[SimulationRunExecutionMode | None] = sim_run_exec_mode_dropdown.currentData() + should_simulation_run_execution_controls_be_enabled: Final[bool] = ( + result == QtWidgets.QDialog.DialogCode.Accepted + and curr_sim_run_exec_mode is not None + and curr_sim_run_exec_mode != SimulationRunExecutionMode.RUN_SINGLE + ) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted + curr_active_tab_widget, should_simulation_run_execution_controls_be_enabled ) optional_add_sim_run_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( From 58bca5bf7e6aa12337ef640efb44ce1c19569775 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 00:17:25 +0100 Subject: [PATCH 80/88] Simulation run json export worker now puts element in queue before emitting batchCompleted signal --- .../workers/simulation_run_json_export_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index 84face33..d8a4dfba 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -134,12 +134,12 @@ def start_export(self) -> None: ) ) batch_start_timestamp = batch_timestamps.end - self.batchCompleted.emit(batch_timestamps.duration) self.send_queue.put_nowait( ExportedBatchData( exported_sim_runs=n_exported_sim_runs_in_batch, skipped_sim_runs=n_skipped_sim_runs_in_batch ) ) + self.batchCompleted.emit(batch_timestamps.duration) n_skipped_sim_runs_in_batch = 0 n_exported_sim_runs_in_batch = 0 From 4916ee589ff1f1980696c54ef2ad7eb3280fd02e Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 16:42:18 +0100 Subject: [PATCH 81/88] Further CodeRabbit suggestions --- .../quantum_circuit_simulation_dialog.py | 3 ++ .../dialogs/base_progress_dialog.py | 1 + .../dialogs/simulation_run_dialog.py | 10 ++-- .../dialogs/simulation_run_editor_dialog.py | 6 ++- .../simulation_run_json_import_dialog.py | 1 - .../simulation_view/simulation_run_model.py | 9 +++- .../all_input_states_generator_worker.py | 5 +- .../simulation_run_json_export_worker.py | 17 ++---- .../simulation_run_json_import_worker.py | 52 +++++++++++-------- .../workers/simulation_run_worker.py | 7 +-- 10 files changed, 62 insertions(+), 49 deletions(-) diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 8363859a..44245a1e 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -170,6 +170,7 @@ def initialize_simulation_runs_tab_widget( self, shared_simulation_runs_model: QtSimulationRunModel, tab_widget_object_name: str, + *, create_load_from_file_controls: bool = False, ) -> QtWidgets.QWidget: tab_wrapper_widget = QtWidgets.QFrame(objectName=tab_widget_object_name) @@ -396,6 +397,8 @@ def handle_simulation_run_add_btn_click(self) -> None: SimulationRunModel( input_state=NBitValuesContainer(self._expected_input_output_state_size), expected_output_state=None, + actual_output_state=None, + create_new_n_bit_values_container_instances=False, ) ) diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index 2b82b58c..c9d3aaf7 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -261,6 +261,7 @@ def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) def _update_displayed_error_text( self, error: Exception | str, + *, log_error: bool = True, num_additionally_skipped_stack_frames_starting_from_this_function: int = 0, ) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index dc90e26b..67c74620 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -202,13 +202,13 @@ def start_simulations( self._worker = SimulationRunWorker( self._annotatable_quantum_computation, self._expected_input_state_size, - self._stop_at_first_output_state_mismatch, worker_recv_queue_config=QueueConfig( queue_instance=self._sim_run_model_queue, queue_batch_size=sim_run_model_queue_batch_size ), worker_send_queue_config=QueueConfig( queue_instance=self._sim_run_result_queue, queue_batch_size=sim_run_result_queue_batch_size ), + stop_at_first_output_state_mismatch=self._stop_at_first_output_state_mismatch, ) self._worker_thread = QtCore.QThread() @@ -306,8 +306,8 @@ def _handle_simulation_run_execution_batch_done(self, batch_generation_duration_ self._shared_simulation_runs_model.update_model_using_simulation_run_result( self._shared_simulation_runs_model.index(to_be_updated_sim_run_number), simulation_run_result.actual_output_state, - simulation_run_result.do_expected_and_actual_outputs_match, - simulation_run_result.sim_runtime_in_ms, + do_expected_and_actual_output_states_match=simulation_run_result.do_expected_and_actual_outputs_match, + execution_runtime_in_ms=simulation_run_result.sim_runtime_in_ms, ) n_received_sim_run_execution_results += 1 except queue.Empty: @@ -414,8 +414,8 @@ def _perform_single_sim_run_execution(self, idx_of_sim_run_to_execute: QtCore.QM self._shared_simulation_runs_model.update_model_using_simulation_run_result( idx_of_sim_run_to_execute, result.actual_output_state, - result.do_expected_and_actual_outputs_match, - result.sim_runtime_in_ms, + do_expected_and_actual_output_states_match=result.do_expected_and_actual_outputs_match, + execution_runtime_in_ms=result.sim_runtime_in_ms, ) sim_runtime_in_seconds: Final[float] = ( diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index ec3e1bb2..fb1f101e 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -484,7 +484,9 @@ def _handle_input_state_qubit_value_checkbox_state_change( updated_qubit_value, return_as_high_low_state=True ) - if not self.edited_simulation_run_model.update_input_state_qubit_value(associated_qubit, updated_qubit_value): + if not self.edited_simulation_run_model.update_input_state_qubit_value( + associated_qubit, new_qubit_value=updated_qubit_value + ): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, message_box_parent=self, @@ -555,7 +557,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( return if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( - associated_qubit, updated_qubit_value + associated_qubit, new_qubit_value=updated_qubit_value ): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 2ee9f6ef..7a15b2e4 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -191,7 +191,6 @@ def _handle_imported_sim_run_batch(self, batch_generation_duration_in_seconds: f if self._progress_bar is not None: self._progress_bar.setValue(self._num_imported_simulation_runs) - self._progress_info_text_lbl.setText("") QtCore.QTimer.singleShot(DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, self._allow_worker_to_continue) @QtCore.pyqtSlot(bool) # type: ignore[untyped-decorator] diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 61f78343..81fc0936 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -56,6 +56,7 @@ def __init__( input_state: NBitValuesContainer, expected_output_state: NBitValuesContainer | None = None, actual_output_state: NBitValuesContainer | None = None, + *, create_new_n_bit_values_container_instances: bool = False, ) -> None: SimulationRunModel._assert_n_bit_value_container_sizes_match( @@ -100,6 +101,7 @@ def reset_result_of_execution(self, reset_actual_output_state: bool = True) -> N def set_result_of_simulation_execution( self, actual_output_state: NBitValuesContainer, + *, do_expected_and_actual_output_states_match: bool | None, execution_runtime_in_ms: float, ) -> None: @@ -123,7 +125,7 @@ def set_result_of_simulation_execution( def update_input_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: return SimulationRunModel._update_n_bit_values_container_qubit_value(self.input_state, qubit, new_qubit_value) - def update_expected_output_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: + def update_expected_output_state_qubit_value(self, qubit: int, *, new_qubit_value: bool) -> bool: if self.expected_output_state is None: return False @@ -347,6 +349,7 @@ def update_model_using_simulation_run_result( self, index: QtCore.QModelIndex, actual_output_state: NBitValuesContainer, + *, do_expected_and_actual_output_states_match: bool | None, execution_runtime_in_ms: float, ) -> None: @@ -356,7 +359,9 @@ def update_model_using_simulation_run_result( raise ValueError(msg) self._simulation_run_models[index.row()].set_result_of_simulation_execution( - actual_output_state, do_expected_and_actual_output_states_match, execution_runtime_in_ms + actual_output_state, + do_expected_and_actual_output_states_match=do_expected_and_actual_output_states_match, + execution_runtime_in_ms=execution_runtime_in_ms, ) self.dataChanged.emit(index, index) diff --git a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py index 278a9bfe..77d9333f 100644 --- a/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -101,5 +101,8 @@ def _generate_sim_run_model_for_input_state( expected_input_state_size: int, integer_defining_input_state: int ) -> SimulationRunModel: return SimulationRunModel( - NBitValuesContainer(expected_input_state_size, integer_defining_input_state), expected_output_state=None + NBitValuesContainer(expected_input_state_size, integer_defining_input_state), + expected_output_state=None, + actual_output_state=None, + create_new_n_bit_values_container_instances=False, ) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py index d8a4dfba..cabc74e9 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -58,9 +58,8 @@ def start_export(self) -> None: n_remaining_sim_runs_in_batch_to_process: int = self._recv_queue_batch_size n_skipped_sim_runs_in_batch: int = 0 n_exported_sim_runs_in_batch: int = 0 - has_exported_first_batch: bool = False has_reached_end_sentinel: bool = False - + has_exported_any_sim_run: bool = False try: self._assert_valid_user_provided_parameter_values() @@ -84,7 +83,7 @@ def start_export(self) -> None: dequeued_sim_run_model: SimulationRunModel | None = None try: - dequeued_sim_run_model = self.recv_queue.get(block=False, timeout=0.2) + dequeued_sim_run_model = self.recv_queue.get(block=False) except queue.Empty: self.requestingData.emit() break @@ -112,14 +111,13 @@ def start_export(self) -> None: n_skipped_sim_runs_in_batch += 1 continue - if SimulationRunJsonExportWorker._should_sim_run_export_delimiter_be_serialized( - has_exported_first_batch, n_exported_sim_runs_in_batch - ): + if has_exported_any_sim_run: file.write(",") file.write( json.dumps(dequeued_sim_run_model, default=SimulationRunJsonExportWorker.serialize_to_json) ) + has_exported_any_sim_run = True n_exported_sim_runs_in_batch += 1 if self.is_cancellation_requested(): @@ -143,7 +141,6 @@ def start_export(self) -> None: n_skipped_sim_runs_in_batch = 0 n_exported_sim_runs_in_batch = 0 - has_exported_first_batch = True n_remaining_sim_runs_in_batch_to_process = self._recv_queue_batch_size # An error during during the serialization of the simulation runs to their .json representation will cause the content of the @@ -158,12 +155,6 @@ def start_export(self) -> None: log_error_to_console(error_msg) self.failed.emit(error) - @staticmethod - def _should_sim_run_export_delimiter_be_serialized( - has_exported_first_batch: bool, num_exported_elements_in_current_batch: int - ) -> bool: - return num_exported_elements_in_current_batch > 0 or has_exported_first_batch - @staticmethod def _validate_parameters(batch_size: int) -> None: if batch_size < 1: diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py index b8b7ec83..4dcab5e1 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -135,36 +135,44 @@ def _try_deserialize_simulation_run( msg = f"Values of input state (expected json key '{INPUT_STATE_JSON_KEY}') was not defined in json object!" raise ValueError(msg) - stringified_input_state: Final[str] = parsed_json_elem_values_dict[INPUT_STATE_JSON_KEY] - if len(stringified_input_state) != expected_state_size: - msg = f"Parsed input state size (n={len(stringified_input_state)}) did not match expected input state size (n={expected_state_size})!" + raw_input_state_json_value: Final[Any] = parsed_json_elem_values_dict[INPUT_STATE_JSON_KEY] + if not isinstance(raw_input_state_json_value, str): + msg = f"Expected input state (expected json key '{INPUT_STATE_JSON_KEY}') to be defined as a string but was actually {type(raw_input_state_json_value)}" + raise TypeError(msg) + + if len(raw_input_state_json_value) != expected_state_size: + msg = f"Parsed input state size (n={len(raw_input_state_json_value)}) did not match expected input state size (n={expected_state_size})!" raise ValueError(msg) - if any(qubit_value not in {"0", "1"} for qubit_value in stringified_input_state): - msg = f"Qubit values of input state must be defined as an enumeration of '0' and '1' literals combined without any delimiter (i.e. a 4 qubit state must be defined as '0101') but was actually {stringified_input_state}" + if any(qubit_value not in {"0", "1"} for qubit_value in raw_input_state_json_value): + msg = f"Qubit values of input state must be defined as an enumeration of '0' and '1' literals combined without any delimiter (i.e. a 4 qubit state must be defined as '0101') but was actually {raw_input_state_json_value}" raise ValueError(msg) - stringified_expected_output_state: Final[str | None] = parsed_json_elem_values_dict.get( - EXPECTED_OUTPUT_STATE_JSON_KEY - ) + expected_output_state: NBitValuesContainer | None = None + raw_expected_output_state: Final[Any | None] = parsed_json_elem_values_dict.get(EXPECTED_OUTPUT_STATE_JSON_KEY) + if raw_expected_output_state is not None: + if not isinstance(raw_expected_output_state, str): + msg = f"Expected output state (expected json key '{EXPECTED_OUTPUT_STATE_JSON_KEY}') to be defined as a string but was actually {type(raw_expected_output_state)}" + raise TypeError(msg) - if stringified_expected_output_state is not None: - if len(stringified_expected_output_state) != expected_state_size: - msg = f"Parsed expected output state size (n={len(stringified_expected_output_state)}) did not match expected input state size (n={expected_state_size})!" + if len(raw_expected_output_state) != expected_state_size: + msg = f"Parsed expected output state size (n={len(raw_expected_output_state)}) did not match expected input state size (n={expected_state_size})!" raise ValueError(msg) - if any(qubit_value not in {"0", "1"} for qubit_value in stringified_expected_output_state): - msg = f"Qubit values of expected output state must be defined as an enumeration of '0' and '1' literals combined without any delimiter (i.e. a 4 qubit state must be defined as '0101') but was actually {stringified_expected_output_state}" + if any(qubit_value not in {"0", "1"} for qubit_value in raw_expected_output_state): + msg = f"Qubit values of expected output state must be defined as an enumeration of '0' and '1' literals combined without any delimiter (i.e. a 4 qubit state must be defined as '0101') but was actually {raw_expected_output_state}" raise ValueError(msg) + expected_output_state = NBitValuesContainer(expected_state_size) + for i in range(expected_state_size): + expected_output_state.set(i, raw_expected_output_state[i] != "0") + input_state: NBitValuesContainer = NBitValuesContainer(expected_state_size) - expected_output_state: NBitValuesContainer | None = ( - NBitValuesContainer(expected_state_size) if stringified_expected_output_state is not None else None - ) for i in range(expected_state_size): - input_state.set(i, stringified_input_state[i] != "0") - - if expected_output_state is not None: - for i in range(expected_state_size): - expected_output_state.set(i, stringified_expected_output_state[i] != "0") # type: ignore[index] + input_state.set(i, raw_input_state_json_value[i] != "0") - return SimulationRunModel(input_state, expected_output_state) + return SimulationRunModel( + input_state, + expected_output_state, + actual_output_state=None, + create_new_n_bit_values_container_instances=False, + ) diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 88b70284..0bed6969 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -38,16 +38,17 @@ class SimulationRunWorker(CancellableProducerConsumerWorker[SimulationRunModel, def __init__( self, annotatable_quantum_computation: AnnotatableQuantumComputation, - _expected_input_state_size: int, - stop_at_first_output_state_mismatch: bool, + expected_input_state_size: int, worker_recv_queue_config: QueueConfig[SimulationRunModel | None], worker_send_queue_config: QueueConfig[SimulationRunResult], + *, + stop_at_first_output_state_mismatch: bool, ) -> None: super().__init__( worker_send_queue_config=worker_send_queue_config, worker_recv_queue_config=worker_recv_queue_config, ) - self._expected_input_state_size: Final[int] = _expected_input_state_size + self._expected_input_state_size: Final[int] = expected_input_state_size self._annotatable_quantum_computation: Final[AnnotatableQuantumComputation] = annotatable_quantum_computation self._should_stop_at_first_output_state_mismatch: Final[bool] = stop_at_first_output_state_mismatch From 348ad2ada3a7cc186a614c0c0aa8c159969c9182 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 22:03:22 +0100 Subject: [PATCH 82/88] CodeRabbit suggestions to replace positional boolean arguments with keyword arguments --- python/mqt/syrec/message_box_utils.py | 21 ++++----- .../quantum_circuit_simulation_dialog.py | 44 ++++++++++++------- .../all_input_states_generator_dialog.py | 2 +- .../dialogs/base_progress_dialog.py | 16 ++++--- .../dialogs/simulation_run_dialog.py | 5 ++- .../dialogs/simulation_run_editor_dialog.py | 10 +++-- .../simulation_run_json_export_dialog.py | 6 +-- .../simulation_run_json_import_dialog.py | 6 +-- .../simulation_view/simulation_run_model.py | 4 +- ...tion_run_execution_styled_item_delegate.py | 1 + 10 files changed, 67 insertions(+), 48 deletions(-) diff --git a/python/mqt/syrec/message_box_utils.py b/python/mqt/syrec/message_box_utils.py index 9cb8d876..558ebc28 100644 --- a/python/mqt/syrec/message_box_utils.py +++ b/python/mqt/syrec/message_box_utils.py @@ -33,6 +33,7 @@ def show_and_request_ok_in_optionally_cancellable_notification( message_box_parent: QtWidgets.QWidget, message_box_title: str, message_box_content: str, + *, is_cancellable: bool, log_contents: bool = True, ) -> bool: @@ -49,8 +50,8 @@ def show_and_request_ok_in_optionally_cancellable_notification( message_box_parent, message_box_title, message_box_content, - buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), - defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable=is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable=is_cancellable), ) return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case MessageBoxType.INFO: @@ -64,8 +65,8 @@ def show_and_request_ok_in_optionally_cancellable_notification( message_box_parent, message_box_title, message_box_content, - buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), - defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable=is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable=is_cancellable), ) return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case MessageBoxType.WARNING: @@ -79,8 +80,8 @@ def show_and_request_ok_in_optionally_cancellable_notification( message_box_parent, message_box_title, message_box_content, - buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), - defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable=is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable=is_cancellable), ) return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case MessageBoxType.ERROR: @@ -94,8 +95,8 @@ def show_and_request_ok_in_optionally_cancellable_notification( message_box_parent, message_box_title, message_box_content, - buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable), - defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable), + buttons=_get_buttons_for_message_box_type(message_box_type, is_cancellable=is_cancellable), + defaultButton=_get_default_button_for_message_box_type(message_box_type, is_cancellable=is_cancellable), ) return _check_whether_message_ok_was_clicked(message_box_type, clicked_message_box_button) case _: @@ -104,7 +105,7 @@ def show_and_request_ok_in_optionally_cancellable_notification( def _get_buttons_for_message_box_type( - message_box_type: MessageBoxType, is_cancellable: bool + message_box_type: MessageBoxType, *, is_cancellable: bool ) -> QtWidgets.QMessageBox.StandardButton: if message_box_type == MessageBoxType.QUESTION: return ( @@ -121,7 +122,7 @@ def _get_buttons_for_message_box_type( # Get the default button that will be pressed if the user presses ENTER in the open message box def _get_default_button_for_message_box_type( - message_box_type: MessageBoxType, is_cancellable: bool + message_box_type: MessageBoxType, *, is_cancellable: bool ) -> QtWidgets.QMessageBox.StandardButton: if message_box_type == MessageBoxType.QUESTION: return QtWidgets.QMessageBox.StandardButton.No if is_cancellable else QtWidgets.QMessageBox.StandardButton.Yes diff --git a/python/mqt/syrec/quantum_circuit_simulation_dialog.py b/python/mqt/syrec/quantum_circuit_simulation_dialog.py index 44245a1e..dae6fda5 100644 --- a/python/mqt/syrec/quantum_circuit_simulation_dialog.py +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -122,11 +122,15 @@ def __init__( self._simulation_runs_tab_widget = QtWidgets.QTabWidget(self) self._simulation_runs_tab_widget.currentChanged.connect(self.handle_simulation_runs_tab_widget_tab_changed) self._simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self._simulation_runs_model, SOME_SIM_RUNS_TAB_WIDGET_NAME), + self.initialize_simulation_runs_tab_widget( + self._simulation_runs_model, SOME_SIM_RUNS_TAB_WIDGET_NAME, create_load_from_file_controls=False + ), "Check some input-output mapping combinations", ) self._simulation_runs_tab_widget.addTab( - self.initialize_simulation_runs_tab_widget(self._simulation_runs_model, ALL_SIM_RUNS_TAB_WIDGET_NAME), + self.initialize_simulation_runs_tab_widget( + self._simulation_runs_model, ALL_SIM_RUNS_TAB_WIDGET_NAME, create_load_from_file_controls=False + ), "Check all input-output mapping combinations", ) self._simulation_runs_tab_widget.addTab( @@ -388,7 +392,7 @@ def handle_simulation_run_selection_change( edit_simulation_run_btn.setEnabled(is_list_item_selected) delete_simulation_run_btn.setEnabled(is_list_item_selected) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( - curr_active_tab_widget, is_list_item_selected + curr_active_tab_widget, is_simulation_run_selected=is_list_item_selected ) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -411,7 +415,9 @@ def handle_simulation_run_add_btn_click(self) -> None: return curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) - self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(curr_active_tab_widget, True) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, should_controls_be_enabled=True + ) optional_simulation_runs_list_view: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( QtWidgets.QListView, SIMULATION_RUNS_LIST_VIEW_NAME @@ -572,7 +578,7 @@ def handle_input_states_generator_dialog_close(self, result: int) -> None: curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, result == QtWidgets.QDialog.DialogCode.Accepted + curr_active_tab_widget, should_controls_be_enabled=(result == QtWidgets.QDialog.DialogCode.Accepted) ) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -603,7 +609,9 @@ def handle_simulation_run_delete_btn_click(self) -> None: # and the subsequent update of the backing model of the QListView selection will switch to the element the index # of the previously selected element thus the simulation run execution controls should not be enabled after an element # is deleted - self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(curr_active_tab_widget, False) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + curr_active_tab_widget, should_controls_be_enabled=False + ) def initialize_load_simulation_runs_from_file_controls(self) -> QtWidgets.QLayout: controls_layout = QtWidgets.QHBoxLayout() @@ -725,7 +733,9 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i self._simulation_runs_tab_widget.setCurrentIndex(self._prev_active_simulation_runs_tab_idx) return self._clear_simulation_run_list_and_backing_model(prev_active_tab_widget) - self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(to_be_switched_to_tab_widget, False) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + to_be_switched_to_tab_widget, should_controls_be_enabled=False + ) if to_be_switched_to_tab_widget.objectName() == ALL_SIM_RUNS_TAB_WIDGET_NAME: n_input_state_combinations: int = 2**self._expected_input_output_state_size @@ -751,7 +761,9 @@ def handle_simulation_runs_tab_widget_tab_changed(self, switched_to_tab_index: i # and our override of the selection change slot of the ComboBox not changing the selected index then invalid dropdown indices # can only stem from this setter call (assuming that no other sets are added to this class in the future). sim_run_exec_mode_dropdown.setCurrentIndex(self._shared_selected_sim_run_execution_mode_dropdown_index) - self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget(prev_active_tab_widget, False) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( + prev_active_tab_widget, should_controls_be_enabled=False + ) self._prev_active_simulation_runs_tab_idx = switched_to_tab_index @QtCore.pyqtSlot() # type: ignore[untyped-decorator] @@ -843,7 +855,9 @@ def _open_simulation_runs_execution_dialog(self) -> None: self._simulation_run_dialog.start_simulation(selected_sim_run_model_idx) case SimulationRunExecutionMode.RUN_ALL | SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE: self._simulation_run_dialog.start_simulations( - curr_sim_run_exec_mode == SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE + stop_at_first_output_state_mismatch=( + curr_sim_run_exec_mode == SimulationRunExecutionMode.RUN_ALL_STOP_AT_FIRST_FAILURE + ) ) case _: assert_never(curr_sim_run_exec_mode) @@ -922,9 +936,7 @@ def open_import_from_file_dialog(self) -> None: return selected_filename_lbl: Final[QtWidgets.QLabel] = cast("QtWidgets.QLabel", optional_selected_filename_lbl) - curr_active_tab_widget: Final[QtWidgets.QTabWidget] = cast( - "QtWidgets.QTabWidget", optional_curr_active_tab_widget - ) + curr_active_tab_widget: Final[QtWidgets.QWidget] = cast("QtWidgets.QWidget", optional_curr_active_tab_widget) if self._simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0: if not show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.WARNING, @@ -980,7 +992,7 @@ def handle_import_from_file_dialog_close(self, result: int) -> None: ) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - curr_active_tab_widget, should_simulation_run_execution_controls_be_enabled + curr_active_tab_widget, should_controls_be_enabled=should_simulation_run_execution_controls_be_enabled ) optional_add_sim_run_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( @@ -997,7 +1009,7 @@ def handle_import_from_file_dialog_close(self, result: int) -> None: add_sim_run_btn.setEnabled(result == QtWidgets.QDialog.DialogCode.Accepted) def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( - self, tab_widget: QtWidgets.QWidget, should_controls_be_enabled: bool + self, tab_widget: QtWidgets.QWidget, *, should_controls_be_enabled: bool ) -> None: optional_run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, SIM_RUN_EXECUTION_TRIGGER_BTN_NAME @@ -1038,7 +1050,7 @@ def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget( ) def set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( - self, tab_widget: QtWidgets.QWidget, is_simulation_run_selected: bool + self, tab_widget: QtWidgets.QWidget, *, is_simulation_run_selected: bool ) -> None: optional_run_simulation_runs_btn: QtWidgets.QPushButton | None = tab_widget.findChild( QtWidgets.QPushButton, SIM_RUN_EXECUTION_TRIGGER_BTN_NAME @@ -1170,7 +1182,7 @@ def handle_simulation_run_execution_mode_selection_change(self, selected_sim_run "QtWidgets.QListView", optional_simulation_runs_list_view ) self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( - curr_active_tab_widget, len(simulation_runs_list_view.selectedIndexes()) == 1 + curr_active_tab_widget, is_simulation_run_selected=(len(simulation_runs_list_view.selectedIndexes()) == 1) ) def _clear_simulation_run_list_and_backing_model( diff --git a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py index c6920a3a..a5800efa 100644 --- a/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -109,7 +109,7 @@ def start_generation( self._worker_thread.finished.connect(self._worker_thread.deleteLater) self._worker_thread.finished.connect(self._reset_workers) self._worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancel_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=True) @override def closeEvent(self, event: QtGui.QCloseEvent) -> None: diff --git a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py index c9d3aaf7..f6d5b9da 100644 --- a/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -61,6 +61,7 @@ def __init__( shared_simulation_runs_model: QtSimulationRunModel, dialog_title: str, optional_progress_bar_text_format: str | None = None, + *, create_default_layout: bool = True, user_provided_dialog_size: QtCore.QSize | None = None, center_dialog: bool = True, @@ -125,8 +126,8 @@ def __init__( QtWidgets.QDialogButtonBox.StandardButton.Ok | QtWidgets.QDialogButtonBox.StandardButton.Cancel ) self._dialog_button_box.setCenterButtons(True) - self._change_dialog_ok_button_enable_state(False) - self._change_dialog_cancel_button_enable_state(False) + self._change_dialog_ok_button_enable_state(should_button_be_enabled=False) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) if create_default_layout: layout.addWidget(self._title_lbl) @@ -242,20 +243,20 @@ def _request_worker_cancellation(self) -> None: self._worker.request_cancellation() self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) - def _change_dialog_cancel_button_enable_state(self, should_button_be_enabled: bool) -> None: + def _change_dialog_cancel_button_enable_state(self, *, should_button_be_enabled: bool) -> None: BaseProgressDialog._change_dialog_button_enable_state( self._dialog_button_box, QtWidgets.QDialogButtonBox.StandardButton.Cancel, - should_button_be_enabled, btn_not_found_notification_parent=self, + should_button_be_enabled=should_button_be_enabled, ) - def _change_dialog_ok_button_enable_state(self, should_button_be_enabled: bool) -> None: + def _change_dialog_ok_button_enable_state(self, *, should_button_be_enabled: bool) -> None: BaseProgressDialog._change_dialog_button_enable_state( self._dialog_button_box, QtWidgets.QDialogButtonBox.StandardButton.Ok, - should_button_be_enabled, btn_not_found_notification_parent=self, + should_button_be_enabled=should_button_be_enabled, ) def _update_displayed_error_text( @@ -282,8 +283,9 @@ def _reset_workers(self) -> None: def _change_dialog_button_enable_state( dialog_button_box: QtWidgets.QDialogButtonBox, to_be_modified_button: QtWidgets.QDialogButtonBox.StandardButton, - should_button_be_enabled: bool, btn_not_found_notification_parent: QtWidgets.QWidget, + *, + should_button_be_enabled: bool, ) -> None: dialog_button: QtWidgets.QPushButton | None = dialog_button_box.button(to_be_modified_button) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py index 67c74620..6323bf94 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -139,10 +139,11 @@ def start_simulation(self, idx_of_sim_run_to_execute: QtCore.QModelIndex) -> Non log_info_to_console(f"Starting execution of simulation run (index: {idx_of_sim_run_to_execute.row()})") self._title_lbl.setText(f"Executing simulation run {idx_of_sim_run_to_execute.row()}!") self._perform_single_sim_run_execution(idx_of_sim_run_to_execute) - self._change_dialog_ok_button_enable_state(True) + self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) def start_simulations( self, + *, stop_at_first_output_state_mismatch: bool, sim_run_model_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, sim_run_result_queue_batch_size: int = DEFAULT_SMALL_QUEUE_SIZE, @@ -229,7 +230,7 @@ def start_simulations( self._worker_thread.finished.connect(self._reset_workers) self._worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancel_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=True) self._enqueue_next_simulation_runs() # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index fb1f101e..6df40871 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -457,6 +457,7 @@ def _handle_input_state_qubit_value_checkbox_state_change( associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int, + *, update_associated_state_input_field: bool = False, ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( @@ -519,6 +520,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( associated_qreg_name: str, associated_qubit: int, relative_qubit_index_in_quantum_register: int, + *, update_associated_state_input_field: bool, ) -> None: optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( @@ -557,7 +559,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( return if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( - associated_qubit, new_qubit_value=updated_qubit_value + associated_qubit, updated_qubit_value ): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -674,14 +676,14 @@ def _create_in_or_out_state_edit_field( in_or_out_state_edit_field.editingFinished.connect( lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_control_created_for_input_state: ( self._handle_input_or_output_state_text_change( - associated_qreg_name, expected_text_length, is_editing_input_state + associated_qreg_name, expected_text_length, is_editing_input_state=is_editing_input_state ) ) ) in_or_out_state_edit_field.focusOut.connect( lambda associated_qreg_name=qreg_layout.qreg_name, expected_text_length=qreg_layout.qreg_size, is_editing_input_state=is_control_created_for_input_state: ( self._handle_input_or_output_state_text_change( - associated_qreg_name, expected_text_length, is_editing_input_state + associated_qreg_name, expected_text_length, is_editing_input_state=is_editing_input_state ) ) ) @@ -1234,7 +1236,7 @@ def _handle_init_expected_output_state_button_click(self) -> None: @QtCore.pyqtSlot(str, int, bool) # type: ignore[untyped-decorator] def _handle_input_or_output_state_text_change( - self, associated_qreg_name: str, expected_qreg_size: int, is_editing_input_state: bool + self, associated_qreg_name: str, expected_qreg_size: int, *, is_editing_input_state: bool ) -> None: optional_input_state_text_field: QtWidgets.QLineEdit | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py index 3eb03260..f750b9c1 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -141,7 +141,7 @@ def start_export( self._worker_thread.finished.connect(self._worker_thread.deleteLater) self._worker_thread.finished.connect(self._reset_workers) self._worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancel_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=True) self._enqueue_next_simulation_runs_to_export() # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. @@ -239,8 +239,8 @@ def _handle_export_completion(self, was_cancellation_requested: bool) -> None: self._request_worker_cancellation() self._shutdown_worker_thread_and_await_completion() - self._change_dialog_cancel_button_enable_state(False) - self._change_dialog_ok_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) + self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_export_to_file_cancel_button_click(self) -> bool: diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py index 7a15b2e4..50d20c63 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -141,7 +141,7 @@ def start_import( self._worker_thread.finished.connect(self._reset_workers) # Only this call will actually start a new thread self._worker_thread.start(QtCore.QThread.Priority.LowPriority) - self._change_dialog_cancel_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=True) # Pressing the ESC key will only close the dialog but not close it thus no closeEvent will be triggered. @override @@ -205,8 +205,8 @@ def _handle_import_completion(self, was_cancellation_requested: bool) -> None: self._request_worker_cancellation() self._shutdown_worker_thread_and_await_completion() - self._change_dialog_cancel_button_enable_state(False) - self._change_dialog_ok_button_enable_state(True) + self._change_dialog_cancel_button_enable_state(should_button_be_enabled=False) + self._change_dialog_ok_button_enable_state(should_button_be_enabled=True) @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_import_from_file_cancel_button_click(self) -> bool: diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 81fc0936..3fb8528d 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -91,7 +91,7 @@ def initialize_expected_output_state_as_copy_of_input_state(self) -> None: for i in range(self.expected_output_state.size()): self.expected_output_state.set(i, self.input_state.test(i)) # type: ignore[arg-type] - def reset_result_of_execution(self, reset_actual_output_state: bool = True) -> None: + def reset_result_of_execution(self, *, reset_actual_output_state: bool = True) -> None: if reset_actual_output_state: self.actual_output_state = None @@ -125,7 +125,7 @@ def set_result_of_simulation_execution( def update_input_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: return SimulationRunModel._update_n_bit_values_container_qubit_value(self.input_state, qubit, new_qubit_value) - def update_expected_output_state_qubit_value(self, qubit: int, *, new_qubit_value: bool) -> bool: + def update_expected_output_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: if self.expected_output_state is None: return False diff --git a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py index 9b118a5d..da5daf21 100644 --- a/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -514,6 +514,7 @@ def _draw_card_border_and_header( simulation_run_number: int, card_content_rect: QtCore.QRect, available_header_width: int, + *, draw_rect_corners: bool = False, ) -> QtCore.QPoint: painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) From b8bc8ced5818e4d6e65d5384bc8491327e7f89a1 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 23:08:07 +0100 Subject: [PATCH 83/88] Transformed some boolean positional arguments to keyword only ones and disabled FBT001 ruff rule due to its conflict with the PyQt6 signal/slot mechanism --- .../dialogs/simulation_run_editor_dialog.py | 4 ++-- .../syrec/simulation_view/simulation_run_model.py | 12 +++++++----- .../simulation_view/workers/simulation_run_worker.py | 11 ++++++++--- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 6df40871..fa32d5ca 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -559,7 +559,7 @@ def _handle_expected_output_state_qubit_value_checkbox_state_change( return if not self.edited_simulation_run_model.update_expected_output_state_qubit_value( - associated_qubit, updated_qubit_value + associated_qubit, new_qubit_value=updated_qubit_value ): show_and_request_ok_in_optionally_cancellable_notification( message_box_type=MessageBoxType.ERROR, @@ -1526,7 +1526,7 @@ def _get_internal_qubit_labels_for_qreg( return internal_qubit_labels @staticmethod - def _stringify_qubit_value(qubit_value: bool | None, return_as_high_low_state: bool) -> str: + def _stringify_qubit_value(qubit_value: bool | None, *, return_as_high_low_state: bool) -> str: if qubit_value is None: return "UNKNOWN" if return_as_high_low_state else "-" diff --git a/python/mqt/syrec/simulation_view/simulation_run_model.py b/python/mqt/syrec/simulation_view/simulation_run_model.py index 3fb8528d..72032c64 100644 --- a/python/mqt/syrec/simulation_view/simulation_run_model.py +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -122,15 +122,17 @@ def set_result_of_simulation_execution( self.do_expected_and_actual_outputs_match = do_expected_and_actual_output_states_match self.execution_runtime_in_ms = execution_runtime_in_ms - def update_input_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: - return SimulationRunModel._update_n_bit_values_container_qubit_value(self.input_state, qubit, new_qubit_value) + def update_input_state_qubit_value(self, qubit: int, *, new_qubit_value: bool) -> bool: + return SimulationRunModel._update_n_bit_values_container_qubit_value( + self.input_state, qubit, new_qubit_value=new_qubit_value + ) - def update_expected_output_state_qubit_value(self, qubit: int, new_qubit_value: bool) -> bool: + def update_expected_output_state_qubit_value(self, qubit: int, *, new_qubit_value: bool) -> bool: if self.expected_output_state is None: return False return SimulationRunModel._update_n_bit_values_container_qubit_value( - self.expected_output_state, qubit, new_qubit_value + self.expected_output_state, qubit, new_qubit_value=new_qubit_value ) def update_user_editable_data( @@ -183,7 +185,7 @@ def do_output_states_match( @staticmethod def _update_n_bit_values_container_qubit_value( - n_bit_values_container: NBitValuesContainer, qubit: int, new_qubit_value: bool + n_bit_values_container: NBitValuesContainer, qubit: int, *, new_qubit_value: bool ) -> bool: if qubit < 0 or qubit >= n_bit_values_container.size(): return False diff --git a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py index 0bed6969..04cd3227 100644 --- a/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -68,13 +68,18 @@ def start_simulations(self) -> None: batch_start_timestamp = SimulationRunWorker.get_timestamp() n_remaining_batch_elems_to_generate: int = self._send_queue_batch_size - while self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel): + while self._should_continue_processing( + output_state_mismatch_flag=found_outputs_mismatch, reached_end_sentinel_flag=has_reached_end_sentinel + ): self._wait_on_cancellation_or_input_data() one_time_request_new_data_flag: bool = False for _ in range(self._send_queue_batch_size): if ( - not self._should_continue_processing(found_outputs_mismatch, has_reached_end_sentinel) + not self._should_continue_processing( + output_state_mismatch_flag=found_outputs_mismatch, + reached_end_sentinel_flag=has_reached_end_sentinel, + ) or n_remaining_batch_elems_to_generate < 0 ): break @@ -145,7 +150,7 @@ def start_simulations(self) -> None: log_error_to_console(error_msg) self.failed.emit(error) - def _should_continue_processing(self, output_state_mismatch_flag: bool, reached_end_sentinel_flag: bool) -> bool: + def _should_continue_processing(self, *, output_state_mismatch_flag: bool, reached_end_sentinel_flag: bool) -> bool: return ( not self.is_cancellation_requested() and (not self._should_stop_at_first_output_state_mismatch or not output_state_mismatch_flag) From 63ae6c7f8183c83399c1123861d02d17c166c848 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 23:08:32 +0100 Subject: [PATCH 84/88] Transformed some boolean positional arguments to keyword only ones and disabled FBT001 ruff rule due to its conflict with the PyQt6 signal/slot mechanism --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 516169f0..87a6953c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -214,6 +214,7 @@ extend-select = [ "YTT", # flake8-2020 ] ignore = [ + "FBT001", # Boolean positional arguments - conflicts with PyQt6 signal/slot mechanism "PLR09", # Too many <...> "PLR2004", # Magic value used in comparison "S101", # Use of assert detected From f03adbe7a8d26dffd876a28ce068716d9a79b467 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 23:30:01 +0100 Subject: [PATCH 85/88] Fixed some invalid types in typing.cast calls as well as wrong type hints --- .../dialogs/simulation_run_editor_dialog.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index fa32d5ca..42cdf869 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -931,7 +931,7 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ if associated_qreg_layout is None: return - optional_qreg_qubits_groupbox: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( + optional_qreg_qubits_groupbox: QtWidgets.QWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -944,7 +944,7 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ return qreg_qubits_groupbox = cast("QtWidgets.QGroupBox", optional_qreg_qubits_groupbox) - optional_qubit_search_input_field: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + optional_qubit_search_input_field: QtWidgets.QWidget | None = qreg_qubits_groupbox.findChild( QtWidgets.QLineEdit, QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_quantum_register_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -961,7 +961,7 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ associated_qreg_layout.first_qubit_of_qreg, associated_qreg_layout.first_qubit_of_qreg + associated_qreg_layout.qreg_size, ): - optional_qubit_value_label: QtWidgets.QtWidget | None = qreg_qubits_groupbox.findChild( + optional_qubit_value_label: QtWidgets.QWidget | None = qreg_qubits_groupbox.findChild( QtWidgets.QLabel, QUBIT_LABEL_NAME_FORMAT.format(qubit=qubit), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, @@ -1031,7 +1031,7 @@ def _handle_qubit_search_trigger_button_click(self, associated_quantum_register_ @QtCore.pyqtSlot(str) # type: ignore[untyped-decorator] def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_name: str) -> None: is_any_qubit_values_groupbox_collapsed: bool = False - optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + optional_expected_output_state_value_toggle_button: QtWidgets.QWidget | None = ( self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, @@ -1039,11 +1039,11 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam ) ) - optional_qreg_search_input_field: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( + optional_qreg_search_input_field: QtWidgets.QWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME ) - optional_qreg_search_trigger_btn: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( + optional_qreg_search_trigger_btn: QtWidgets.QWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_SEARCH_TRIGGER_BUTTON_NAME ) @@ -1059,29 +1059,29 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name - optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( + optional_qreg_input_state_input_field: QtWidgets.QWidget | None = ( self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) ) - optional_qreg_expected_output_state_input_field: QtWidgets.QtWidget | None = ( + optional_qreg_expected_output_state_input_field: QtWidgets.QWidget | None = ( self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) - optional_qubit_values_groupbox: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( + optional_qubit_values_groupbox: QtWidgets.QWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QGroupBox, QREG_QUBIT_VALUES_GROUPBOX_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - optional_qubit_values_toggle_button: QtWidgets.QtWidget | None = self._simulation_run_wrapper_box.findChild( + optional_qubit_values_toggle_button: QtWidgets.QWidget | None = self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name), QtCore.Qt.FindChildOption.FindDirectChildrenOnly, ) - optional_qubit_values_groupbox_qubit_search_field: QtWidgets.QtWidget | None = ( + optional_qubit_values_groupbox_qubit_search_field: QtWidgets.QWidget | None = ( optional_qubit_values_groupbox.findChild( QtWidgets.QLineEdit, QREG_QUBIT_SEARCH_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), @@ -1107,7 +1107,7 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam qreg_expected_output_state_input_field = cast( "QtWidgets.QLineEdit", optional_qreg_expected_output_state_input_field ) - qubit_values_groupbox = cast("QtWidgets.QCheckBox", optional_qubit_values_groupbox) + qubit_values_groupbox = cast("QtWidgets.QGroupBox", optional_qubit_values_groupbox) qubit_values_toggle_button = cast("QtWidgets.QPushButton", optional_qubit_values_toggle_button) qubit_values_groupbox_qubit_search_field = cast( @@ -1131,7 +1131,7 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam "QtWidgets.QPushButton", optional_expected_output_state_value_toggle_button ) qreg_search_input_field = cast("QtWidgets.QLineEdit", optional_qreg_search_input_field) - qreg_search_trigger_btn = cast("QtWidgets.QPushButtonLineEdit", optional_qreg_search_trigger_btn) + qreg_search_trigger_btn = cast("QtWidgets.QPushButton", optional_qreg_search_trigger_btn) expected_output_state_value_toggle_button.setEnabled(not is_any_qubit_values_groupbox_collapsed) qreg_search_input_field.setEnabled(not is_any_qubit_values_groupbox_collapsed) @@ -1139,7 +1139,7 @@ def _handle_qreg_qubit_values_edit_toggle_button_click(self, associated_qreg_nam @QtCore.pyqtSlot() # type: ignore[untyped-decorator] def _handle_init_expected_output_state_button_click(self) -> None: - optional_expected_output_state_value_toggle_button: QtWidgets.QtQWidget | None = ( + optional_expected_output_state_value_toggle_button: QtWidgets.QWidget | None = ( self._simulation_run_wrapper_box.findChild( QtWidgets.QPushButton, QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, @@ -1167,13 +1167,13 @@ def _handle_init_expected_output_state_button_click(self) -> None: for qreg_layout in self._qreg_layouts: qreg_name: str = qreg_layout.qreg_name - optional_qreg_input_state_input_field: QtWidgets.QtWidget | None = ( + optional_qreg_input_state_input_field: QtWidgets.QWidget | None = ( self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) ) - optional_qreg_output_state_input_field: QtWidgets.QtWidget | None = ( + optional_qreg_output_state_input_field: QtWidgets.QWidget | None = ( self._simulation_run_wrapper_box.findChild( QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) ) @@ -1485,10 +1485,10 @@ def _handle_input_or_output_state_text_change( return not_edited_input_state_qubit_checkbox = cast( - "QtWidgets.QGroupBox", optional_not_edited_input_state_qubit_checkbox + "QtWidgets.QCheckBox", optional_not_edited_input_state_qubit_checkbox ) not_edited_output_state_qubit_checkbox = cast( - "QtWidgets.QGroupBox", optional_not_edited_output_state_qubit_checkbox + "QtWidgets.QCheckBox", optional_not_edited_output_state_qubit_checkbox ) not_edited_input_state_qubit_checkbox.setEnabled(are_stringified_qreg_contents_valid) From 61181f4d19c5173f2aac73fed4b6b8b92f8c1203 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Thu, 5 Feb 2026 23:46:44 +0100 Subject: [PATCH 86/88] Removed some dead code and simplified match check for quantum register and qubit name search --- .../simulation_view/dialogs/simulation_run_editor_dialog.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 42cdf869..6e76300f 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -438,7 +438,8 @@ def _handle_quantum_register_name_search(self) -> None: qreg_edit_qubit_values_toggle_button = cast( "QtWidgets.QPushButton", optional_qreg_edit_qubit_values_toggle_button ) - should_control_be_visible: bool = qreg_name_search_input_field.text() is None or qreg_name.startswith( + + should_control_be_visible: bool = not qreg_name_search_input_field.text() or qreg_name.startswith( qreg_name_search_input_field.text() ) qreg_name_label.setVisible(should_control_be_visible) @@ -1356,8 +1357,6 @@ def _handle_input_or_output_state_text_change( ): return - cast("QtWidgets.QGroupBox", optional_effected_qreg_qubit_values_groupbox) - first_qubit_of_edited_qreg: Final[int] = edited_qreg_layout.first_qubit_of_qreg n_qubits_of_edited_qreg: Final[int] = edited_qreg_layout.qreg_size for qubit_of_edited_qreg in range( From c5c204b03e3dd29773f210485a68e67328864742 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Fri, 6 Feb 2026 00:28:07 +0100 Subject: [PATCH 87/88] Updated parent widget constructor argument type signaturet in LineEditWithDynamicWidth widget --- .../simulation_view/dialogs/simulation_run_editor_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 6e76300f..96ef1b10 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -62,7 +62,7 @@ class QubitValueLabelAndCheckbox: class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] focusOut = QtCore.pyqtSignal() # noqa: N815 - def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget = None): + def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget | None = None): super().__init__(parent) self._expected_max_num_characters = expected_max_num_characters self.setMaxLength(expected_max_num_characters) From f2b0ab3b5894999703ebd5c5bbca61d8ef5c4fc5 Mon Sep 17 00:00:00 2001 From: Fabian Hingerl <fabian.hingerl@gmail.com> Date: Fri, 6 Feb 2026 00:29:20 +0100 Subject: [PATCH 88/88] Added missing return type to LineEditWithDynamicWidth constructor --- .../simulation_view/dialogs/simulation_run_editor_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py index 96ef1b10..d4b14fc3 100644 --- a/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -62,7 +62,7 @@ class QubitValueLabelAndCheckbox: class LineEditWithDynamicWidth(QtWidgets.QLineEdit): # type: ignore[misc] focusOut = QtCore.pyqtSignal() # noqa: N815 - def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget | None = None): + def __init__(self, expected_max_num_characters: int, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent) self._expected_max_num_characters = expected_max_num_characters self.setMaxLength(expected_max_num_characters)