Source code for ibllib.qc.task_qc_viewer.ViewEphysQC

"""An interactive PyQT QC data frame."""

import logging

from PyQt5 import QtWidgets
from PyQt5.QtCore import (
    Qt,
    QModelIndex,
    pyqtSignal,
    pyqtSlot,
    QCoreApplication,
    QSettings,
    QSize,
    QPoint,
)
from PyQt5.QtGui import QPalette, QShowEvent
from PyQt5.QtWidgets import QMenu, QAction
from iblqt.core import ColoredDataFrameTableModel
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT
import pandas as pd
import numpy as np

from ibllib.misc import qt

_logger = logging.getLogger(__name__)


[docs] class PlotCanvas(FigureCanvasQTAgg): def __init__(self, parent=None, width=5, height=4, dpi=100, wheel=None): fig = Figure(figsize=(width, height), dpi=dpi) FigureCanvasQTAgg.__init__(self, fig) self.setParent(parent) FigureCanvasQTAgg.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) FigureCanvasQTAgg.updateGeometry(self) if wheel: self.ax, self.ax2 = fig.subplots(2, 1, gridspec_kw={'height_ratios': [2, 1]}, sharex=True) else: self.ax = fig.add_subplot(111) self.draw()
[docs] class PlotWindow(QtWidgets.QWidget): def __init__(self, parent=None, wheel=None): QtWidgets.QWidget.__init__(self, parent=None) self.canvas = PlotCanvas(wheel=wheel) self.vbl = QtWidgets.QVBoxLayout() # Set box for plotting self.vbl.addWidget(self.canvas) self.setLayout(self.vbl) self.vbl.addWidget(NavigationToolbar2QT(self.canvas, self))
[docs] class GraphWindow(QtWidgets.QWidget): _pinnedColumns = [] def __init__(self, parent=None, wheel=None): QtWidgets.QWidget.__init__(self, parent=parent) self.columnPinned = pyqtSignal(int, bool) # load button self.pushButtonLoad = QtWidgets.QPushButton('Select File', self) self.pushButtonLoad.clicked.connect(self.loadFile) # define table model & view self.tableModel = ColoredDataFrameTableModel(self) self.tableView = QtWidgets.QTableView(self) self.tableView.setModel(self.tableModel) self.tableView.setSortingEnabled(True) self.tableView.horizontalHeader().setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.tableView.horizontalHeader().setSectionsMovable(True) self.tableView.horizontalHeader().setContextMenuPolicy(Qt.CustomContextMenu) self.tableView.horizontalHeader().customContextMenuRequested.connect(self.contextMenu) self.tableView.verticalHeader().hide() self.tableView.doubleClicked.connect(self.tv_double_clicked) # define colors for highlighted cells p = self.tableView.palette() p.setColor(QPalette.Highlight, Qt.black) p.setColor(QPalette.HighlightedText, Qt.white) self.tableView.setPalette(p) # QAction for pinning columns self.pinAction = QAction('Pin column', self) self.pinAction.setCheckable(True) self.pinAction.toggled.connect(self.pinColumn) # Filter columns by name self.lineEditFilter = QtWidgets.QLineEdit(self) self.lineEditFilter.setPlaceholderText('Filter columns') self.lineEditFilter.textChanged.connect(self.changeFilter) self.lineEditFilter.setMinimumWidth(200) # colormap picker self.comboboxColormap = QtWidgets.QComboBox(self) colormaps = {self.tableModel.colormap, 'inferno', 'magma', 'plasma', 'summer'} self.comboboxColormap.addItems(sorted(list(colormaps))) self.comboboxColormap.setCurrentText(self.tableModel.colormap) self.comboboxColormap.currentTextChanged.connect(self.tableModel.setColormap) # slider for alpha values self.sliderAlpha = QtWidgets.QSlider(Qt.Horizontal, self) self.sliderAlpha.setMaximumWidth(100) self.sliderAlpha.setMinimum(0) self.sliderAlpha.setMaximum(255) self.sliderAlpha.setValue(self.tableModel.alpha) self.sliderAlpha.valueChanged.connect(self.tableModel.setAlpha) # Horizontal layout hLayout = QtWidgets.QHBoxLayout() hLayout.addWidget(self.lineEditFilter) hLayout.addSpacing(50) hLayout.addWidget(QtWidgets.QLabel('Colormap', self)) hLayout.addWidget(self.comboboxColormap) hLayout.addWidget(QtWidgets.QLabel('Alpha', self)) hLayout.addWidget(self.sliderAlpha) hLayout.addSpacing(50) hLayout.addWidget(self.pushButtonLoad) # Vertical layout vLayout = QtWidgets.QVBoxLayout(self) vLayout.addLayout(hLayout) vLayout.addWidget(self.tableView) # Recover layout from QSettings self.settings = QSettings() self.settings.beginGroup('MainWindow') self.resize(self.settings.value('size', QSize(800, 600), QSize)) self.comboboxColormap.setCurrentText(self.settings.value('colormap', 'plasma', str)) self.sliderAlpha.setValue(self.settings.value('alpha', 255, int)) self.settings.endGroup() self.wplot = PlotWindow(wheel=wheel) self.wplot.show() self.tableModel.dataChanged.connect(self.wplot.canvas.draw) self.wheel = wheel
[docs] def closeEvent(self, _) -> bool: self.settings.beginGroup('MainWindow') self.settings.setValue('size', self.size()) self.settings.setValue('colormap', self.tableModel.colormap) self.settings.setValue('alpha', self.tableModel.alpha) self.settings.endGroup() self.wplot.close()
[docs] def showEvent(self, a0: QShowEvent) -> None: super().showEvent(a0) self.activateWindow()
[docs] def contextMenu(self, pos: QPoint): idx = self.sender().logicalIndexAt(pos) action = self.pinAction action.setData(idx) action.setChecked(idx in self._pinnedColumns) menu = QMenu(self) menu.addAction(action) menu.exec(self.sender().mapToGlobal(pos))
[docs] @pyqtSlot(bool) @pyqtSlot(bool, int) def pinColumn(self, pin: bool, idx: int | None = None): idx = idx if idx is not None else self.sender().data() if not pin and idx in self._pinnedColumns: self._pinnedColumns.remove(idx) if pin and idx not in self._pinnedColumns: self._pinnedColumns.append(idx) self.changeFilter(self.lineEditFilter.text())
[docs] def changeFilter(self, string: str): headers = [ self.tableModel.headerData(x, Qt.Horizontal, Qt.DisplayRole).lower() for x in range(self.tableModel.columnCount()) ] tokens = [y.lower() for y in (x.strip() for x in string.split(',')) if len(y)] showAll = len(tokens) == 0 for idx, column in enumerate(headers): show = showAll or any((t in column for t in tokens)) or idx in self._pinnedColumns self.tableView.setColumnHidden(idx, not show)
[docs] def loadFile(self): fileName, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open File', '', 'CSV Files (*.csv)') if len(fileName) == 0: return df = pd.read_csv(fileName) self.updateDataframe(df)
[docs] def updateDataframe(self, df: pd.DataFrame): # clear pinned columns self._pinnedColumns = [] # try to identify and sort columns containing timestamps col_names = df.select_dtypes('number').columns df_interp = df[col_names].replace([-np.inf, np.inf], np.nan) df_interp = df_interp.interpolate(limit_direction='both') cols_mono = col_names[[df_interp[c].is_monotonic_increasing for c in col_names]] cols_mono = [c for c in cols_mono if df[c].nunique() > 1] cols_mono = df_interp[cols_mono].mean().sort_values().keys() for idx, col_name in enumerate(cols_mono): df.insert(idx, col_name, df.pop(col_name)) # columns containing boolean values are sorted to the end # of those, columns containing 'pass' in their title will be sorted by number of False values col_names = df.columns cols_bool = list(df.select_dtypes(['bool', 'boolean']).columns) cols_pass = [c for c in cols_bool if 'pass' in c] cols_bool = [c for c in cols_bool if c not in cols_pass] # I know. Friday evening, brain is fried ... sorry. cols_pass = list((~df[cols_pass]).sum().sort_values().keys()) cols_bool += cols_pass for col_name in cols_bool: df = df.join(df.pop(col_name)) # trial_no should always be the first column if 'trial_no' in col_names: df.insert(0, 'trial_no', df.pop('trial_no')) # define columns that should be pinned by default for col in ['trial_no']: self._pinnedColumns.append(df.columns.get_loc(col)) self.tableModel.setDataFrame(df)
[docs] def tv_double_clicked(self, index: QModelIndex): data = self.tableModel.dataFrame.iloc[index.row()] t0 = data['intervals_0'] t1 = data['intervals_1'] dt = t1 - t0 if self.wheel: idx = np.searchsorted(self.wheel['re_ts'], np.array([t0 - dt / 10, t1 + dt / 10])) period = self.wheel['re_pos'][idx[0]:idx[1]] if period.size == 0: _logger.warning('No wheel data during trial #%i', index.row()) else: min_val, max_val = np.min(period), np.max(period) self.wplot.canvas.ax2.set_ylim(min_val - 1, max_val + 1) self.wplot.canvas.ax2.set_xlim(t0 - dt / 10, t1 + dt / 10) self.wplot.canvas.ax.set_xlim(t0 - dt / 10, t1 + dt / 10) self.wplot.setWindowTitle(f"Trial {data.get('trial_no', '?')}") self.wplot.canvas.draw()
[docs] def viewqc(qc=None, title=None, wheel=None): app = qt.create_app() app.setStyle('Fusion') QCoreApplication.setOrganizationName('International Brain Laboratory') QCoreApplication.setOrganizationDomain('internationalbrainlab.org') QCoreApplication.setApplicationName('QC Viewer') qcw = GraphWindow(wheel=wheel) qcw.setWindowTitle(title) if qc is not None: qcw.updateDataframe(qc) qcw.show() return qcw