functional connectivity, pylance, and other improvements

This commit is contained in:
2026-08-22 23:38:13 -07:00
parent 19bd3f1279
commit e37275a1bb
14 changed files with 1455 additions and 841 deletions
+26 -23
View File
@@ -6,19 +6,22 @@ Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
import os
import json
from enum import Enum, auto
from datetime import datetime
from typing import Optional
# External library imports
import numpy as np
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QComboBox, QHBoxLayout, QMessageBox, QFileDialog
from PySide6.QtCore import Qt
from mne import Annotations
from mne.io import read_raw_snirf
from mne_nirs.io import write_raw_snirf
from mne.io import read_raw_snirf #type: ignore
from mne_nirs.io import write_raw_snirf #type: ignore
from src.shared.shareddata import APP_NAME
@@ -29,7 +32,7 @@ class EventUpdateMode(Enum):
class UpdateEventsWindow(QWidget):
def __init__(self, parent=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
def __init__(self, parent: Optional[QWidget]=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
super().__init__(parent, Qt.WindowType.Window)
self.mode = mode
@@ -91,7 +94,7 @@ class UpdateEventsWindow(QWidget):
help_btn_a = QPushButton("?")
help_btn_a.setFixedWidth(25)
help_btn_a.setToolTip(help_text_a)
help_btn_a.clicked.connect(lambda _, text=help_text_a: self.show_help_popup(text))
help_btn_a.clicked.connect(lambda: self.show_help_popup(help_text_a))
file_a_layout.addWidget(help_btn_a)
# Container for label + line_edit + browse button with tooltip
@@ -114,7 +117,7 @@ class UpdateEventsWindow(QWidget):
help_btn_b = QPushButton("?")
help_btn_b.setFixedWidth(25)
help_btn_b.setToolTip(help_text_b)
help_btn_b.clicked.connect(lambda _, text=help_text_b: self.show_help_popup(text))
help_btn_b.clicked.connect(lambda: self.show_help_popup(help_text_b))
file_b_layout.addWidget(help_btn_b)
file_b_container = QWidget()
@@ -136,7 +139,7 @@ class UpdateEventsWindow(QWidget):
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
help_btn_suffix.clicked.connect(lambda: self.show_help_popup(help_text_suffix))
suffix_layout.addWidget(help_btn_suffix)
suffix_container = QWidget()
@@ -157,7 +160,7 @@ class UpdateEventsWindow(QWidget):
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
help_btn_suffix.clicked.connect(lambda: self.show_help_popup(help_text_suffix))
suffix2_layout.addWidget(help_btn_suffix)
suffix2_container = QWidget()
@@ -177,7 +180,7 @@ class UpdateEventsWindow(QWidget):
help_btn_snirf_events = QPushButton("?")
help_btn_snirf_events.setFixedWidth(25)
help_btn_snirf_events.setToolTip(help_text_snirf_events)
help_btn_snirf_events.clicked.connect(lambda _, text=help_text_snirf_events: self.show_help_popup(text))
help_btn_snirf_events.clicked.connect(lambda: self.show_help_popup(help_text_snirf_events))
snirf_events_layout.addWidget(help_btn_snirf_events)
snirf_events_container = QWidget()
@@ -199,13 +202,13 @@ class UpdateEventsWindow(QWidget):
self.setLayout(layout)
def show_help_popup(self, text):
def show_help_popup(self, text: str) -> None:
msg = QMessageBox(self)
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
msg.setText(text)
msg.exec()
def browse_file_a(self):
def browse_file_a(self) -> None:
file_path, _ = QFileDialog.getOpenFileName(self, "Select SNIRF File", "", "SNIRF Files (*.snirf)")
if file_path:
self.line_edit_file_a.setText(file_path)
@@ -235,7 +238,7 @@ class UpdateEventsWindow(QWidget):
self.combo_snirf_events.clear()
self.combo_snirf_events.setEnabled(False)
def browse_file_b(self):
def browse_file_b(self) -> None:
file_path, _ = QFileDialog.getOpenFileName(self, "Select BORIS File", "", "BORIS project Files (*.boris)")
if file_path:
self.line_edit_file_b.setText(file_path)
@@ -288,11 +291,11 @@ class UpdateEventsWindow(QWidget):
self.combo_events.addItems(event_entries)
self.combo_events.setEnabled(bool(event_entries))
def clear_files(self):
def clear_files(self) -> None:
self.line_edit_file_a.clear()
self.line_edit_file_b.clear()
def go_action(self):
def go_action(self) -> None:
file_a = self.line_edit_file_a.text()
suffix = "flare"
@@ -540,7 +543,7 @@ class UpdateEventsWindow(QWidget):
class UpdateEventsBlazesWindow(QWidget):
def __init__(self, parent=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
def __init__(self, parent: Optional[QWidget]=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
super().__init__(parent, Qt.WindowType.Window)
self.mode = mode
@@ -595,7 +598,7 @@ class UpdateEventsBlazesWindow(QWidget):
help_btn_a = QPushButton("?")
help_btn_a.setFixedWidth(25)
help_btn_a.setToolTip(help_text_a)
help_btn_a.clicked.connect(lambda _, text=help_text_a: self.show_help_popup(text))
help_btn_a.clicked.connect(lambda: self.show_help_popup(help_text_a))
file_a_layout.addWidget(help_btn_a)
# Container for label + line_edit + browse button with tooltip
@@ -618,7 +621,7 @@ class UpdateEventsBlazesWindow(QWidget):
help_btn_b = QPushButton("?")
help_btn_b.setFixedWidth(25)
help_btn_b.setToolTip(help_text_b)
help_btn_b.clicked.connect(lambda _, text=help_text_b: self.show_help_popup(text))
help_btn_b.clicked.connect(lambda: self.show_help_popup(help_text_b))
file_b_layout.addWidget(help_btn_b)
file_b_container = QWidget()
@@ -640,7 +643,7 @@ class UpdateEventsBlazesWindow(QWidget):
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
help_btn_suffix.clicked.connect(lambda: self.show_help_popup(help_text_suffix))
suffix2_layout.addWidget(help_btn_suffix)
suffix2_container = QWidget()
@@ -660,7 +663,7 @@ class UpdateEventsBlazesWindow(QWidget):
help_btn_snirf_events = QPushButton("?")
help_btn_snirf_events.setFixedWidth(25)
help_btn_snirf_events.setToolTip(help_text_snirf_events)
help_btn_snirf_events.clicked.connect(lambda _, text=help_text_snirf_events: self.show_help_popup(text))
help_btn_snirf_events.clicked.connect(lambda: self.show_help_popup(help_text_snirf_events))
snirf_events_layout.addWidget(help_btn_snirf_events)
snirf_events_container = QWidget()
@@ -683,13 +686,13 @@ class UpdateEventsBlazesWindow(QWidget):
self.setLayout(layout)
def show_help_popup(self, text):
def show_help_popup(self, text: str) -> None:
msg = QMessageBox(self)
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
msg.setText(text)
msg.exec()
def browse_file_a(self):
def browse_file_a(self) -> None:
file_path, _ = QFileDialog.getOpenFileName(self, "Select SNIRF File", "", "SNIRF Files (*.snirf)")
if file_path:
self.line_edit_file_a.setText(file_path)
@@ -719,7 +722,7 @@ class UpdateEventsBlazesWindow(QWidget):
self.combo_snirf_events.clear()
self.combo_snirf_events.setEnabled(False)
def browse_file_b(self):
def browse_file_b(self) -> None:
file_path, _ = QFileDialog.getOpenFileName(self, "Select JSON Timeline File", "", "JSON Files (*.json)")
if file_path:
self.line_edit_file_b.setText(file_path)
@@ -762,12 +765,12 @@ class UpdateEventsBlazesWindow(QWidget):
return event_strings
def clear_files(self):
def clear_files(self) -> None:
self.line_edit_file_a.clear()
self.line_edit_file_b.clear()
def go_action(self):
def go_action(self) -> None:
file_a = self.line_edit_file_a.text()
file_b = self.line_edit_file_b.text()
suffix = APP_NAME
+33 -18
View File
@@ -1,16 +1,21 @@
"""
Filename: updateoptodes.py
Description: Methods to update optode locations for FLARES
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
import os
from pathlib import Path
from typing import Dict, Optional, Union
# External library imports
import pandas as pd
import numpy as np
import numpy.typing as npt
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QMessageBox, QLineEdit, QPushButton, QFileDialog
from PySide6.QtCore import Qt
@@ -24,7 +29,7 @@ from src.shared.shareddata import APP_NAME
class UpdateOptodesWindow(QWidget):
def __init__(self, parent=None):
def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent, Qt.WindowType.Window)
self.setWindowTitle(f"Update optode positions - {APP_NAME.upper()}")
self.resize(760, 200)
@@ -50,7 +55,6 @@ class UpdateOptodesWindow(QWidget):
self.btn_clear.clicked.connect(self.clear_files)
self.btn_go.clicked.connect(self.go_action)
# ---
layout = QVBoxLayout()
self.description = QLabel()
self.description.setTextFormat(Qt.TextFormat.RichText)
@@ -75,7 +79,7 @@ class UpdateOptodesWindow(QWidget):
help_btn_a = QPushButton("?")
help_btn_a.setFixedWidth(25)
help_btn_a.setToolTip(help_text_a)
help_btn_a.clicked.connect(lambda _, text=help_text_a: self.show_help_popup(text))
help_btn_a.clicked.connect(lambda: self.show_help_popup(help_text_a))
file_a_layout.addWidget(help_btn_a)
# Container for label + line_edit + browse button with tooltip
@@ -98,7 +102,7 @@ class UpdateOptodesWindow(QWidget):
help_btn_b = QPushButton("?")
help_btn_b.setFixedWidth(25)
help_btn_b.setToolTip(help_text_b)
help_btn_b.clicked.connect(lambda _, text=help_text_b: self.show_help_popup(text))
help_btn_b.clicked.connect(lambda: self.show_help_popup(help_text_b))
file_b_layout.addWidget(help_btn_b)
file_b_container = QWidget()
@@ -121,7 +125,7 @@ class UpdateOptodesWindow(QWidget):
help_btn_suffix = QPushButton("?")
help_btn_suffix.setFixedWidth(25)
help_btn_suffix.setToolTip(help_text_suffix)
help_btn_suffix.clicked.connect(lambda _, text=help_text_suffix: self.show_help_popup(text))
help_btn_suffix.clicked.connect(lambda: self.show_help_popup(help_text_suffix))
suffix_layout.addWidget(help_btn_suffix)
suffix_container = QWidget()
@@ -143,13 +147,13 @@ class UpdateOptodesWindow(QWidget):
self.setLayout(layout)
def show_help_popup(self, text):
def show_help_popup(self, text: str) -> None:
msg = QMessageBox(self)
msg.setWindowTitle(f"Parameter Info - {APP_NAME.upper()}")
msg.setText(text)
msg.exec()
def handle_link_click(self, link):
def handle_link_click(self, link: str) -> None:
if link == "custom_link":
msg = QMessageBox(self)
msg.setWindowTitle("Example Digitization File")
@@ -166,21 +170,21 @@ class UpdateOptodesWindow(QWidget):
msg.setText(text)
msg.exec()
def browse_file_a(self):
def browse_file_a(self) -> None:
file_path, _ = QFileDialog.getOpenFileName(self, "Select SNIRF File", "", "SNIRF Files (*.snirf)")
if file_path:
self.line_edit_file_a.setText(file_path)
def browse_file_b(self):
def browse_file_b(self) -> None:
file_path, _ = QFileDialog.getOpenFileName(self, "Select File", "", "Supported Files (*.txt *.xlsx)")
if file_path:
self.line_edit_file_b.setText(file_path)
def clear_files(self):
def clear_files(self) -> None:
self.line_edit_file_a.clear()
self.line_edit_file_b.clear()
def go_action(self):
def go_action(self) -> None:
file_a = self.line_edit_file_a.text()
file_b = self.line_edit_file_b.text()
suffix = self.line_edit_suffix.text().strip() or "flare"
@@ -220,7 +224,12 @@ class UpdateOptodesWindow(QWidget):
QMessageBox.information(self, "File Saved", f"File was saved to:\n{save_path}")
def update_optode_positions(self, file_a, file_b, save_path):
def update_optode_positions(
self,
file_a: Union[str, Path],
file_b: Union[str, Path],
save_path: Union[str, Path]
) -> None:
fiducials = {}
ch_positions = {}
@@ -247,16 +256,22 @@ class UpdateOptodesWindow(QWidget):
elif extension == '.xlsx':
# TODO: Bad! Why assume sheet1 has the contents?
df = pd.read_excel(file_b, sheet_name='Sheet1')
df = pd.read_excel(file_b, sheet_name='Sheet1') # type: ignore
def _get_block_data(df, block_id, row_mapping, scale=0.001):
def _get_block_data(
target_df: pd.DataFrame,
block_id: int,
row_mapping: Union[Dict[int, str], str],
scale: float = 0.001
) -> Dict[str, npt.NDArray[np.float64]]:
"""Isolates a block, cleans numeric data, and returns a scaled dictionary."""
# 1. Isolate and clean
block = df[df['block_id'] == block_id].iloc[:, [1, 2, 3]].copy()
block = target_df[target_df['block_id'] == block_id].iloc[:, [1, 2, 3]].copy()
block = block.apply(pd.to_numeric, errors='coerce')
# 2. Extract into dictionary based on mapping
result = {}
result: Dict[str, npt.NDArray[np.float64]] = {}
# If row_mapping is a dict (like {0: 'nz'}), use it directly
if isinstance(row_mapping, dict):
@@ -265,7 +280,7 @@ class UpdateOptodesWindow(QWidget):
result[key] = block.iloc[row_idx].to_numpy(dtype=float) * scale
# If row_mapping is a string prefix (like 'D' or 'S'), auto-generate keys
elif isinstance(row_mapping, str):
else:
for i in range(len(block)):
result[f"{row_mapping}{i+1}"] = block.iloc[i].to_numpy(dtype=float) * scale
@@ -292,5 +307,5 @@ class UpdateOptodesWindow(QWidget):
# Read the SNIRF file, set the montage, and write it back
raw = read_raw_snirf(file_a, preload=True)
raw.set_montage(initial_montage)
raw.set_montage(initial_montage) # type: ignore
write_raw_snirf(raw, save_path)
+53 -8
View File
@@ -1,15 +1,25 @@
"""
Filename: viewerlauncher.py
Description: Viewer launcher window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from pathlib import Path
from typing import Any, Callable, Type
# External library imports
from pandas import DataFrame
from PySide6.QtWidgets import QPushButton, QWidget, QVBoxLayout
from PySide6.QtCore import QTimer
from mne import Epochs
from mne.io.base import BaseRaw
from src.analysis.exporttocsv import ExportToCSVWidget
from src.analysis.intragroupbrainimage import IntraGroupBrainImageWidget
from src.analysis.intergroupbrainimage import InterGroupBrainImageWidget
@@ -24,18 +34,31 @@ from src.shared.shareddata import APP_NAME
class ViewerLauncherWidget(QWidget):
def __init__(self, haemo_dict, epochs_dict, cha_dict, df_ind_dict, design_matrix_dict, config_dict, fig_bytes_dict, contrast_results_dict, roi_channel_map_dict, folding_bypass):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
epochs_dict: dict[str, Epochs],
cha_dict: dict[str, DataFrame],
df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame],
config_dict: dict[str, dict[str, Any]],
fig_bytes_dict: dict[str, dict[str, bytes]],
contrast_results_dict: dict[str, dict[str, Any]],
roi_channel_map_dict: dict[str, dict[str, str]],
folding_bypass: bool,
) -> None:
super().__init__()
self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}")
group_dict = {f: c.get("GROUP", "Unknown") for f, c in config_dict.items()}
btn_data = [
btn_data: list[tuple[str, Type[QWidget], list[Any], bool]] = [
("Participant Image Viewer", ParticipantImageViewerWidget, [haemo_dict, fig_bytes_dict], True),
("Participant Brain Viewer", ParticipantBrainViewerWidget, [haemo_dict, cha_dict], True),
("Participant Fold Channels Viewer", ParticipantFoldChannelsWidget, [haemo_dict, cha_dict], False),
("Participant Functional Connectivity Viewer [BETA]", ParticipantFunctionalConnectivityWidget, [haemo_dict, epochs_dict], True),
("Intra-Group Functional Connectivity Viewer [BETA]", IntraGroupFunctionalConnectivityWidget, [haemo_dict, group_dict, config_dict], True),
("Intra-Group Functional Connectivity Viewer [BETA]", IntraGroupFunctionalConnectivityWidget, [haemo_dict, epochs_dict, group_dict], True),
("Intra-Group Stats Viewer", IntraGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, roi_channel_map_dict, group_dict], True),
("Inter-Group Stats Viewer", InterGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, roi_channel_map_dict, group_dict], True),
("Intra-Group Brain and Image Viewer", IntraGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
@@ -47,21 +70,43 @@ class ViewerLauncherWidget(QWidget):
for label, widget_class, args, requires_bypass in btn_data:
btn = QPushButton(f"Open {label}")
# Connect directly to the generic opener
btn.clicked.connect(lambda _, c=widget_class, b=btn, a=args: self._open_viewer(c, b, *a))
btn.clicked.connect(self._make_viewer_callback(widget_class, btn, args))
btn.setEnabled(not (requires_bypass and folding_bypass))
layout.addWidget(btn)
def _open_viewer(self, widget_class, btn, *args):
def _make_viewer_callback(
self,
widget_class: Type[QWidget],
btn: QPushButton,
args: list[Any],
) -> Callable[[bool], None]:
def callback(_checked: bool = False) -> None:
self._open_viewer(widget_class, btn, *args)
return callback
def _open_viewer(
self,
widget_class: Type[QWidget],
btn: QPushButton,
*args: Any
) -> None:
# Instantiate and show dynamically
self.active_viewer = widget_class(*args)
self.active_viewer.show()
self._trigger_success(btn)
def _launch(self, func, btn, *args):
def _launch(
self,
func: Callable[..., Any],
btn: QPushButton,
*args: Any
) -> None:
func(*args)
self._trigger_success(btn)
def _trigger_success(self, button):
def _trigger_success(self, button: QPushButton) -> None:
"""Temporarily adds a green checkmark to the button text."""
original_text = button.text()
button.setText(f"{original_text} ✔")
@@ -70,6 +115,6 @@ class ViewerLauncherWidget(QWidget):
# Revert after 1 second
QTimer.singleShot(1000, lambda: self._revert_button(button, original_text))
def _revert_button(self, button, original_text):
def _revert_button(self, button: QPushButton, original_text: str) -> None:
button.setText(original_text)
button.setStyleSheet("")