Source code for ibl_alignment_gui.plugins.qc_dialog

from typing import TYPE_CHECKING

from qtpy import QtWidgets

if TYPE_CHECKING:
    from ibl_alignment_gui.app.controllers.app_controller import (
        AlignmentGUIController,
        AlignmentGUIView,
    )

PLUGIN_NAME = 'QC dialog'


def _get_critical_descriptions() -> list[str]:
    """Return the QC reason descriptions, or an empty list if ``ibllib`` is missing.

    The reasons come from ``ibllib`` (the optional ``ibl`` extra). QC is only submitted in online
    mode, so in offline mode without ``ibllib`` the dialog is still built, simply without reason
    checkboxes.

    Returns
    -------
    list[str]
        The QC reason descriptions used to populate the dialog checkboxes.
    """
    try:
        from ibllib.qc import critical_reasons  # noqa: PLC0415
    except ImportError:
        return []
    return critical_reasons.CriticalInsertionNote.descriptions_gui


[docs] def setup(controller: 'AlignmentGUIController') -> None: """ Set up the QC dialog and connect its accepted signal to the callback. Parameters ---------- controller: AlignmentController The main application controller. """ controller.qc_dialog = QCDialog(controller.view) controller.qc_dialog.accepted.connect(lambda: callback(controller))
[docs] def display( controller: 'AlignmentGUIController', shank: str, allow_apply_all: bool = False ) -> int: """ Show the QC dialog. Parameters ---------- controller: The main application controller. shank: str The shank identifier for which the QC dialog is displayed. allow_apply_all: bool, default=False Whether to offer the option to apply the QC information to all the other shanks. Returns ------- int The result of the dialog execution (Accepted or Rejected). """ # The dialog is reused, so the option must be reset each time it is shown controller.qc_dialog.apply_all.setChecked(False) controller.qc_dialog.apply_all.setVisible(allow_apply_all) controller.qc_dialog.setWindowTitle(f'QC assessment {shank}') return controller.qc_dialog.exec_()
[docs] def callback(controller: 'AlignmentGUIController'): """ Gather QC inputs and update upload loader with the QC information. Parameters ---------- controller: AlignmentController The main application controller. """ apply_to_shanks(controller, [controller.model.selected_shank])
[docs] def apply_to_shanks(controller: 'AlignmentGUIController', shanks: list[str]) -> None: """ Update the upload loaders of the given shanks with the QC information from the dialog. Parameters ---------- controller: AlignmentController The main application controller. shanks: list of str The shanks to apply the QC information to. """ qc = controller.qc_dialog.get_qc() for shank in shanks: # Get the uploader for the shank and default configuration upload = controller.model.get_current_shank( shank, controller.model.default_config ).loaders['upload'] # Pass in the QC information from the dialog upload.set_user_qc(*qc)
[docs] class QCDialog(QtWidgets.QDialog): """ Dialog for collecting QC information from the user. Parameters ---------- view : AlignmentGUIView The main application view. """ def __init__(self, view: 'AlignmentGUIView'): super().__init__(view) self.setWindowTitle('QC assessment') self.resize(300, 150) self.setup() @property def apply_to_all(self) -> bool: """Whether the assessment should be applied to all the shanks being uploaded.""" return self.apply_all.isChecked()
[docs] def setup(self) -> None: """Set up the dialog layout and widgets.""" # Alignment QC align_qc_label = QtWidgets.QLabel('Confidence of alignment:') self.align_qc = QtWidgets.QComboBox() self.align_qc.addItems(['High', 'Medium', 'Low']) # Ephys QC ephys_qc_label = QtWidgets.QLabel('QC for ephys recording:') self.ephys_qc = QtWidgets.QComboBox() self.ephys_qc.addItems(['Pass', 'Warning', 'Critical']) # Ephys QC descriptions self.desc_buttons = QtWidgets.QButtonGroup() self.desc_buttons.setExclusive(False) desc_layout = QtWidgets.QVBoxLayout() for i, label in enumerate(_get_critical_descriptions()): checkbox = QtWidgets.QCheckBox(label) self.desc_buttons.addButton(checkbox, i) desc_layout.addWidget(checkbox) desc_group = QtWidgets.QGroupBox('Describe problem with recording:') desc_group.setLayout(desc_layout) # Force upload option resolve_label = QtWidgets.QLabel( 'Do you want to resolve this alignment with the current alignment?' ) self.resolve = QtWidgets.QComboBox() self.resolve.addItem('No', False) self.resolve.addItem('Yes', True) # Option to give all the other shanks the same assessment, rather than being asked # about each of them in turn. Only shown when there is more than one shank to assess. self.apply_all = QtWidgets.QCheckBox('Apply to all shanks') self.apply_all.setChecked(False) self.apply_all.setVisible(False) # Dialog buttons button_box = QtWidgets.QDialogButtonBox( QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel ) button_box.accepted.connect(self.on_accept) button_box.rejected.connect(self.reject) # Assemble layout dialog_layout = QtWidgets.QVBoxLayout() dialog_layout.addWidget(align_qc_label) dialog_layout.addWidget(self.align_qc) dialog_layout.addWidget(ephys_qc_label) dialog_layout.addWidget(self.ephys_qc) dialog_layout.addWidget(desc_group) dialog_layout.addWidget(resolve_label) dialog_layout.addWidget(self.resolve) dialog_layout.addWidget(self.apply_all) dialog_layout.addWidget(button_box) self.setLayout(dialog_layout)
[docs] def on_accept(self) -> None: """Validate the input before accepting.""" ephys_qc = self.ephys_qc.currentText() ephys_desc = [btn.text() for btn in self.desc_buttons.buttons() if btn.isChecked()] if ephys_qc != 'Pass' and not ephys_desc: QtWidgets.QMessageBox.warning( self, 'Missing Information', 'You must select a reason for QC choice' ) return self.accept()
[docs] def get_qc(self) -> tuple[str, str, list[str], bool]: """ Retrieve the QC information from the dialog. Returns ------- tuple[str, str, list[str], bool] A tuple containing alignment QC, ephys QC, ephys descriptions, and force resolve flag """ align_qc = self.align_qc.currentText() ephys_qc = self.ephys_qc.currentText() ephys_desc = [btn.text() for btn in self.desc_buttons.buttons() if btn.isChecked()] force_resolve = self.resolve.currentData() return align_qc, ephys_qc, ephys_desc, force_resolve