"""Widget to define channels, montage and filters.
"""
from copy import deepcopy
from json import dump, load
from logging import getLogger
from os.path import splitext
from PyQt5.Qt import Qt
from PyQt5.QtGui import (QColor,
)
from PyQt5.QtWidgets import (QAbstractItemView,
QAction,
QColorDialog,
QCheckBox,
QDoubleSpinBox,
QFileDialog,
QFormLayout,
QGridLayout,
QGroupBox,
QHBoxLayout,
QInputDialog,
QLabel,
QListWidget,
QListWidgetItem,
QPushButton,
QVBoxLayout,
QTabWidget,
QWidget
)
from .settings import Config
from .utils import FormFloat, FormStr, FormBool
lg = getLogger(__name__)
[docs]class ConfigChannels(Config):
"""Widget with preferences in Settings window for Channels."""
def __init__(self, update_widget):
super().__init__('channels', update_widget)
[docs] def create_config(self):
box0 = QGroupBox('Channels')
self.index['hp'] = FormFloat()
self.index['lp'] = FormFloat()
self.index['notch'] = FormFloat()
self.index['color'] = FormStr()
self.index['scale'] = FormFloat()
self.index['demean'] = FormBool('')
form_layout = QFormLayout()
form_layout.addRow('Default High-Pass Filter', self.index['hp'])
form_layout.addRow('Default Low-Pass Filter', self.index['lp'])
form_layout.addRow('Default Notch Filter', self.index['notch'])
form_layout.addRow('Default Color', self.index['color'])
form_layout.addRow('Default Scale', self.index['scale'])
form_layout.addRow('Default Demeaning', self.index['demean'])
box0.setLayout(form_layout)
main_layout = QVBoxLayout()
main_layout.addWidget(box0)
main_layout.addStretch(1)
self.setLayout(main_layout)
[docs]class ChannelsGroup(QWidget):
"""Tab inside the Channels widget.
Parameters
----------
chan_name : list of str
list of all the channels in the dataset
config_value : dict
default values for the channels
s_freq : int
sampling frequency (to define max of filter)
Attributes
----------
chan_name : list of str
list of all the channels in the dataset
idx_l0 : QListWidget
list with the channels to plot
idx_l1 : QListWidget
list with the channels to use as reference
idx_hp : QDoubleSpinBox
spin box to indicate the high-pass filter
idx_lp : QDoubleSpinBox
spin box to indicate the low-pass filter
idx_scale : QDoubleSpinBox
spin_box to indicate the group-specific scaling
idx_reref : QPushButton
it triggers a selection of reference channels equal to the channels to
plot.
idx_color : QColor
color of the traces beloning to this channel group (it could be a
property of QWidget)
Notes
-----
TODO: re-referencing should be more flexible, by allowing other types of
referencing.
Use config_value instead of config, because it's easier to pass dict
when loading channels montage.
"""
def __init__(self, chan_name, group_name, config_value, s_freq):
super().__init__()
self.chan_name = chan_name
self.group_name = group_name
self.idx_l0 = QListWidget()
self.idx_l1 = QListWidget()
self.add_channels_to_list(self.idx_l0, add_ref=True)
self.add_channels_to_list(self.idx_l1)
self.idx_hp = QDoubleSpinBox()
hp = config_value['hp']
if hp is None:
hp = 0
self.idx_hp.setValue(hp)
self.idx_hp.setSuffix(' Hz')
self.idx_hp.setDecimals(1)
self.idx_hp.setMaximum(s_freq / 2)
self.idx_hp.setToolTip('0 means no filter')
self.idx_lp = QDoubleSpinBox()
lp = config_value['lp']
if lp is None:
lp = 0
self.idx_lp.setValue(lp)
self.idx_lp.setSuffix(' Hz')
self.idx_lp.setDecimals(1)
self.idx_lp.setMaximum(s_freq / 2)
self.idx_lp.setToolTip('0 means no filter')
self.idx_notch = QDoubleSpinBox()
if 'notch' in config_value: # for backward compatibility
notch = config_value['notch']
else:
notch = None
if notch is None:
notch = 0
self.idx_notch.setValue(notch)
self.idx_notch.setSuffix(' Hz')
self.idx_notch.setDecimals(1)
self.idx_notch.setMaximum(s_freq / 2)
self.idx_notch.setToolTip('50Hz or 60Hz depending on the power line frequency. 0 means no filter')
self.idx_scale = QDoubleSpinBox()
self.idx_scale.setValue(config_value['scale'])
self.idx_scale.setSuffix('x')
self.idx_reref = QPushButton('Average')
self.idx_reref.clicked.connect(self.rereference)
self.idx_color = QColor(config_value['color'])
self.idx_demean = QCheckBox()
self.idx_demean.setCheckState(Qt.Unchecked)
if 'demean' in config_value: # for backward compatibility
if config_value['demean']:
self.idx_demean.setCheckState(Qt.Checked)
l_form = QFormLayout()
l_form.addRow('High-Pass', self.idx_hp)
l_form.addRow('Low-Pass', self.idx_lp)
l_form.addRow('Notch Filter', self.idx_notch)
r_form = QFormLayout()
r_form.addRow('Reference', self.idx_reref)
r_form.addRow('Scaling', self.idx_scale)
r_form.addRow('Demean', self.idx_demean)
l0_layout = QVBoxLayout()
l0_layout.addWidget(QLabel('Active'))
l0_layout.addWidget(self.idx_l0)
l1_layout = QVBoxLayout()
l1_layout.addWidget(QLabel('Reference'))
l1_layout.addWidget(self.idx_l1)
l_layout = QHBoxLayout()
l_layout.addLayout(l0_layout)
l_layout.addLayout(l1_layout)
lr_form = QHBoxLayout()
lr_form.addLayout(l_form)
lr_form.addLayout(r_form)
layout = QVBoxLayout()
layout.addLayout(l_layout)
layout.addLayout(lr_form)
self.setLayout(layout)
[docs] def add_channels_to_list(self, l, add_ref=False):
"""Create list of channels (one for those to plot, one for ref).
Parameters
----------
l : instance of QListWidget
one of the two lists (chan_to_plot or ref_chan)
"""
l.clear()
l.setSelectionMode(QAbstractItemView.ExtendedSelection)
for chan in self.chan_name:
item = QListWidgetItem(chan)
l.addItem(item)
if add_ref:
item = QListWidgetItem('_REF')
l.addItem(item)
[docs] def highlight_channels(self, l, selected_chan):
"""Highlight channels in the list of channels.
Parameters
----------
selected_chan : list of str
channels to indicate as selected.
"""
for row in range(l.count()):
item = l.item(row)
if item.text() in selected_chan:
item.setSelected(True)
else:
item.setSelected(False)
[docs] def rereference(self):
"""Automatically highlight channels to use as reference, based on
selected channels."""
selectedItems = self.idx_l0.selectedItems()
chan_to_plot = []
for selected in selectedItems:
chan_to_plot.append(selected.text())
self.highlight_channels(self.idx_l1, chan_to_plot)
[docs] def get_info(self):
"""Get the information about the channel groups.
Returns
-------
dict
information about this channel group
Notes
-----
The items in selectedItems() are ordered based on the user's selection
(which appears pretty random). It's more consistent to use the same
order of the main channel list. That's why the additional for-loop
is necessary. We don't care about the order of the reference channels.
"""
selectedItems = self.idx_l0.selectedItems()
selected_chan = [x.text() for x in selectedItems]
chan_to_plot = []
for chan in self.chan_name + ['_REF']:
if chan in selected_chan:
chan_to_plot.append(chan)
selectedItems = self.idx_l1.selectedItems()
ref_chan = []
for selected in selectedItems:
ref_chan.append(selected.text())
hp = self.idx_hp.value()
if hp == 0:
low_cut = None
else:
low_cut = hp
lp = self.idx_lp.value()
if lp == 0:
high_cut = None
else:
high_cut = lp
notch_val = self.idx_notch.value()
if notch_val == 0:
notch = None
else:
notch = notch_val
scale = self.idx_scale.value()
demean = self.idx_demean.isChecked()
group_info = {'name': self.group_name,
'chan_to_plot': chan_to_plot,
'ref_chan': ref_chan,
'hp': low_cut,
'lp': high_cut,
'notch': notch,
'scale': float(scale),
'color': self.idx_color,
'demean': demean,
}
return group_info
[docs]class Channels(QWidget):
"""Widget with information about channel groups.
Attributes
----------
parent : QMainWindow
the main window
config : ConfigChannels
preferences for this widget
filename : path to file
file with the channel groups
groups : list of dict
each dict contains information about one channel group
tabs : QTabWidget
Widget that contains the tabs with channel groups
"""
def __init__(self, parent):
super().__init__()
self.parent = parent
self.config = ConfigChannels(lambda: None)
self.filename = None
self.groups = []
self.tabs = None
self.create()
self.create_action()
[docs] def create(self):
"""Create Channels Widget"""
add_button = QPushButton('New')
add_button.clicked.connect(self.new_group)
color_button = QPushButton('Color')
color_button.clicked.connect(self.color_group)
del_button = QPushButton('Delete')
del_button.clicked.connect(self.del_group)
apply_button = QPushButton('Apply')
apply_button.clicked.connect(self.apply)
self.button_add = add_button
self.button_color = color_button
self.button_del = del_button
self.button_apply = apply_button
buttons = QGridLayout()
buttons.addWidget(add_button, 0, 0)
buttons.addWidget(color_button, 1, 0)
buttons.addWidget(del_button, 0, 1)
buttons.addWidget(apply_button, 1, 1)
self.tabs = QTabWidget()
layout = QVBoxLayout()
layout.addLayout(buttons)
layout.addWidget(self.tabs)
self.setLayout(layout)
self.setEnabled(False)
self.button_color.setEnabled(False)
self.button_del.setEnabled(False)
self.button_apply.setEnabled(False)
[docs] def create_action(self):
"""Create actions related to channel selection."""
actions = {}
act = QAction('Load Montage...', self)
act.triggered.connect(self.load_channels)
act.setEnabled(False)
actions['load_channels'] = act
act = QAction('Save Montage...', self)
act.triggered.connect(self.save_channels)
act.setEnabled(False)
actions['save_channels'] = act
self.action = actions
[docs] def update(self):
self.setEnabled(True)
self.action['load_channels'].setEnabled(True)
self.action['save_channels'].setEnabled(True)
[docs] def new_group(self, checked=False, test_name=None):
"""Create a new channel group.
Parameters
----------
checked : bool
comes from QAbstractButton.clicked
test_name : str
used for testing purposes to avoid modal window
Notes
-----
Don't call self.apply() just yet, only if the user wants it.
"""
chan_name = self.parent.labels.chan_name
if chan_name is None:
msg = 'No dataset loaded'
self.parent.statusBar().showMessage(msg)
lg.debug(msg)
else:
if test_name is None:
new_name = QInputDialog.getText(self, 'New Channel Group',
'Enter Name')
else:
new_name = [test_name, True] # like output of getText
if new_name[1]:
s_freq = self.parent.info.dataset.header['s_freq']
group = ChannelsGroup(chan_name, new_name[0],
self.config.value, s_freq)
self.tabs.addTab(group, new_name[0])
self.tabs.setCurrentIndex(self.tabs.currentIndex() + 1)
# activate buttons
self.button_color.setEnabled(True)
self.button_del.setEnabled(True)
self.button_apply.setEnabled(True)
[docs] def color_group(self, checked=False, test_color=None):
"""Change the color of the group."""
group = self.tabs.currentWidget()
if test_color is None:
newcolor = QColorDialog.getColor(group.idx_color)
else:
newcolor = test_color
group.idx_color = newcolor
self.apply()
[docs] def del_group(self):
"""Delete current group."""
idx = self.tabs.currentIndex()
self.tabs.removeTab(idx)
self.apply()
[docs] def apply(self):
"""Apply changes to the plots."""
self.read_group_info()
if self.tabs.count() == 0:
# disactivate buttons
self.button_color.setEnabled(False)
self.button_del.setEnabled(False)
self.button_apply.setEnabled(False)
else:
# activate buttons
self.button_color.setEnabled(True)
self.button_del.setEnabled(True)
self.button_apply.setEnabled(True)
if self.groups:
self.parent.overview.update_position()
self.parent.spectrum.update()
self.parent.notes.enable_events()
else:
self.parent.traces.reset()
self.parent.spectrum.reset()
self.parent.notes.enable_events()
[docs] def read_group_info(self):
"""Get information about groups directly from the widget."""
self.groups = []
for i in range(self.tabs.count()):
one_group = self.tabs.widget(i).get_info()
# one_group['name'] = self.tabs.tabText(i)
self.groups.append(one_group)
[docs] def load_channels(self, checked=False, test_name=None):
"""Load channel groups from file.
Parameters
----------
test_name : path to file
when debugging the function, you can open a channels file from the
command line
"""
chan_name = self.parent.labels.chan_name
if self.filename is not None:
filename = self.filename
elif self.parent.info.filename is not None:
filename = (splitext(self.parent.info.filename)[0] +
'_channels.json')
else:
filename = None
if test_name is None:
filename, _ = QFileDialog.getOpenFileName(self,
'Open Channels Montage',
filename,
'Channels File (*.json)')
else:
filename = test_name
if filename == '':
return
self.filename = filename
with open(filename, 'r') as outfile:
groups = load(outfile)
s_freq = self.parent.info.dataset.header['s_freq']
no_in_dataset = []
for one_grp in groups:
no_in_dataset.extend(set(one_grp['chan_to_plot']) -
set(chan_name))
chan_to_plot = set(chan_name) & set(one_grp['chan_to_plot'])
ref_chan = set(chan_name) & set(one_grp['ref_chan'])
group = ChannelsGroup(chan_name, one_grp['name'], one_grp, s_freq)
group.highlight_channels(group.idx_l0, chan_to_plot)
group.highlight_channels(group.idx_l1, ref_chan)
self.tabs.addTab(group, one_grp['name'])
if no_in_dataset:
msg = 'Channels not present in the dataset: ' + ', '.join(no_in_dataset)
self.parent.statusBar().showMessage(msg)
lg.debug(msg)
self.apply()
[docs] def save_channels(self, checked=False, test_name=None):
"""Save channel groups to file."""
self.read_group_info()
if self.filename is not None:
filename = self.filename
elif self.parent.info.filename is not None:
filename = (splitext(self.parent.info.filename)[0] +
'_channels.json')
else:
filename = None
if test_name is None:
filename, _ = QFileDialog.getSaveFileName(self,
'Save Channels Montage',
filename,
'Channels File (*.json)')
else:
filename = test_name
if filename == '':
return
self.filename = filename
groups = deepcopy(self.groups)
for one_grp in groups:
one_grp['color'] = one_grp['color'].rgba()
with open(filename, 'w') as outfile:
dump(groups, outfile, indent=' ')
[docs] def reset(self):
"""Reset all the information of this widget."""
self.filename = None
self.groups = []
self.tabs.clear()
self.setEnabled(False)
self.button_color.setEnabled(False)
self.button_del.setEnabled(False)
self.button_apply.setEnabled(False)
self.action['load_channels'].setEnabled(False)
self.action['save_channels'].setEnabled(False)