Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QC viewer #850

Merged
merged 43 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3be3d22
get_protocol_period: relax assertion for spacer_times
bimac Sep 25, 2024
a607f24
lower alpha for event markers
bimac Sep 25, 2024
68beb14
some cleaning-up
bimac Sep 25, 2024
9f99ce6
remove unused rolenames from DataFrameTableModel, various small fixes
bimac Sep 25, 2024
bfbe100
Update task_qc.py
bimac Sep 25, 2024
6ae91f6
add ColoredDataFrameTableModel
bimac Sep 25, 2024
5644b35
Update ViewEphysQC.py
bimac Sep 25, 2024
074b01a
Update ViewEphysQC.py
bimac Sep 25, 2024
9203f04
Update ViewEphysQC.py
bimac Sep 25, 2024
2f8a5c9
Update ViewEphysQC.py
bimac Sep 26, 2024
4b4b063
moveable sections, tooltips for header
bimac Sep 26, 2024
ddbad74
speed up sort and color handling
bimac Sep 26, 2024
2fbc5c6
add filter for column names
bimac Sep 26, 2024
7bfcf72
Update ViewEphysQC.py
bimac Sep 26, 2024
f227b9a
Update ViewEphysQC.py
bimac Sep 26, 2024
fd30945
allow pinning of columns to that they won't be filtered
bimac Sep 26, 2024
520441c
correct location of context menu popup
bimac Sep 26, 2024
54b0df0
happy colors
bimac Sep 27, 2024
cdbbed5
add signals & slots for ColoredDataFrameTableModel, alpha slider
bimac Sep 27, 2024
57ce078
add picker for colormap
bimac Sep 27, 2024
61fbb6c
Update ViewEphysQC.py
bimac Sep 27, 2024
da50ac9
separate normalization from rgba calculation
bimac Sep 28, 2024
027c2ea
dynamic handling of text color
bimac Sep 28, 2024
7f6dbb6
switch to using pyqtgraph's colormaps
bimac Sep 28, 2024
5608ec1
filter by tokens
bimac Sep 29, 2024
02e9a2c
move models to iblqt
bimac Oct 1, 2024
af0abe3
fix stim freeze indexing issue in ephys_fpga extraction
oliche Oct 2, 2024
4a3627f
change ITI constants from 1s to 500ms
bimac Oct 3, 2024
04ef5bc
sort dataframe, store UI settings
bimac Oct 4, 2024
72aec0f
Update requirements.txt
bimac Oct 4, 2024
c6d480b
add passing status of individual tests
bimac Oct 4, 2024
4c58d00
require iblqt >= 0.2.0
bimac Oct 9, 2024
af96df2
Resolves #853
k1o0 Oct 15, 2024
bbf915d
Revert "Resolves #853"
bimac Oct 16, 2024
4cb2ee3
Remove 'peakVelocity_times` from QC trials table
bimac Oct 16, 2024
f32005d
Revert "Remove 'peakVelocity_times` from QC trials table"
bimac Oct 16, 2024
5e54694
Reapply "Resolves #853"
bimac Oct 16, 2024
a2a5d96
Merge branch 'develop' into qc_viewer
k1o0 Oct 16, 2024
01848f3
Merge branch 'develop' into qc_viewer
bimac Dec 17, 2024
8bfa7e3
Update requirements.txt
bimac Dec 17, 2024
fb3ea31
fix CI
bimac Dec 17, 2024
74c6fda
Merge branch 'qc_viewer' of github.com:int-brain-lab/ibllib into qc_v…
bimac Dec 17, 2024
ad682fe
Update task_qc.py
bimac Dec 17, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
move models to iblqt
  • Loading branch information
bimac committed Oct 1, 2024
commit 02e9a2cd268ca613d74473baac26b69ea72046cb
164 changes: 5 additions & 159 deletions ibllib/qc/task_qc_viewer/ViewEphysQC.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,174 +3,20 @@
import logging

from PyQt5 import QtWidgets
from PyQt5.QtCore import pyqtProperty, Qt, QVariant, QAbstractTableModel, QModelIndex, \
QObject, QPoint, pyqtSignal, pyqtSlot, QCoreApplication, QSettings
from PyQt5.QtGui import QColor, QPalette, QShowEvent
from PyQt5.QtCore import Qt, QModelIndex, QPoint, pyqtSignal, pyqtSlot, QCoreApplication, QSettings
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 pyqtgraph import colormap, ColorMap

from ibllib.misc import qt

_logger = logging.getLogger(__name__)


class DataFrameTableModel(QAbstractTableModel):
def __init__(self, parent: QObject = ..., dataFrame: pd.DataFrame | None = None):
super().__init__(parent)
self._dataframe = pd.DataFrame() if dataFrame is None else dataFrame

def setDataFrame(self, dataFrame: pd.DataFrame):
self.beginResetModel()
self._dataframe = dataFrame.copy()
self.endResetModel()

def dataFrame(self) -> pd.DataFrame:
return self._dataframe

dataFrame = pyqtProperty(pd.DataFrame, fget=dataFrame, fset=setDataFrame)

def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...):
if role in (Qt.DisplayRole, Qt.ToolTipRole):
if orientation == Qt.Horizontal:
return self._dataframe.columns[section]
else:
return str(self._dataframe.index[section])
return QVariant()

def rowCount(self, parent: QModelIndex = ...):
if isinstance(parent, QModelIndex) and parent.isValid():
return 0
return len(self._dataframe.index)

def columnCount(self, parent: QModelIndex = ...):
if isinstance(parent, QModelIndex) and parent.isValid():
return 0
return self._dataframe.columns.size

