functional connectivity, pylance, and other improvements
This commit is contained in:
@@ -6,11 +6,16 @@ Author: Tyler de Zeeuw
|
||||
License: GPL-3.0
|
||||
"""
|
||||
|
||||
# Built-in Imports
|
||||
import os
|
||||
from pathlib import Path
|
||||
import time
|
||||
import traceback
|
||||
from multiprocessing import Process, current_process, Manager
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
# External library imports
|
||||
from matplotlib.backend_bases import Event
|
||||
import numpy as np
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
@@ -18,25 +23,27 @@ import matplotlib.image as mpimg
|
||||
from matplotlib.figure import Figure
|
||||
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||
|
||||
from PySide6.QtWidgets import QFrame, QGridLayout, QHBoxLayout, QLabel, QProgressBar, QPushButton, QScrollArea, QSizePolicy, QWidget, QDialog, QVBoxLayout
|
||||
from PySide6.QtCore import QThread, Qt, QSize, QTimer
|
||||
from PySide6.QtGui import QPixmap, QImage
|
||||
from PySide6.QtWidgets import QFrame, QGridLayout, QHBoxLayout, QLabel, QLayout, QProgressBar, QPushButton, QScrollArea, QSizePolicy, QWidget, QDialog, QVBoxLayout
|
||||
from PySide6.QtCore import QThread, Qt, QSize, QTimer, QObject, Signal
|
||||
from PySide6.QtGui import QCloseEvent, QMouseEvent, QPixmap, QImage
|
||||
from pandas import DataFrame
|
||||
|
||||
from mne.io.base import BaseRaw
|
||||
from src.shared.flaresbasewidget import FlaresBaseWidget
|
||||
from src.shared.shareddata import APP_NAME, resource_path
|
||||
|
||||
|
||||
class MultiProgressDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, parent: Optional[QWidget] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("fOLD Analysis Progress")
|
||||
self.setFixedWidth(400)
|
||||
self.setWindowModality(Qt.WindowModality.NonModal)
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.bars = {}
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.bars: Dict[str, QProgressBar] = {}
|
||||
self.allow_closing = False
|
||||
|
||||
def add_participant(self, label, total_steps):
|
||||
def add_participant(self, label: Any, total_steps: Union[int, float, str]) -> None:
|
||||
clean_key = str(label).strip()
|
||||
label_widget = QLabel(f"Analyzing {clean_key}...")
|
||||
pbar = QProgressBar()
|
||||
@@ -44,16 +51,17 @@ class MultiProgressDialog(QDialog):
|
||||
pbar.setMaximum(int(total_steps)) # Ensure this is a strict integer
|
||||
pbar.setValue(0)
|
||||
|
||||
self.layout.addWidget(label_widget)
|
||||
self.layout.addWidget(pbar)
|
||||
self.bars[label] = pbar
|
||||
self.main_layout.addWidget(label_widget)
|
||||
self.main_layout.addWidget(pbar)
|
||||
self.bars[clean_key] = pbar
|
||||
|
||||
def update_bar(self, label, value):
|
||||
if label in self.bars:
|
||||
def update_bar(self, label: Any, value: Union[int, float, str]) -> None:
|
||||
clean_key = str(label).strip()
|
||||
if clean_key in self.bars:
|
||||
# Force integers to prevent QProgressBar from breaking or flickering
|
||||
self.bars[label].setValue(int(value))
|
||||
self.bars[clean_key].setValue(int(value))
|
||||
|
||||
def closeEvent(self, event):
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
if self.allow_closing:
|
||||
event.accept()
|
||||
else:
|
||||
@@ -64,8 +72,13 @@ class MultiProgressDialog(QDialog):
|
||||
self.close()
|
||||
|
||||
|
||||
def single_participant_worker(
|
||||
file_path: str,
|
||||
raw_data: Any,
|
||||
result_queue: Any,
|
||||
progress_queue: Any,
|
||||
) -> None:
|
||||
|
||||
def single_participant_worker(file_path, raw_data, result_queue, progress_queue):
|
||||
""" Runs inside its own dedicated process """
|
||||
p_name = os.path.basename(file_path)
|
||||
try:
|
||||
@@ -81,8 +94,7 @@ def single_participant_worker(file_path, raw_data, result_queue, progress_queue)
|
||||
progress_queue.put(f"ERROR: {p_name} - {str(e)}")
|
||||
|
||||
|
||||
|
||||
def get_landmark_color_map():
|
||||
def get_landmark_color_map() -> Dict[str, Tuple[float, float, float, float]]:
|
||||
"""Generates the unified 40-color map for fOLD landmarks."""
|
||||
landmarks = [
|
||||
"1 - Primary Somatosensory Cortex", "2 - Primary Somatosensory Cortex",
|
||||
@@ -116,7 +128,15 @@ class StaticChannelCanvas(FigureCanvas):
|
||||
"""The Pop-up Window Canvas.
|
||||
Renders the interactive pie chart on the left, and a matching PNG image on the right.
|
||||
"""
|
||||
def __init__(self, channel_name, data_list, color_map, image_path=None, parent=None):
|
||||
def __init__(
|
||||
self,
|
||||
channel_name: str,
|
||||
data_list: List[Dict[str, Any]],
|
||||
color_map: Dict[str, Union[str, Tuple[float, float, float, float]]],
|
||||
image_path: Optional[str] = None,
|
||||
parent: Optional[QWidget] = None,
|
||||
) -> None:
|
||||
|
||||
self.fig = Figure(figsize=(11.0, 5.5))
|
||||
|
||||
self.ax = self.fig.subplots(1, 2)
|
||||
@@ -194,7 +214,7 @@ class StaticChannelCanvas(FigureCanvas):
|
||||
|
||||
self.mpl_connect('motion_notify_event', self._on_hover)
|
||||
|
||||
def _on_hover(self, event):
|
||||
def _on_hover(self, event: Event) -> None:
|
||||
try:
|
||||
# FIX: Only track mouse events when hovering over the LEFT axis frame containing the pie chart
|
||||
if event.inaxes != self.ax[0]:
|
||||
@@ -231,10 +251,10 @@ class StaticChannelCanvas(FigureCanvas):
|
||||
self.draw_idle()
|
||||
|
||||
except Exception as err:
|
||||
print("[ERROR] Internal failure inside _on_hover loop:")
|
||||
print(f"[ERROR] Internal failure inside _on_hover loop: {err}")
|
||||
traceback.print_exc()
|
||||
|
||||
def _explode_wedge(self, index_to_expand):
|
||||
def _explode_wedge(self, index_to_expand: int) -> None:
|
||||
changed = False
|
||||
for idx, wedge in enumerate(self.wedges):
|
||||
if idx == index_to_expand:
|
||||
@@ -252,7 +272,7 @@ class StaticChannelCanvas(FigureCanvas):
|
||||
if changed:
|
||||
self.draw_idle()
|
||||
|
||||
def _reset_wedges(self):
|
||||
def _reset_wedges(self) -> None:
|
||||
changed = False
|
||||
for wedge in self.wedges:
|
||||
if wedge.center != (0.0, 0.0):
|
||||
@@ -274,7 +294,7 @@ class StandaloneLegendDialog(QWidget):
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
|
||||
# Reuse your exact card creation method to render inside the popup window
|
||||
legend_card = canvas_engine.create_legend_card(title_prefix, self)
|
||||
legend_card = canvas_engine.create_legend_card(title_prefix)
|
||||
layout.addWidget(legend_card)
|
||||
|
||||
|
||||
@@ -381,7 +401,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
self.mpl_connect('button_press_event', self._on_canvas_click)
|
||||
|
||||
|
||||
def create_matrix_card(self, title_prefix, layout_to_attach_to):
|
||||
def create_matrix_card(self, title_prefix: str, layout_to_attach_to: QLayout) -> QFrame:
|
||||
"""Wraps the channel matrix layout inside a responsive, matching hover-stylized card frame."""
|
||||
# 1. Create matching styled container card frame
|
||||
card_frame = QFrame()
|
||||
@@ -422,7 +442,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
layout_to_attach_to.addWidget(card_frame)
|
||||
return card_frame
|
||||
|
||||
def _on_canvas_click(self, event):
|
||||
def _on_canvas_click(self, event: Any) -> None:
|
||||
# CASE 1: Whitespace Clicked -> Open full 25-matrix in fullscreen window
|
||||
if event.inaxes is None:
|
||||
self._open_fullscreen_grid()
|
||||
@@ -473,7 +493,8 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
self._fullscreen_refs = [w for w in self._fullscreen_refs if w.isVisible()]
|
||||
self._fullscreen_refs.append(fullscreen_window)
|
||||
|
||||
def _calculate_total_brodmann_profile(self, channels_data):
|
||||
|
||||
def _calculate_total_brodmann_profile(self, channels_data: Dict[str, Any]):
|
||||
"""Sums and normalizes the specificity profile across all channels."""
|
||||
totals = {}
|
||||
num_channels = len(channels_data)
|
||||
@@ -553,7 +574,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
self._open_popups.append(popup)
|
||||
|
||||
|
||||
def create_total_summary_card(self, title_prefix, layout_to_attach_to):
|
||||
def create_total_summary_card(self, title_prefix: str, layout_to_attach_to: QLayout) -> QFrame:
|
||||
"""Generates a highly compact, clickable embedded card on the main window showing aggregated data."""
|
||||
# 1. Calculate the normalized profile data payload using the instance's own data
|
||||
summary_data = self._calculate_total_brodmann_profile(self.channels_data)
|
||||
@@ -609,7 +630,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
card_layout.addWidget(summary_canvas)
|
||||
card_layout.addStretch(0)
|
||||
|
||||
def handle_card_click(event):
|
||||
def handle_card_click(event: QMouseEvent) -> None:
|
||||
# Only trigger expansion if it's a primary left-click action
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self._open_expanded_summary_window(title_prefix, summary_data)
|
||||
@@ -626,7 +647,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
|
||||
|
||||
|
||||
def create_legend_card(self, title_prefix, layout_to_attach_to):
|
||||
def create_legend_card(self, title_prefix: str) -> QFrame:
|
||||
card = QFrame()
|
||||
card.setStyleSheet("QFrame { background-color: #ffffff; border-radius: 8px; border: 1px solid #e9ecef; }")
|
||||
|
||||
@@ -686,7 +707,7 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
return card
|
||||
|
||||
|
||||
def _open_expanded_summary_window(self, title_prefix, summary_data):
|
||||
def _open_expanded_summary_window(self, title_prefix: str, summary_data: List[Any]) -> None:
|
||||
"""Pops open a beautifully scaled, independent large window when the card is clicked."""
|
||||
popup = QWidget(None)
|
||||
popup.setWindowTitle(f"Grand Total Profile Details - {title_prefix}")
|
||||
@@ -719,16 +740,18 @@ class InteractiveParticipantGridCanvas(FigureCanvas):
|
||||
self._summary_popups.append(popup)
|
||||
|
||||
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from multiprocessing import Manager, Process
|
||||
|
||||
class ProcessOrchestrator(QObject):
|
||||
# Fires when Manager + Processes are completely ready
|
||||
# Emits: (manager_instance, result_queue, progress_queue, active_processes_list)
|
||||
setup_finished = Signal(object, object, object, list)
|
||||
setup_failed = Signal(str)
|
||||
|
||||
def __init__(self, selected_files, haemo_dict, worker_func):
|
||||
def __init__(self,
|
||||
selected_files,
|
||||
haemo_dict: dict[str | Path, BaseRaw],
|
||||
worker_func
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
self.selected_files = selected_files
|
||||
self.haemo_dict = haemo_dict
|
||||
@@ -758,7 +781,12 @@ class ProcessOrchestrator(QObject):
|
||||
|
||||
|
||||
class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
def __init__(self, haemo_dict, cha_dict):
|
||||
def __init__(
|
||||
self,
|
||||
haemo_dict: dict[str | Path, BaseRaw],
|
||||
cha_dict: dict[str, DataFrame]
|
||||
) -> None:
|
||||
|
||||
super().__init__("ParticipantFoldChannels")
|
||||
self.setWindowTitle(f"Participant Fold Channels Viewer - {APP_NAME.upper()}")
|
||||
self.haemo_dict = haemo_dict
|
||||
@@ -773,9 +801,9 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
self.participant_map[file_path] = short_label
|
||||
self.participant_dropdown_items.append(display_label)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.main_layout = QVBoxLayout(self)
|
||||
self.top_bar = QHBoxLayout()
|
||||
self.layout.addLayout(self.top_bar)
|
||||
self.main_layout.addLayout(self.top_bar)
|
||||
|
||||
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
|
||||
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
|
||||
@@ -829,7 +857,7 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
self.scroll_area.setWidget(self.scroll_content_widget)
|
||||
|
||||
# Add the self.scroll_area widget to your root layout view frame panel
|
||||
self.layout.addWidget(self.scroll_area)
|
||||
self.main_layout.addWidget(self.scroll_area)
|
||||
|
||||
self.thumb_size = QSize(280, 180)
|
||||
self.showMaximized()
|
||||
@@ -889,7 +917,14 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
self.orchestrator_thread.start()
|
||||
print(f"After 4: {datetime.now()}")
|
||||
|
||||
def on_orchestration_success(self, manager, result_queue, progress_queue, active_processes):
|
||||
def on_orchestration_success(
|
||||
self,
|
||||
manager: Any,
|
||||
result_queue: Any,
|
||||
progress_queue: Any,
|
||||
active_processes: List[Any]
|
||||
) -> None:
|
||||
|
||||
""" Executed on the Main GUI Thread once background process setup finishes """
|
||||
self.manager = manager
|
||||
self.result_queue = result_queue
|
||||
@@ -902,15 +937,15 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
self.result_timer.timeout.connect(self.check_parallel_results)
|
||||
self.result_timer.start()
|
||||
|
||||
def on_orchestration_failed(self, error_msg):
|
||||
|
||||
def on_orchestration_failed(self, error_msg: str) -> None:
|
||||
""" Fallback handler if Windows permissions or pickling fails in background """
|
||||
if hasattr(self, 'multi_progress'):
|
||||
self.multi_progress.close()
|
||||
print(f"[CRITICAL FAILURE] Background Orchestration Failed:\n{error_msg}")
|
||||
|
||||
|
||||
|
||||
def check_parallel_results(self):
|
||||
def check_parallel_results(self) -> None:
|
||||
# Check for progress/completion signals
|
||||
|
||||
while not self.progress_queue.empty():
|
||||
@@ -991,8 +1026,7 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
|
||||
legend_title = "Grand Total Brodmann Mapping Profile"
|
||||
legend_card = global_canvas.create_legend_card(
|
||||
title_prefix=legend_title,
|
||||
layout_to_attach_to=self.scroll_content_widget.layout()
|
||||
title_prefix=legend_title
|
||||
)
|
||||
|
||||
def handle_legend_click(event):
|
||||
@@ -1006,52 +1040,8 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
col = count % 3
|
||||
self.grid_layout.addWidget(legend_card, row, col)
|
||||
|
||||
|
||||
|
||||
|
||||
# def add_images_to_grid(self, result_dict):
|
||||
# """
|
||||
# result_dict format: { file_path: {"main": bytes, "legend": bytes} }
|
||||
# """
|
||||
# for file_path, images in result_dict.items():
|
||||
|
||||
# if self.grid_layout.count() == 0 and "legend" in images:
|
||||
# self._add_legend_to_grid(images["legend"])
|
||||
|
||||
# # Create a container for this participant's results
|
||||
# container = QFrame()
|
||||
# container.setFrameShape(QFrame.StyledPanel)
|
||||
# vbox = QVBoxLayout(container)
|
||||
|
||||
# participant_label = self.participant_map.get(file_path, os.path.basename(file_path))
|
||||
# title = QLabel(f"<b>{participant_label}</b>")
|
||||
# title.setAlignment(Qt.AlignCenter)
|
||||
# vbox.addWidget(title)
|
||||
|
||||
# # We primarily want to show the 'main' plot in the grid
|
||||
# if "main" in images:
|
||||
# pixmap = self._bytes_to_pixmap(images["main"])
|
||||
# img_label = QLabel()
|
||||
# # Scale it to fit the thumbnail size defined in __init__
|
||||
# img_label.setPixmap(pixmap.scaled(
|
||||
# self.thumb_size,
|
||||
# Qt.KeepAspectRatio,
|
||||
# Qt.SmoothTransformation
|
||||
# ))
|
||||
# img_label.setAlignment(Qt.AlignCenter)
|
||||
|
||||
# # Optional: Click to open full size
|
||||
# img_label.mousePressEvent = lambda e, p=pixmap, t=participant_label: self._open_full_size(p, t)
|
||||
|
||||
# vbox.addWidget(img_label)
|
||||
|
||||
# # Determine grid position (row-major order)
|
||||
# count = self.grid_layout.count()
|
||||
# row = count // 3 # 3 columns wide
|
||||
# col = count % 3
|
||||
# self.grid_layout.addWidget(container, row, col)
|
||||
|
||||
def add_images_to_grid(self, result_dict):
|
||||
def add_images_to_grid(self, result_dict: Dict[str, Dict[str, Any]]) -> None:
|
||||
color_map = get_landmark_color_map()
|
||||
|
||||
for file_path, channels_data in result_dict.items():
|
||||
@@ -1091,12 +1081,12 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
self.grid_layout.addWidget(summary_card, row, col)
|
||||
|
||||
|
||||
def _bytes_to_pixmap(self, png_bytes):
|
||||
def _bytes_to_pixmap(self, png_bytes: bytes) -> QPixmap:
|
||||
"""Converts raw bytes from the multiprocess queue to a QPixmap."""
|
||||
image = QImage.fromData(png_bytes)
|
||||
return QPixmap.fromImage(image)
|
||||
|
||||
def _open_full_size(self, pixmap, title):
|
||||
def _open_full_size(self, pixmap: QPixmap, title: str) -> None:
|
||||
"""Simple popup to view the image at a readable scale."""
|
||||
view = QDialog(self)
|
||||
view.setWindowTitle(f"Full View - {title}")
|
||||
@@ -1106,7 +1096,7 @@ class ParticipantFoldChannelsWidget(FlaresBaseWidget):
|
||||
layout.addWidget(label)
|
||||
view.show()
|
||||
|
||||
def _add_legend_to_grid(self, legend_bytes):
|
||||
def _add_legend_to_grid(self, legend_bytes: bytes) -> None:
|
||||
"""Helper to put the legend in the first slot."""
|
||||
container = QFrame()
|
||||
container.setStyleSheet("background-color: #f9f9f9; border: 1px solid #ccc;")
|
||||
|
||||
Reference in New Issue
Block a user