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

Enh: channel sensitivity ui #273

Merged
merged 21 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
161 changes: 131 additions & 30 deletions mne_qt_browser/_pg_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,37 @@ def _get_channel_scaling(widget, ch_type):
return inv_norm


def _calc_chan_type_to_physical(widget, ch_type, units="mm"):
"""Convert data to physical units."""
# Get the ViewBox and its height in pixels
vb = widget.mne.viewbox
height_px = vb.geometry().height()

# Get the view range in data units (here we write V for logical simplicity and
# dimensional analysis but it works for any underlying data unit)
view_range = vb.viewRange()
height_V = view_range[1][1] - view_range[1][0]

# Calculate the pixel-to-data ratio
if height_V == 0:
return 0

# Get the screen DPI
px_per_in = QApplication.primaryScreen().logicalDotsPerInch()

# Convert to inches
height_in = height_px / px_per_in

# Convert pixels to inches
in_per_V = height_in / height_V

# Convert inches to millimeters (or something else, but using mm in the name for
# simplicity)
mm_per_in = dict(mm=25.4, cm=2.54, inch=1.0)[units]
mm_per_V = in_per_V * mm_per_in
return _get_channel_scaling(widget, ch_type) / mm_per_V


def propagate_to_children(method): # noqa: D103
@functools.wraps(method)
def wrapper(*args, **kwargs):
Expand Down Expand Up @@ -1658,7 +1689,9 @@ def __init__(self, mne, ch_type):
QGraphicsLineItem.__init__(self)

self.setZValue(1)
self.setPen(self.mne.mkPen(color="#AA3377", width=5))
pen = self.mne.mkPen(color="#AA3377", width=5)
pen.setCapStyle(Qt.FlatCap)
self.setPen(pen)
self.update_y_position()

def _set_position(self, x, y):
Expand Down Expand Up @@ -1809,34 +1842,83 @@ def __init__(self, main, title="Settings", **kwargs):
self.scroll_sensitivity_slider.setValue(self.mne.scroll_sensitivity)
layout.addRow("horizontal scroll sensitivity", self.scroll_sensitivity_slider)

# Add subgroup box to show channel type scalings
layout.addItem(QSpacerItem(10, 10, QSizePolicy.Minimum, QSizePolicy.Expanding))
ch_scaling_box = QGroupBox("Channel Type Scalings")
ch_scaling_box.setStyleSheet("QGroupBox { font-size: 12pt; }")
ch_scaling_layout = QFormLayout()
self.ch_scaling_spinboxes = {}

# Get all unique channel types and allow scaling
# Get all unique channel types
ordered_types = self.mne.ch_types[self.mne.ch_order]
unique_type_idxs = np.unique(ordered_types, return_index=True)[1]
ch_types_ordered = [ordered_types[idx] for idx in sorted(unique_type_idxs)]
for ch in ch_types_ordered:
if ch in self.mne.unit_scalings.keys():
ch_spinbox = QDoubleSpinBox()
ch_spinbox.setMinimumWidth(100)
ch_spinbox.setRange(0, float("inf"))
ch_spinbox.setDecimals(1)
ch_spinbox.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType)
inv_norm = _get_channel_scaling(self, ch)
ch_spinbox.setValue(inv_norm)
ch_spinbox.valueChanged.connect(
_methpartial(self._update_spinbox_values, ch_type=ch)
)
self.ch_scaling_spinboxes[ch] = ch_spinbox
ch_scaling_layout.addRow(f"{ch} ({self.mne.units[ch]})", ch_spinbox)
ch_types_ordered = [
ordered_types[idx]
for idx in sorted(unique_type_idxs)
if ordered_types[idx] in self.mne.unit_scalings
]

# Grid layout for channel spinboxes and settings
ch_grid_layout = QGridLayout()

# Create dropdown to choose units
self.physical_units_cmbx = QComboBox()
self.physical_units_cmbx.addItems(["/ mm", "/ cm", "/ inch"])
self.physical_units_cmbx.currentIndexChanged.connect(
self._update_sensitivity_spinbox_values
)
current_units = self.physical_units_cmbx.currentText().split()[-1]