def data(self, index: QModelIndex, role: int = ...) -> QVariant:
if not index.isValid():
return QVariant()
if role == Qt.DisplayRole:
val = self._dataframe.iloc[index.row(), index.column()]
if isinstance(val, np.generic):
return val.item()
return QVariant(str(val))
return QVariant()

def sort(self, column: int, order: Qt.SortOrder = ...):
if self.columnCount() == 0:
return
column = self._dataframe.columns[column]
self.layoutAboutToBeChanged.emit()
self._dataframe.sort_values(by=column, ascending=not order, inplace=True)
self.layoutChanged.emit()


class ColoredDataFrameTableModel(DataFrameTableModel):
colormapChanged = pyqtSignal(str)
alphaChanged = pyqtSignal(int)
_normData = pd.DataFrame()
_background: np.ndarray
_foreground: np.ndarray
_cmap: ColorMap
_alpha: int

def __init__(self, parent: QObject = ..., dataFrame: pd.DataFrame | None = None,
colormap: str = 'plasma', alpha: int = 255):
super().__init__(parent=parent, dataFrame=dataFrame)
self.modelReset.connect(self._normalizeData)
self.dataChanged.connect(self._normalizeData)
self.colormapChanged.connect(self._defineColors)
self.setColormap(colormap)
self.setAlpha(alpha)

@pyqtSlot(str)
def setColormap(self, name: str):
for source in [None, 'matplotlib', 'colorcet']:
if name in colormap.listMaps(source):
self._cmap = colormap.get(name, source)
self.colormapChanged.emit(name)
return
_logger.warning(f'No such colormap: "{name}"')

def getColormap(self) -> str:
return self._cmap.name

colormap = pyqtProperty(str, fget=getColormap, fset=setColormap)

@pyqtSlot(int)
def setAlpha(self, alpha: int = 255):
_, self._alpha, _ = sorted([0, alpha, 255])
self.alphaChanged.emit(self._alpha)
self.layoutChanged.emit()

def getAlpha(self) -> int:
return self._alpha

alpha = pyqtProperty(int, fget=getAlpha, fset=setAlpha)

def _normalizeData(self):
df = self._dataframe.copy()

# coerce non-bool / non-numeric values to numeric
cols = df.select_dtypes(exclude=['bool', 'number']).columns
df[cols] = df[cols].apply(pd.to_numeric, errors='coerce')

# normalize numeric values, avoiding inf values and division by zero
cols = df.select_dtypes(include=['number']).columns
df[cols].replace([np.inf, -np.inf], np.nan)
m = df[cols].nunique() <= 1 # boolean mask for columns with only 1 unique value
df[cols[m]] = df[cols[m]].where(df[cols[m]].isna(), other=0.0)
cols = cols[~m]
df[cols] = (df[cols] - df[cols].min()) / (df[cols].max() - df[cols].min())

# convert boolean values
cols = df.select_dtypes(include=['bool']).columns
df[cols] = df[cols].astype(float)

# store as property & call _defineColors()
self._normData = df
self._defineColors()

def _defineColors(self):
"""
Define the background and foreground colors according to the table's data.

The background color is set to the colormap-mapped values of the normalized
data, and the foreground color is set to the inverse of the background's
approximated luminosity.

The `layoutChanged` signal is emitted after the colors are defined.
"""
if self._normData.empty:
self._background = np.zeros((0, 0, 3), dtype=int)
self._foreground = np.zeros((0, 0), dtype=int)
else:
m = np.isfinite(self._normData) # binary mask for finite values
self._background = np.ones((*self._normData.shape, 3), dtype=int) * 255
self._background[m] = self._cmap.mapToByte(self._normData.values[m])[:, :3]
self._foreground = 255 - (self._background * np.array([[[0.21, 0.72, 0.07]]])).sum(axis=2).astype(int)
self.layoutChanged.emit()

def data(self, index, role=...):
if not index.isValid():
return QVariant()
if role in (Qt.BackgroundRole, Qt.ForegroundRole):
row = self._dataframe.index[index.row()]
col = index.column()
if role == Qt.BackgroundRole:
val = self._background[row][col]
return QColor.fromRgb(*val, self._alpha)
if role == Qt.ForegroundRole:
val = self._foreground[row][col]
return QColor('black' if (val * self._alpha) < 32512 else 'white')
return super().data(index, role)


class PlotCanvas(FigureCanvasQTAgg):
def __init__(self, parent=None, width=5, height=4, dpi=100, wheel=None):
fig = Figure(figsize=(width, height), dpi=dpi)
Expand Down Expand Up @@ -245,7 +91,7 @@ def __init__(self, parent=None, wheel=None):

# colormap picker
self.comboboxColormap = QtWidgets.QComboBox(self)
colormaps = {self.tableModel.colormap, 'inferno', 'magma', 'plasma'}
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)
Expand Down Expand Up @@ -305,7 +151,7 @@ def pinColumn(self, pin: bool, idx: int | None = None):
self.changeFilter(self.lineEditFilter.text())

def changeFilter(self, string: str):
headers = [self.tableModel.headerData(x, Qt.Horizontal, Qt.DisplayRole).lower()
headers = [self.tableModel.headerData(x, Qt.Horizontal, Qt.DisplayRole).value().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
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tqdm>=4.32.1
iblatlas>=0.5.3
ibl-neuropixel>=1.0.1
iblutil>=1.11.0
iblqt>=0.1.0
mtscomp>=1.0.1
ONE-api~=2.9.rc0
phylib>=2.6.0
Expand Down
Loading