diff --git a/pyproject.toml b/pyproject.toml index d1c670a9..87a6953c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,8 @@ requires-python = ">=3.10" dependencies = [ "mqt.core~=3.4.1", "PyQt6>=6.8", + "ijson>=3.4.0", + "typing-extensions>=4.1.0; python_version < '3.11'" ] dynamic = ["version"] @@ -153,7 +155,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 @@ -212,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 diff --git a/python/mqt/syrec/logger_utils.py b/python/mqt/syrec/logger_utils.py new file mode 100644 index 00000000..30858c5b --- /dev/null +++ b/python/mqt/syrec/logger_utils.py @@ -0,0 +1,65 @@ +# 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) + 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( + 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 advanced 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 advanced 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 advanced 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 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/message_box_utils.py b/python/mqt/syrec/message_box_utils.py new file mode 100644 index 00000000..558ebc28 --- /dev/null +++ b/python/mqt/syrec/message_box_utils.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 sys +from enum import Enum + +from PyQt6 import QtWidgets + +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 + + +class MessageBoxType(Enum): + QUESTION = 0 + INFO = 1 + WARNING = 2 + ERROR = 3 + + +def show_and_request_ok_in_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=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: + 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=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: + 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=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: + 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=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 _: + # 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_ok_was_clicked( + message_box_type: MessageBoxType, + 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 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 new file mode 100644 index 00000000..dae6fda5 --- /dev/null +++ b/python/mqt/syrec/quantum_circuit_simulation_dialog.py @@ -0,0 +1,1256 @@ +# 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 re +import sys +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Final, cast + +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: + from typing_extensions import assert_never + +from PyQt6 import QtCore, QtGui, QtWidgets + +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 +from .simulation_view.dialogs.base_progress_dialog import BaseProgressDialog +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.simulation_run_overview_styled_item_delegate import ( + SimulationRunOverviewStyledItemDelegate, +) +from .widget_check_utils import assert_all_required_widgets_found_or_close_dialog + +if TYPE_CHECKING: + from mqt.syrec import AnnotatableQuantumComputation + +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" +SAVE_SIM_RUNS_TO_FILE_BTN_NAME: Final[str] = "save_sims_to_file_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] = "" + +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" + + +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. + __syrec_program_comment_regex: re.Pattern[str] = re.compile("|".join(map(re.escape, [r"//", r"/*"]))) + + def __init__( + self, + associated_stringified_syrec_program: str, + annotatable_quantum_computation: AnnotatableQuantumComputation, + parent: QtWidgets.QWidget, + ) -> None: + super().__init__(parent) + 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: Final[str] = ( + associated_stringified_syrec_program if not self._did_syrec_program_contain_comments else "" + ) + + self._annotatable_quantum_computation: Final[AnnotatableQuantumComputation] = 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) + + 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 + 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] = ( + 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 + + 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, 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, create_load_from_file_controls=False + ), + "Check all input-output mapping combinations", + ) + self._simulation_runs_tab_widget.addTab( + self.initialize_simulation_runs_tab_widget( + 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.setLayout(self.layout) + self.setSizeGripEnabled(True) + + def show_save_changes_reminder(self) -> None: + 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", + 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 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, + tab_widget_object_name: str, + *, + create_load_from_file_controls: bool = False, + ) -> QtWidgets.QWidget: + 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) + + 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(self) + ) + tab_wrapper_widget_layout.addSpacing(manual_y_space_size) + + # BEGIN: Create simulation runs list view Qt elements + 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) + 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) + # 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) + + 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) + 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 + simulation_runs_list_modification_buttons_layout = QtWidgets.QHBoxLayout() + simulation_runs_list_modification_buttons_layout.addStretch() + + 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(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) + + 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(self.handle_simulation_run_edit_btn_click) + simulation_runs_list_modification_buttons_layout.addWidget(edit_simulation_run_button) + + delete_simulation_run_button = QtWidgets.QPushButton( + 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) + simulation_runs_list_modification_buttons_layout.addWidget(delete_simulation_run_button) + + 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 + + # BEGIN: Create simulation runs execution Qt elements + 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("") + 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", + 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) + 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)" + ) + 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) + tab_wrapper_widget_layout.addLayout(simulation_runs_execution_buttons_layout) + # 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. + @override + def reject(self) -> None: + if self.show_close_confirmation_dialog_and_return_boolean_user_choice(): + super().reject() + + @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() + + @QtCore.pyqtSlot(QtCore.QItemSelection, QtCore.QItemSelection) # type: ignore[untyped-decorator] + def handle_simulation_run_selection_change( + 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() + 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], + 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) + + optional_add_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME + ) + optional_edit_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, EDIT_SIM_RUN_BTN_NAME + ) + optional_delete_simulation_run_btn: QtWidgets.QPushButton | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, DELETE_SIM_RUN_BTN_NAME + ) + + 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) + self.set_enabled_state_of_simulation_run_execution_controls_in_tab_widget_based_on_sim_run_selection_status( + curr_active_tab_widget, is_simulation_run_selected=is_list_item_selected + ) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def handle_simulation_run_add_btn_click(self) -> None: + self._simulation_runs_model.add_simulation_run_model( + 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, + ) + ) + + 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 + + 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, 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 + ) + 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() + + @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 = ( + 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: Final[QtWidgets.QWidget] = cast( + "QtWidgets.QListView", optional_simulation_runs_list_view + ) + + 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() + ): + 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 + + # 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, + reference_sim_run_model.actual_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() + + @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: + 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, + ) + 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}", + is_cancellable=False, + log_contents=False, + ) + 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: + 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.getSaveFileName( + 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( + 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.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 + + @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( + 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( + 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) + + @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 + + 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 + + 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, should_controls_be_enabled=(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 = ( + 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 + + 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 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 + # 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, should_controls_be_enabled=False + ) + + 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_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", + 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) + 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() + 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 + + 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_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, + 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) + return + + 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 + ) + + 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._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, 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 + 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) + 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._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 + ) + # 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, should_controls_be_enabled=False + ) + 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: + 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 + + 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 + + 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.QComboBox", 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() + 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 + 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, + 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() + + 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( + 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) + + @QtCore.pyqtSlot(int) # type: ignore[untyped-decorator] + 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)" + ) + + 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_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 + ) + 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 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.QPushButton] = cast("QtWidgets.QPushButton", 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 + + 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) + 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_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) + 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, + 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._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 + ) + 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._expected_input_output_state_size, + ) + + @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 + + 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, 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, should_controls_be_enabled=should_simulation_run_execution_controls_be_enabled + ) + + optional_add_sim_run_btn: QtWidgets.QWidget | None = curr_active_tab_widget.findChild( + QtWidgets.QPushButton, ADD_SIM_RUN_BTN_NAME + ) + 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.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( + 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 + ) + + 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 + ) + 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) + sim_run_exec_mode_dropdown.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 + ) -> 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 + ) + 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: + 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 enabled 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) + + 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 + and self._simulation_runs_model.rowCount(QtCore.QModelIndex()) > 0 + ) + + 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() + ) + + 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(associated_tab_widget.objectName() != LOAD_SIM_RUNS_FROM_FILE_TAB_WIDGET_NAME) + 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, is_simulation_run_selected=(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, + ) + + # 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_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 + ) + 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_non_ancillary_qubits += int( + not QuantumCircuitSimulationDialog._does_qubit_label_start_with_internal_qubit_label_prefix( + fetched_qubit_label + ) + ) + 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 + 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/all_input_states_generator_dialog.py b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py new file mode 100644 index 00000000..a5800efa --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/all_input_states_generator_dialog.py @@ -0,0 +1,218 @@ +# 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 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: + from PyQt6 import QtGui, QtWidgets + + 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.all_input_states_generator_worker import AllInputStatesGeneratorWorker +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]): + 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._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, + expected_input_state_size: int, + worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, + ) -> None: + 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 positive integers!", + 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 + 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) + else: + show_and_request_ok_in_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, + ) + + 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( + 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( + self._handle_generated_input_state_batch, QtCore.Qt.ConnectionType.QueuedConnection + ) + self._worker.finished.connect( + self._handle_input_state_generator_finished, QtCore.Qt.ConnectionType.QueuedConnection + ) + 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._change_dialog_cancel_button_enable_state(should_button_be_enabled=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(): + self.accept() + else: + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() + else: + 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() + + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_input_state_generator_failure(self, err: Exception) -> None: + self._handle_non_recoverable_error(err) + + @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 + + 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()) + 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) + return + + 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("") + + 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) + log_info_to_console(info_msg) + 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() + + 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: + if self._worker is None: + return True + + 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!", + 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!") + self._handle_non_recoverable_error(None) + return True + return False + + 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 + # 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) + + 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, + ) + + 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 new file mode 100644 index 00000000..f6d5b9da --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/base_progress_dialog.py @@ -0,0 +1,318 @@ +# 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 abc import abstractmethod +from typing import TYPE_CHECKING, 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_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}" +) +DEFAULT_BATCH_RUNTIME_INFO_TEXT_FORMAT: Final[str] = ( + "Batch of {n_batch_elements:d} completed! Runtime [in ms]: {batch_duration_in_ms:f}" +) + +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[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. + 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__( + 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, + user_provided_dialog_size: QtCore.QSize | None = None, + center_dialog: bool = True, + ) -> 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._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) + + 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() + 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") + # 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._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(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) + 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) + self.setLayout(layout) + + @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(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.QPoint(0, 0) + + return QtCore.QPoint( + (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), + ) + + @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.""" + 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( + n_batch_elements=n_batch_elements, batch_duration_in_ms=batch_duration_in_seconds * 1000 + ) + ) + + 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: + """ + 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() + 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: + """ + 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!") + 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, + 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: + BaseProgressDialog._change_dialog_button_enable_state( + self._dialog_button_box, + QtWidgets.QDialogButtonBox.StandardButton.Ok, + btn_not_found_notification_parent=self, + should_button_be_enabled=should_button_be_enabled, + ) + + 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) + + def _reset_workers(self) -> 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( + dialog_button_box: QtWidgets.QDialogButtonBox, + to_be_modified_button: QtWidgets.QDialogButtonBox.StandardButton, + 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) + + if dialog_button is not None: + dialog_button.setEnabled(should_button_be_enabled) + else: + 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", + message_box_content=f"Could not find {to_be_modified_button.name} button of dialog, this should not happen", + is_cancellable=False, + ) + + @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 new file mode 100644 index 00000000..6323bf94 --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_dialog.py @@ -0,0 +1,430 @@ +# 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 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 + +if TYPE_CHECKING: + from PyQt6 import QtGui + + from mqt.syrec import AnnotatableQuantumComputation + + from ..simulation_run_model import QtSimulationRunModel, SimulationRunModel + 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 +from ..simulation_run_model import SIMULATION_RUN_IO_STATE_QT_ROLE +from ..styled_item_delegates.simulation_run_execution_styled_item_delegate import ( + SimulationRunExecutionStyledItemDelegate, +) +from ..workers.cancellable_worker_variants import QueueConfig +from ..workers.simulation_run_worker import SimulationRunWorker +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}" +) + + +# 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, + parent: QtWidgets.QWidget, + shared_simulation_runs_model: QtSimulationRunModel, + annotatable_quantum_computation: AnnotatableQuantumComputation, + expected_input_output_state_size: int, + ) -> 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: 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 + 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_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) + + 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() + 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) + # 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) + + 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.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) + + 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) + 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(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, + ) -> None: + 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={self._expected_input_state_size}) to be positive integers!", + is_cancellable=False, + ) + 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._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] = 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})!" + ) + 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) + else: + show_and_request_ok_in_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, + ) + + 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._annotatable_quantum_computation, + self._expected_input_state_size, + 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() + 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._handle_simulation_run_execution_batch_done, QtCore.Qt.ConnectionType.QueuedConnection + ) + 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_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(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. + @override + def reject(self) -> None: + if self._handle_simulation_runs_cancel_button_click(): + super().reject() + + @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(): + self.accept() + else: + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() + else: + event.ignore() + + @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!") + + 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() + + 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(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: + if self._worker is None: + return True + + 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!", + 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(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 + + n_received_sim_run_execution_results: int = 0 + to_be_updated_sim_run_number: int = -1 + + 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() + 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), + simulation_run_result.actual_output_state, + 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: + # 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( + 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 + + batch_results_processing_duration_in_seconds: Final[float] = ( + SimulationRunWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_results_processing_start_timestamp + ).duration + ) + + 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: + 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 + 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)}" + ) + + 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: + 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: + 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) + + 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, + 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] = ( + 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_editor_dialog.py b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py new file mode 100644 index 00000000..d4b14fc3 --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_editor_dialog.py @@ -0,0 +1,1550 @@ +# 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 +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Final, cast + +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: + from typing_extensions import assert_never + +from mqt.syrec import QubitLabelType + +from ...logger_utils import log_error_to_console +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, +) +from .base_progress_dialog import BaseProgressDialog + +if TYPE_CHECKING: + from collections.abc import Iterable + + from mqt.syrec import AnnotatableQuantumComputation, NBitValuesContainer + + from ..simulation_run_model import ( + QuantumRegisterLayout, + SimulationRunModel, + ) + + +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] + focusOut = QtCore.pyqtSignal() # noqa: N815 + + 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) + 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. + 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 = max(nominal, self.width()) + return QtCore.QSize(preferred, sh.height()) + + def focusOutEvent(self, ev: QtGui.QFocusEvent) -> None: # noqa: N802 + super().focusOutEvent(ev) + self.focusOut.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" +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" + +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" + +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})" +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!" +) + +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__( + self, + simulation_run_model_index: QtCore.QModelIndex, + copy_of_reference_edit_sim_run_model: SimulationRunModel, + 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._qreg_layouts: list[QuantumRegisterLayout] = simulation_run_model_index.data( + QUANTUM_REGISTER_LAYOUT_QT_ROLE + ) + self._annotatable_quantum_computation: AnnotatableQuantumComputation = simulation_run_model_index.data( + ANNOTATABLE_QUANTUM_COMPUTATION_QT_ROLE + ) + + initial_input_state: NBitValuesContainer = self.edited_simulation_run_model.input_state + initial_expected_output_state: NBitValuesContainer | None = ( + self.edited_simulation_run_model.expected_output_state + ) + initial_actual_output_state: NBitValuesContainer | None = 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") + + 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()) + ) + + 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 + ) + + 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=QREG_EXPECTED_OUTPUT_STATE_VALUE_INIT_TOGGLE_NAME, + ) + 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") + + 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 + ) + + qreg_controls_grid_row: int = 2 + for qreg_layout in self._qreg_layouts: + qreg_name: str = qreg_layout.qreg_name + + quantum_register_label = QtWidgets.QLabel( + "Quantum register: " + qreg_name, objectName=QREG_LABEL_NAME_FORMAT.format(qreg_name=qreg_name) + ) + quantum_register_controls_grid_layout.addWidget( + quantum_register_label, + qreg_controls_grid_row, + 0, + alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + ) + + 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_widgets.contents_widget, + qreg_controls_grid_row, + 1, + alignment=QtCore.Qt.AlignmentFlag.AlignCenter, + ) + + 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, + ) + 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, + 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, 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( + associated_qreg_name + ) + ) + + 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 + ) + + 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, initial_actual_output_state + ), + qreg_controls_grid_row, + 0, + 1, + 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. + 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, qreg_controls_grid_row, 5 + ) + 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), + qreg_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, 1) + + 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) + 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 + 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.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( + 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() + + @override + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + if self._failed_due_to_internal_error: + super().reject() + return + + # Ask for confirmation before closing dialog + if show_and_request_ok_in_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() + 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 + optional_qreg_name_search_input_field: QtWidgets.QLineEdit | None = ( + 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( + 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( + 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( + 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( + 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, 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 = ( + self._simulation_run_wrapper_box.findChild( + QtWidgets.QPushButton, QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=qreg_name) + ) + ) + + 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_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", + ): + 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_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 + ) + + 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) + qreg_layout_info_label.setVisible(should_control_be_visible) + qreg_input_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) + + @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.Qt.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( + 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( + QtWidgets.QLineEdit, QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=associated_qreg_name) + ) + + 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 = 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 + ) + + 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, + 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( + STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) + ) + + 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) + + @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.Qt.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( + 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( + 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( + [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 = 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 + ) + + 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( + 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, new_qubit_value=updated_qubit_value + ): + 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", + 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( + STRINGIFIED_QUBIT_VALUE_FORMAT.format(stringified_qubit_value=stringified_updated_qubit_value) + ) + 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() + qreg_search_label = QtWidgets.QLabel("Quantum register:") + qreg_search_input_field = QtWidgets.QLineEdit(objectName=QREG_SEARCH_INPUT_FIELD_NAME) + qreg_search_input_field.setPlaceholderText("<QUANTUM_REGISTER_NAME>") + + 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) + + 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", 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) + 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: NBitValuesContainer | None, + 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_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: + 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 + ) + ) + + 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( + 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_control_created_for_input_state: ( + self._handle_input_or_output_state_text_change( + 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=is_editing_input_state + ) + ) + ) + return QRegContentsLabelAndCheckbox(prefix_label, in_or_out_state_edit_field) + + def _create_in_or_out_state_qubit_value_checkbox( + self, + associated_qreg_name: str, + optional_qreg_qubit_values: NBitValuesContainer | None, + associated_qubit: int, + relative_qubit_index_in_qreg: int, + 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( + 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 + ) + qubit_value_label: QtWidgets.QLabel | None = None + match qubit_location: + case QubitLocation.INPUT_STATE: + qubit_value_checkbox.checkStateChanged.connect( + 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( + 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 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( + new_check_state, + associated_qreg_name, + associated_qubit, + relative_qubit_index_in_quantum_register, + update_associated_state_input_field=True, + ) + ) + ) + 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 + ) -> 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_LABEL>") + + 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 + ) + ) + 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 + + def _create_qubit_controls_groupbox( + self, + associated_qreg_layout: QuantumRegisterLayout, + initial_input_state: NBitValuesContainer, + initial_expected_output_state: NBitValuesContainer | None, + initial_actual_output_state: NBitValuesContainer | 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, + 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, QubitLabelType.internal + ) + qubit_label = QtWidgets.QLabel( + "Qubit: " + fetched_internal_qubit_label if fetched_internal_qubit_label is not None else "<UNKNOWN>", + 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, qubit_controls_grid_layout_row, 0, 2, 1 + ) + + 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_and_lbl.checkbox, qubit_controls_grid_layout_row, 1, 2, 1 + ) + + 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, + ) + ) + + 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, + 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) + 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 + + @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), + None, + ) + if associated_qreg_layout is None: + return + + 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, + ) + + 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.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, + ) + + 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.QWidget | 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 + ) + + matched_with_qubit_label: str | None = self._annotatable_quantum_computation.get_qubit_label( + qubit, QubitLabelType.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) + + @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.QWidget | 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.QWidget | None = self._simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, QREG_SEARCH_INPUT_FIELD_NAME + ) + + optional_qreg_search_trigger_btn: QtWidgets.QWidget | 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.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.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.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.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.QWidget | 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 optional_qubit_values_groupbox is not None + else None + ) + + if not self._assert_all_required_widgets_found_or_close_dialog( + [ + optional_qreg_input_state_input_field, + optional_qreg_expected_output_state_input_field, + optional_qubit_values_groupbox, + optional_qubit_values_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!", + ): + return + + qreg_input_state_input_field = cast("QtWidgets.QLineEdit", optional_qreg_input_state_input_field) + qreg_expected_output_state_input_field = cast( + "QtWidgets.QLineEdit", optional_qreg_expected_output_state_input_field + ) + 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( + "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) + else: + qubit_values_groupbox.setVisible(False) + qubit_values_toggle_button.setText(EDIT_OUTPUT_STATE_QUBIT_VALUES) + 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.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) + qreg_search_trigger_btn.setEnabled(not is_any_qubit_values_groupbox_collapsed) + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def _handle_init_expected_output_state_button_click(self) -> 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, + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + if not self._assert_all_required_widgets_found_or_close_dialog( + [optional_expected_output_state_value_toggle_button], + "Failed to find all required QtWidgets for init/clear operation of expected 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 + 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 + 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.QWidget | None = ( + self._simulation_run_wrapper_box.findChild( + QtWidgets.QLineEdit, EXPECTED_QREG_OUTPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name) + ) + ) + + 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: + # 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, # type: ignore[arg-type] + 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 + ): + optional_associated_qubit_value_checkbox: QtWidgets.QCheckBox | None = ( + self._simulation_run_wrapper_box.findChild( + QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) + ) + + 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 + else None + ) + 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( + qubit_value, return_as_high_low_state=True + ) + ) + ) + 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: + 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( + 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( + QtWidgets.QPushButton, + QREG_QUBIT_VALUES_TOGGLE_BUTTON_NAME_FORMAT.format(qreg_name=associated_qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + 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, + ) + ) + + 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( + QtWidgets.QLabel, QREG_VALUES_VALIDATION_ERROR_LABEL_NAME + ) + + 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!", + ): + return + + 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) + + 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 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_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!", + 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 + + 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( + QtWidgets.QLineEdit, + QREG_INPUT_STATE_INPUT_FIELD_NAME_FORMAT.format(qreg_name=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + optional_not_edited_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=qreg_name), + QtCore.Qt.FindChildOption.FindDirectChildrenOnly, + ) + ) + + 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, + ) + ) + + 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 + + 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 + ) + + 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( + 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(should_state_controls_be_visible) + + 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 = ( + 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 = ( + qreg_qubit_values_groupbox.findChild( + QtWidgets.QCheckBox, EXPECTED_OUTPUT_STATE_QUBIT_CHECKBOX_NAME_FORMAT.format(qubit=qubit) + ) + ) + + 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.QCheckBox", optional_not_edited_input_state_qubit_checkbox + ) + not_edited_output_state_qubit_checkbox = cast( + "QtWidgets.QCheckBox", 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( + n_bit_values_container: NBitValuesContainer, first_qubit: int, n_qubits: int + ) -> str: + 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 "<UNKNOWN>" + + @staticmethod + def _get_internal_qubit_labels_for_qreg( + annotatable_quantum_computation: AnnotatableQuantumComputation, + 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, QubitLabelType.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" + + def _assert_all_required_widgets_found_or_close_dialog( + self, required_widgets: Iterable[QtWidgets.QWidget], error_dialog_content: str + ) -> bool: + 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 + 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 new file mode 100644 index 00000000..f750b9c1 --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_export_dialog.py @@ -0,0 +1,269 @@ +# 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 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: + from pathlib import Path + + from PyQt6 import QtGui + + 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 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" +) + + +class SimulationRunJsonExportDialog(BaseProgressDialog[SimulationRunJsonExportWorker]): + 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, + ) + 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._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) + 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) + self.setLayout(layout) + + def start_export( + self, + export_location: Path, + associated_stringified_syrec_program: str, + 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}") + + 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. + super().reject() + return + + 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, + 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, + ) + + 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, + 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) + + 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._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. + @override + def reject(self) -> None: + if self._handle_export_to_file_cancel_button_click(): + super().reject() + + @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(): + self.accept() + else: + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() + else: + event.ignore() + + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_export_failure(self, err: Exception) -> None: + self._handle_non_recoverable_error(err) + + @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: + 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: + self._handle_non_recoverable_error(err) + return + + 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 + + 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( + 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: + 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: + self._handle_non_recoverable_error( + 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: + 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) + + # 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() + + 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: + if self._worker is None: + return True + + 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!", + 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 | 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) + + 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 new file mode 100644 index 00000000..50d20c63 --- /dev/null +++ b/python/mqt/syrec/simulation_view/dialogs/simulation_run_json_import_dialog.py @@ -0,0 +1,246 @@ +# 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 sys +from typing import TYPE_CHECKING + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +from PyQt6 import QtCore, QtWidgets + +if TYPE_CHECKING: + from pathlib import Path + + from PyQt6 import QtGui + + 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_import_worker import SimulationRunJsonImportWorker +from .base_progress_dialog import DEFAULT_MEDIUM_QUEUE_SIZE, DEFAULT_WORKER_CONTINUE_DELAY_IN_MS, BaseProgressDialog + + +class SimulationRunJsonImportDialog(BaseProgressDialog[SimulationRunJsonImportWorker]): + 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._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._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) + + 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() + + 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) + + layout.addWidget(self._dialog_button_box) + self.setLayout(layout) + + def start_import( + self, + path_to_json_file: Path, + expected_input_state_size: int, + worker_send_queue_batch_size: int = DEFAULT_MEDIUM_QUEUE_SIZE, + ) -> None: + 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 integers!", + 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, + 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. + # 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_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 + def reject(self) -> None: + if self._handle_import_from_file_cancel_button_click(): + super().reject() + + @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(): + self.accept() + else: + # Avoid requiring duplicate confirmation of close operation by calling reject() function of super class instead of overridden reject function. + super().reject() + else: + event.ignore() + + @QtCore.pyqtSlot(Exception) # type: ignore[untyped-decorator] + def _handle_importer_failure(self, err: Exception) -> None: + self._handle_non_recoverable_error(err) + + @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 + + 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()) + 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) + return + + 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}" + ) + + if self._progress_bar is not None: + self._progress_bar.setValue(self._num_imported_simulation_runs) + 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!") + 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() + + 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: + if self._worker is None: + return True + + 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!", + 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 import requested!") + self._handle_non_recoverable_error(None) + return True + return False + + 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) + + 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, + ) + + 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 new file mode 100644 index 00000000..72032c64 --- /dev/null +++ b/python/mqt/syrec/simulation_view/simulation_run_model.py @@ -0,0 +1,371 @@ +# 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 +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Final + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +from PyQt6 import QtCore + +from mqt.syrec import NBitValuesContainer, QubitLabelType + +from ..logger_utils import log_error_to_console + +if TYPE_CHECKING: + from mqt.syrec import AnnotatableQuantumComputation + +# 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 +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 +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) +class QuantumRegisterLayout: + qreg_name: str + first_qubit_of_qreg: int + qreg_size: int + + +class SimulationRunModel: + input_state: NBitValuesContainer + expected_output_state: NBitValuesContainer | None = None + actual_output_state: NBitValuesContainer | None = None + do_expected_and_actual_outputs_match: bool | None = None + execution_runtime_in_ms: float | None = None + + def __init__( + self, + 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( + 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 + self.expected_output_state = expected_output_state + self.actual_output_state = actual_output_state + else: + self.input_state = NBitValuesContainer(input_state.size()) + for qubit in range(input_state.size()): + self.input_state.set(qubit, input_state.test(qubit)) # type: ignore[arg-type] + if expected_output_state is not None: + self.expected_output_state = NBitValuesContainer(expected_output_state.size()) + for qubit in range(expected_output_state.size()): + 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 = NBitValuesContainer(actual_output_state.size()) + for qubit in range(actual_output_state.size()): + 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: + return + + self.expected_output_state = NBitValuesContainer(self.input_state.size()) + 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: + if reset_actual_output_state: + 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: NBitValuesContainer, + *, + do_expected_and_actual_output_states_match: bool | None, + execution_runtime_in_ms: float, + ) -> None: + 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) + raise ValueError(msg) + + if self.actual_output_state is None: + self.actual_output_state = NBitValuesContainer(self.input_state.size()) + + 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 + + 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: + 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=new_qubit_value + ) + + def update_user_editable_data( + self, + edited_input_state: NBitValuesContainer, + edited_expected_output_state: NBitValuesContainer | None, + ) -> None: + 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()): + 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 = NBitValuesContainer(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( + expected_output_state: NBitValuesContainer | None, actual_output_state: NBitValuesContainer + ) -> bool | None: + if expected_output_state is None: + return None + + 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()) + ) + + @staticmethod + def _update_n_bit_values_container_qubit_value( + n_bit_values_container: NBitValuesContainer, qubit: int, *, new_qubit_value: bool + ) -> bool: + if qubit < 0 or qubit >= n_bit_values_container.size(): + return False + + n_bit_values_container.set(qubit, new_qubit_value) + return True + + @staticmethod + def _assert_n_bit_value_container_sizes_match( + expected_container: NBitValuesContainer, + expected_container_name: str, + optional_actual_container: NBitValuesContainer | 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] + def __init__( + self, annotatable_quantum_computation: AnnotatableQuantumComputation, parent: QtCore.QObject = None + ) -> None: + super().__init__(parent) + 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 + + 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 + ) + 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_qreg + + @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: AnnotatableQuantumComputation, + ) -> 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, QubitLabelType.internal + ) + if qreg.size == 0 or QtSimulationRunModel._does_qubit_label_start_with_internal_qubit_label_prefix( + internal_qubit_label if internal_qubit_label is not None else "" + ): + 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 + + @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) -> Any | None: + if not index.isValid(): + return None + + if role == SIMULATION_RUN_IO_STATE_QT_ROLE: + return self._simulation_run_models[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 + + 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: + 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.endInsertRows() + + def delete_simulation_run_model(self, index: QtCore.QModelIndex) -> bool: + if not index.isValid(): + return False + + self.beginRemoveRows(QtCore.QModelIndex(), index.row(), 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.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 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: + 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) + + 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) + + 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: + 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) + + self._simulation_run_models[index.row()].set_result_of_simulation_execution( + 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) + + def is_model_index_valid(self, index: QtCore.QModelIndex) -> bool: + 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 new file mode 100644 index 00000000..3a37aebf --- /dev/null +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/base_simulation_run_styled_item_delegate.py @@ -0,0 +1,180 @@ +# 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, QtGui + +if TYPE_CHECKING: + from PyQt6 import QtWidgets + + from mqt.syrec import NBitValuesContainer + +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_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] = "<UNKNOWN>" +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 +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 +QREG_CONTENTS_HELP_TEXT_FONT_SIZE: Final[int] = 8 + + +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( + 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: NBitValuesContainer, first_qubit: int, n_qubits: int + ) -> str: + 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 DEFAULT_UNKNOWN_QREG_CONTENT_PLACEHOLDER_TEXT + + @staticmethod + 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: + 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: Final[int] = BaseSimulationRunStyledItemDelegate._get_pixel_width_of_text( + "<UNKNOWN>", 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 (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( + 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) + if total_required_width > 0 + else 0 + ) + + @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/simulation_run_execution_styled_item_delegate.py b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py new file mode 100644 index 00000000..da5daf21 --- /dev/null +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_execution_styled_item_delegate.py @@ -0,0 +1,589 @@ +# 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 +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 ( + 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, + QREG_CONTENTS_HELP_TEXT, + QREG_CONTENTS_HELP_TEXT_FONT_SIZE, + BaseSimulationRunStyledItemDelegate, +) + +if TYPE_CHECKING: + from ..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: QtWidgets.QWidget = None) -> None: + super().__init__(parent) + + @staticmethod + def _get_required_width_for_labels_column(option: QtWidgets.QStyleOptionViewItem, 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: Final[int] = SimulationRunExecutionStyledItemDelegate._get_pixel_height_of_text( + 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: Final[int] = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + # Quantum register contents are displayed in the following format for every quantum register: + # R0: <QREG_NAME_LABEL>: <QREG_NAME> <QREG_LAYOUT_INFO> + # R1: <INPUT_LABEL>: <INPUT_STATE_QREG_VALUES> + # R2: <EXPECTED_OUTPUT_LABEL>: <EXPECTED_OUTPUT_STATE_QREG_VALUES> + # R3: <ACTUAL_OUTPUT_LABEL>: <ACTUAL_OUTPUT_STATE_QREG_VALUES> + # + # Additionally, below the content of all quantum registers the aggregate result of the simulation run is displayed as: + # R5: <AGGR_RESULT_LABEL>: <AGGR_RESULT_TEXT> + # R6: <RUNTIME>: <RUNTIME_IN_MS> + 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: 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: 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_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) + + @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) + ) + available_content_rect: Final[QtCore.QRect] = option.rect + return QtCore.QSize( + 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 + + 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) + available_rect_for_content: QtCore.QRect = option.rect.adjusted( + 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 + + 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_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_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, CARD_CONTENT_FONT_SIZE + ) + ) + required_values_column_width: Final[int] = ( + SimulationRunExecutionStyledItemDelegate._get_required_width_for_qreg_contents_and_outputs_match_result( + option, index, CARD_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_line_height, + ) + 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_line_height, + ) + 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 + + 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 + ) + + 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, + 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, + 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, + ) + + 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 + 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, + ) + + @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 + + @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" 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 new file mode 100644 index 00000000..50c4a49a --- /dev/null +++ b/python/mqt/syrec/simulation_view/styled_item_delegates/simulation_run_overview_styled_item_delegate.py @@ -0,0 +1,567 @@ +# 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 +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 ( + 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_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, + 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_CONTENTS_HELP_TEXT, + QREG_CONTENTS_HELP_TEXT_FONT_SIZE, + QREG_LAYOUT_INFO_FONT_SIZE, + BaseSimulationRunStyledItemDelegate, +) + +if TYPE_CHECKING: + from mqt.syrec import NBitValuesContainer + + from ..simulation_run_model import ( + SimulationRunModel, + ) + + +# 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: QtWidgets.QWidget = None) -> None: + super().__init__(parent) + + @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 + + 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: Final[int] = len(index.data(QUANTUM_REGISTER_LAYOUT_QT_ROLE)) + # Quantum register contents are displayed as two rows containing the following information: + # R0: <QREG_NAME> <STRINGIFIED_INPUT_QUBIT_VALUES> <STRINGIFIED_OUTPUT_QUBIT_VALUES> + # R1: <QREG_LAYOUT_INFO> + group_box_title_height: Final[int] = SimulationRunOverviewStyledItemDelegate._get_pixel_height_of_text( + option.font, CARD_TITLE_FONT_SIZE + ) + 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: Final[int] = ( + QREG_CONTENT_Y_SPACING + + 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 + ) + 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 + + CARD_TITLE_BOTTOM_Y_MARGIN + + 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 + ) + + qreg_name_and_layout_info_column_width: Final[int] = ( + SimulationRunOverviewStyledItemDelegate._get_required_qreg_name_and_layout_column_width( + option, index, CARD_CONTENT_FONT_SIZE + ) + ) + + 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: Final[int] = ( + 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, + ) + ) + + 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: Final[int] = max(qreg_content_header_width, max_qreg_qubits_column_width) + 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 + ), + required_help_text_width, + ) + return QtCore.QSize(total_simulation_run_group_box_width, total_simulation_run_group_box_height) + + @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) + ) + available_content_rect: Final[QtCore.QRect] = option.rect + return QtCore.QSize( + 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 + + 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) + + available_rect_for_content: QtCore.QRect = option.rect.adjusted( + 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, + 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( + painter, option, index, header_text_bottom_left_point, available_rect_for_content.width() + ) + + 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 + ) + 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, + header_row_column_one_text_rect.adjusted(0, curr_row_y_offset, 0, curr_row_y_offset), + CARD_CONTENT_FONT_SIZE, + ) + + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + SimulationRunOverviewStyledItemDelegate._stringify_some_qubits_of_n_bit_values_container( + associated_sim_run_model.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), + CARD_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, + ) + + 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, + 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 + ), + 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, + 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 + ), + 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, QREG_CONTENTS_HELP_TEXT_FONT_SIZE + ), + ) + SimulationRunOverviewStyledItemDelegate._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 + 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: + 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_height: int = SimulationRunOverviewStyledItemDelegate._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 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, header_text, header_text_rect, CARD_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, CARD_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), + CARD_CONTENT_FONT_SIZE, + ) + + QREG_CONTENT_X_SPACING + ) + 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( + [ + 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_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, + 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 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_QREG_NAME_COLUMN_HEADER, + header_row_column_one_text_rect, + CARD_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, + 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 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_INPUT_STATE_QREG_CONTENT_HEADER, + header_row_column_two_text_rect, + CARD_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, + 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 + ) + SimulationRunOverviewStyledItemDelegate._draw_elided_text( + painter, + DEFAULT_OUTPUT_STATE_QREG_CONTENT_HEADER, + header_row_column_three_text_rect, + CARD_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] + + @staticmethod + def _draw_qreg_contents( + painter: QtGui.QPainter, + option: QtWidgets.QStyleOptionViewItem, + qreg_contents_prefix: str, + qreg_contents_container: NBitValuesContainer | None, + first_qubit_of_qreg: int, + qreg_size: int, + largest_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_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 + ) + + 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_alignment=QtCore.Qt.AlignmentFlag.AlignRight, + 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, + text_alignment=QtCore.Qt.AlignmentFlag.AlignLeft, + ) 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 new file mode 100644 index 00000000..77d9333f --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/all_input_states_generator_worker.py @@ -0,0 +1,108 @@ +# 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 +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.syrec import NBitValuesContainer + +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel +from .cancellable_worker_variants import CancellableProducerWorker + +if TYPE_CHECKING: + from .cancellable_worker_variants import BatchTimestamps, QueueConfig + + +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 + + @QtCore.pyqtSlot() # type: ignore[untyped-decorator] + def start_generation(self) -> None: + batch_start_timestamp: float = 0 + batch_timestamps: BatchTimestamps | None = None + integer_encoding_first_input_state_of_batch: int = 0 + + try: + self._assert_valid_user_provided_parameter_values() + + # 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 ( + 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 + + self.send_queue.put_nowait( + AllInputStatesGeneratorWorker._generate_sim_run_model_for_input_state( + self._expected_input_state_size, integer_encoding_input_state + ) + ) + + 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( + 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: + 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) + + @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 + 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, + actual_output_state=None, + create_new_n_bit_values_container_instances=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 new file mode 100644 index 00000000..449a26fc --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/cancellable_worker_variants.py @@ -0,0 +1,179 @@ +# 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 = 0 + end: float = 0 + duration: float = 0 + + +@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] + """ + 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__() + # 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 + # 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() + + 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 _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: + """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: + """ + 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 + 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] +): + """ + 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__( + self, + worker_send_queue_config: QueueConfig[SendQueueElemType], + 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 + # 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: + """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: + """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}!" + 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 new file mode 100644 index 00000000..cabc74e9 --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_export_worker.py @@ -0,0 +1,178 @@ +# 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 queue +import re +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Final + +from PyQt6 import QtCore + +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel +from .cancellable_worker_variants import CancellableProducerConsumerWorker + +if TYPE_CHECKING: + from pathlib import Path + + from .cancellable_worker_variants import BatchTimestamps, QueueConfig + + +@dataclass(frozen=True) +class ExportedBatchData: + exported_sim_runs: int + skipped_sim_runs: int + + +class SimulationRunJsonExportWorker(CancellableProducerConsumerWorker[SimulationRunModel, ExportedBatchData]): + def __init__( + self, + path_to_json_file: Path, + associated_stringified_syrec_program: str, + worker_recv_queue_config: QueueConfig[SimulationRunModel | None], + worker_send_queue_config: QueueConfig[ExportedBatchData], + ) -> None: + super().__init__( + worker_send_queue_config=worker_send_queue_config, + worker_recv_queue_config=worker_recv_queue_config, + ) + + 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: + 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_reached_end_sentinel: bool = False + has_exported_any_sim_run: bool = False + try: + self._assert_valid_user_provided_parameter_values() + + 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":[' + ) + + 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) + 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 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(): + 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.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 + 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("]}") + 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=}" + ) + 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): + msg = f"Cannot serialize object of {type(obj)}" + raise TypeError(msg) + + if obj.expected_output_state is None: + 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) 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 new file mode 100644 index 00000000..4dcab5e1 --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_json_import_worker.py @@ -0,0 +1,178 @@ +# 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 +from typing import TYPE_CHECKING, Any, Final + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +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) + # 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: + 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.syrec import NBitValuesContainer + +from ..simulation_run_model import SimulationRunModel +from .cancellable_worker_variants import CancellableProducerWorker + +if TYPE_CHECKING: + from pathlib import Path + + 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(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: 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: + batch_start_timestamp: float = SimulationRunJsonImportWorker.get_timestamp() + batch_timestamps: BatchTimestamps | None = None + + 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: + # 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: + # 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): + 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 + + self.send_queue.put_nowait( + SimulationRunJsonImportWorker._try_deserialize_simulation_run( + self._expected_input_state_size, arr_elem + ) + ) + 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( + batch_start_timestamp + ) + ) + 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 + 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: + batch_timestamps = ( + SimulationRunJsonImportWorker.calc_batch_duration_and_return_end_timestamp_in_seconds( + batch_start_timestamp + ) + ) + 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) + + @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 + 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) + + 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 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) + + 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 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 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) + for i in range(expected_state_size): + input_state.set(i, raw_input_state_json_value[i] != "0") + + 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 new file mode 100644 index 00000000..04cd3227 --- /dev/null +++ b/python/mqt/syrec/simulation_view/workers/simulation_run_worker.py @@ -0,0 +1,178 @@ +# 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.syrec import NBitValuesContainer, simple_simulation + +from ...logger_utils import log_error_to_console +from ..simulation_run_model import SimulationRunModel +from .cancellable_worker_variants import CancellableProducerConsumerWorker + +if TYPE_CHECKING: + from mqt.syrec import AnnotatableQuantumComputation + + from .cancellable_worker_variants import BatchTimestamps, QueueConfig + + +@dataclass(frozen=True) +class SimulationRunResult: + simulation_run_number: int + actual_output_state: NBitValuesContainer + do_expected_and_actual_outputs_match: bool | None + sim_runtime_in_ms: float + + +class SimulationRunWorker(CancellableProducerConsumerWorker[SimulationRunModel, SimulationRunResult]): + def __init__( + self, + annotatable_quantum_computation: AnnotatableQuantumComputation, + 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._annotatable_quantum_computation: Final[AnnotatableQuantumComputation] = 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: + curr_sim_run_num: int = 0 + 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 + + found_outputs_mismatch: bool = False + has_reached_end_sentinel: bool = False + + 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 + + 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( + output_state_mismatch_flag=found_outputs_mismatch, + reached_end_sentinel_flag=has_reached_end_sentinel, + ) + or n_remaining_batch_elems_to_generate < 0 + ): + break + + dequeued_sim_run_model: SimulationRunModel | None = None + try: + dequeued_sim_run_model = self.recv_queue.get(block=False) + 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 + + 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 + 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_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: AnnotatableQuantumComputation, + sim_run_num: int, + input_state: NBitValuesContainer, + expected_output_state: NBitValuesContainer | None, + ) -> SimulationRunResult: + actual_output_state = NBitValuesContainer(input_state.size()) + + sim_start_timestamp: Final[float] = SimulationRunWorker.get_timestamp() + 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/syrec_editor.py b/python/mqt/syrec/syrec_editor.py index 0d005452..4345f20d 100644 --- a/python/mqt/syrec/syrec_editor.py +++ b/python/mqt/syrec/syrec_editor.py @@ -22,14 +22,18 @@ AnnotatableQuantumComputation, ConfigurableOptions, IntegerConstantTruncationOperation, - NBitValuesContainer, Program, QubitLabelType, cost_aware_synthesis, line_aware_synthesis, - simple_simulation, ) +from .logger_utils import configure_default_console_logger +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 + if TYPE_CHECKING: from collections.abc import Callable @@ -143,7 +147,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, @@ -192,7 +196,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) @@ -286,21 +290,24 @@ 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.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 + self.setup_actions() self.filename: str @@ -320,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) @@ -354,6 +359,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 @@ -400,8 +411,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() @@ -496,126 +507,31 @@ 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 = NBitValuesContainer(no_of_bits, i) - my_out_bitset = NBitValuesContainer(no_of_bits) - simple_simulation(my_out_bitset, self.annotatable_quantum_computation, my_inp_bitset) - - inp_bitset_with_ancillaes_set = NBitValuesContainer(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, QubitLabelType.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 <UNKNOWN>. - # 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 = "<UNKNOWN>" - - 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() + self.quantum_circuit_sim_runs_dialog.show_optional_comments_in_syrec_program_not_supported_notification() class SyReCHighlighter(QtGui.QSyntaxHighlighter): # type: ignore[misc] @@ -670,21 +586,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) @@ -1291,7 +1192,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.setWindowTitle("SyReC Editor") self.setup_widgets() @@ -1300,7 +1200,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) @@ -1312,7 +1212,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) @@ -1386,6 +1286,7 @@ def setup_toolbar(self) -> None: def main() -> int: + configure_default_console_logger() a = QtWidgets.QApplication([]) w = MainWindow() diff --git a/python/mqt/syrec/widget_check_utils.py b/python/mqt/syrec/widget_check_utils.py new file mode 100644 index 00000000..5bcd0384 --- /dev/null +++ b/python/mqt/syrec/widget_check_utils.py @@ -0,0 +1,55 @@ +# 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: + # 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 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_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( + 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 diff --git a/uv.lock b/uv.lock index 49902367..57753bcd 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,8 +1166,10 @@ wheels = [ name = "mqt-syrec" source = { editable = "." } dependencies = [ + { name = "ijson" }, { name = "mqt-core" }, { name = "pyqt6" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] [package.dev-dependencies] @@ -1125,8 +1218,10 @@ test = [ [package.metadata] requires-dist = [ + { name = "ijson", specifier = ">=3.4.0" }, { name = "mqt-core", specifier = "~=3.4.1" }, { name = "pyqt6", specifier = ">=6.8" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.1.0" }, ] [package.metadata.requires-dev]