ch_scaling_box.setLayout(ch_scaling_layout)
layout.addRow(ch_scaling_box)
# Add subgroup box to show channel type scalings
ch_scroll_box = QGroupBox("Channel Configuration")
ch_scroll_box.setStyleSheet("QGroupBox { font-size: 12pt; }")
self.ch_scaling_spinboxes = {}
# self.ch_scaling_spinbox_labels = {}
self.ch_sensitivity_spinboxes = {}
# self.ch_sensitivity_spinbox_labels = {}
self.ch_label_widgets = {}

ch_grid_layout.addWidget(QLabel("Channel Type"), 0, 0)
ch_grid_layout.addWidget(QLabel("Scaling"), 0, 1)
ch_grid_layout.addWidget(QLabel("Sensitivity"), 0, 2)
grid_row = 1
for ch_type in ch_types_ordered:
self.ch_label_widgets[ch_type] = QLabel(
f"{ch_type} ({self.mne.units[ch_type]})"
)

# Make scaling spinbox first
ch_scale_spinbox = QDoubleSpinBox()
ch_scale_spinbox.setMinimumWidth(100)
ch_scale_spinbox.setRange(0, float("inf"))
ch_scale_spinbox.setDecimals(1)
ch_scale_spinbox.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType)
inv_norm = _get_channel_scaling(self, ch_type)
ch_scale_spinbox.setValue(inv_norm)
ch_scale_spinbox.valueChanged.connect(
_methpartial(self._update_scaling_spinbox_values, ch_type=ch_type)
)
self.ch_scaling_spinboxes[ch_type] = ch_scale_spinbox

# Now make sensitivity spinbox
ch_sens_spinbox = QDoubleSpinBox()
ch_sens_spinbox.setMinimumWidth(100)
ch_sens_spinbox.setRange(0, float("inf"))
ch_sens_spinbox.setDecimals(1)
ch_sens_spinbox.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType)
ch_sens_spinbox.setReadOnly(True)
ch_sens_spinbox.setDisabled(True)
ch_sens_spinbox.setValue(
_calc_chan_type_to_physical(self, ch_type, units=current_units)
)
self.ch_sensitivity_spinboxes[ch_type] = ch_sens_spinbox

# Add these to the layout
ch_grid_layout.addWidget(self.ch_label_widgets[ch_type], grid_row, 0)
ch_grid_layout.addWidget(ch_scale_spinbox, grid_row, 1)
ch_grid_layout.addWidget(ch_sens_spinbox, grid_row, 2)
grid_row += 1

ch_grid_layout.addWidget(self.physical_units_cmbx, grid_row, 2)

ch_scroll_box.setLayout(ch_grid_layout)

layout.addRow(ch_scroll_box)

self.setLayout(layout)
self.show()
Expand All @@ -1860,7 +1942,7 @@ def _value_changed(self, new_value, value_name):
def _toggle_antialiasing(self, _):
self.weakmain()._toggle_antialiasing()

def _update_spinbox_values(self, *args, **kwargs):
def _update_scaling_spinbox_values(self, *args, **kwargs):
"""Update spinbox values. If any args or kwargs do a specific channel update."""
# If new value for a channel given update that channel type and redraw
if len(args) > 0:
Expand All @@ -1870,7 +1952,7 @@ def _update_spinbox_values(self, *args, **kwargs):
# If new_value is 0 then scaling is stuck on 0.
# To get out of 0 set scalings to 1 and then set the new value
if new_value == 0:
self.mne.scalings[ch_type] = 0
self.mne.scalings[ch_type] = 1e-12
else:
self.mne.scalings[ch_type] = 1
self.mne.scalings[ch_type] = new_value / _get_channel_scaling(
Expand All @@ -1885,6 +1967,16 @@ def _update_spinbox_values(self, *args, **kwargs):
for ch_type, spinbox in self.ch_scaling_spinboxes.items():
spinbox.setValue(_get_channel_scaling(self, ch_type))

self._update_sensitivity_spinbox_values()

def _update_sensitivity_spinbox_values(self):
"""Update sensitivity spinbox values."""
current_units = self.physical_units_cmbx.currentText().split()[-1]
for ch_type in self.ch_scaling_spinboxes:
self.ch_sensitivity_spinboxes[ch_type].setValue(
_calc_chan_type_to_physical(self, ch_type, units=current_units)
)


class HelpDialog(_BaseDialog):
"""Shows all keyboard-shortcuts."""
Expand Down Expand Up @@ -3909,7 +4001,7 @@ def _update_scalebar_values(self):

def _update_ch_spinbox_values(self):
if self.mne.fig_settings is not None:
self.mne.fig_settings.update_all_spinboxes()
self.mne.fig_settings._update_scaling_spinbox_values()

def _set_scalebars_visible(self, visible):
for scalebar in self.mne.scalebars.values():
Expand Down Expand Up @@ -3949,7 +4041,7 @@ def scale_all(self, checked=False, *, step):
# Update Scalebars
self._update_scalebar_values()
if self.mne.fig_settings is not None:
self.mne.fig_settings._update_spinbox_values()
self.mne.fig_settings._update_scaling_spinbox_values()

def hscroll(self, step):
"""Scroll horizontally by step."""
Expand Down Expand Up @@ -4069,6 +4161,9 @@ def change_nchan(self, checked=False, *, step):
self.mne.ax_vscroll.update_nchan()
self.mne.plt.setYRange(ymin, ymax, padding=0)

if self.mne.fig_settings is not None:
self.mne.fig_settings._update_sensitivity_spinbox_values()

def _remove_vline(self):
if self.mne.vline is not None:
if self.mne.is_epochs:
Expand Down Expand Up @@ -4758,7 +4853,6 @@ def _set_butterfly(self, butterfly):
self.mne.butterfly = butterfly
self._update_picks()
self._update_data()
self._update_ch_spinbox_values()

if butterfly and self.mne.fig_selection is not None:
self.mne.selection_ypos_dict.clear()
Expand Down Expand Up @@ -4803,6 +4897,8 @@ def _set_butterfly(self, butterfly):

self._draw_traces()

self._update_ch_spinbox_values()

def _toggle_butterfly(self):
if self.mne.instance_type != "ica":
self._set_butterfly(not self.mne.butterfly)
Expand Down Expand Up @@ -4986,7 +5082,7 @@ def _draw_traces(self):
def _get_size(self):
inch_width = self.width() / self.logicalDpiX()
inch_height = self.height() / self.logicalDpiY()

logger.debug(f"Window size: {inch_width:0.1f} x {inch_height:0.1f} inches")
return inch_width, inch_height

def _fake_keypress(self, key, fig=None):
Expand Down Expand Up @@ -5214,6 +5310,11 @@ def closeEvent(self, event):
self.deleteLater()
self._closed = True

def resizeEvent(self, event):
super().resizeEvent(event)
if self.mne.fig_settings is not None:
self.mne.fig_settings._update_sensitivity_spinbox_values()

def _fake_click_on_toolbar_action(self, action_name, wait_after=500):
"""Trigger event associated with action 'action_name' in toolbar."""
for action in self.mne.toolbar.actions():
Expand Down
14 changes: 14 additions & 0 deletions mne_qt_browser/tests/test_pg_specific.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,20 @@ def test_pg_settings_dialog(raw_orig, pg_backend):
)
assert inv_norm == ch_spinbox.value()

ch_scale_spinbox = fig.mne.fig_settings.ch_scaling_spinboxes[ch_type_test]
ch_sens_spinbox = fig.mne.fig_settings.ch_sensitivity_spinboxes[ch_type_test]
scaling_spinbox_value = ch_spinbox.value()
sensitivity_spinbox_value = ch_sens_spinbox.value()
scaling_value = fig.mne.scalings[ch_type_test]
new_scaling_spinbox_value = scaling_spinbox_value * 2
new_expected_sensitivity_spinbox_value = sensitivity_spinbox_value * 2
ch_scale_spinbox.setValue(new_scaling_spinbox_value)
new_scaling_value = fig.mne.scalings[ch_type_test]
assert scaling_value != new_scaling_value
np.testing.assert_allclose(
ch_sens_spinbox.value(), new_expected_sensitivity_spinbox_value, atol=0.1
)


def test_pg_help_dialog(raw_orig, pg_backend):
"""Test Settings Dialog toggle on/off for pyqtgraph-backend."""
Expand Down