small rewrite to impose dry principles

This commit is contained in:
2026-07-15 21:21:35 -07:00
parent ffa14693b3
commit 12afc5d3bc
14 changed files with 1825 additions and 1716 deletions
+599 -738
View File
File diff suppressed because it is too large Load Diff
+61 -70
View File
@@ -268,6 +268,12 @@ SECTIONS = [
{"name": "N_JOBS", "default": 1, "type": int, "help": "The number of CPUs to use to do the GLM computation. -1 means 'all CPUs'."}, {"name": "N_JOBS", "default": 1, "type": int, "help": "The number of CPUs to use to do the GLM computation. -1 means 'all CPUs'."},
] ]
}, },
{
"title": "Region of Interest",
"params": [
{"name": "JSON_LOCATION", "default": "", "type": str, "help": "Location of the JSON file containing region of interest results for significance calculations."},
]
},
{ {
"title": "Finishing Touches", "title": "Finishing Touches",
"params": [ "params": [
@@ -287,6 +293,19 @@ SECTIONS = [
DATA_SCHEMA = [
{"key": "raw_haemo_dict", "help": "Dict[file_path, MNE RawArray]: Haemodynamic raw data"},
{"key": "epochs_dict", "help": "Dict[file_path, MNE Epochs]: Time-locked epoch data"},
{"key": "cha_dict", "help": "Dict[file_path, DataFrame]: Channel analysis results"},
{"key": "df_ind_dict", "help": "Dict[file_path, DataFrame]: Individual-level data/ROI results"},
{"key": "design_matrix_dict", "help": "Dict[file_path, DataFrame]: GLM design matrices"},
{"key": "config_dict", "help": "Dict[file_path, dict]: Processing configuration parameters"},
{"key": "fig_bytes_dict", "help": "Dict[file_path, dict]: Serialized figure data"},
{"key": "contrast_results_dict", "help": "Dict[file_path, dict]: Calculated contrast statistical results"},
{"key": "valid_dict", "help": "Dict[file_path, bool]: Boolean validity status per file"}
]
@@ -488,15 +507,9 @@ class MainApplication(QMainWindow):
# Initialization to ensure that saving can occur # Initialization to ensure that saving can occur
self.raw_haemo_dict = {} # Processed Hemodynamic data for item in DATA_SCHEMA:
self.config_dict = {} # Analysis parameters/settings setattr(self, item["key"], {})
self.epochs_dict = {} # Timing/Event data
self.cha_dict = {} # Channel configurations
self.contrast_results_dict = {} # Statistical results
self.df_ind_dict = {} # Individual dataframes
self.design_matrix_dict = {} # GLM Design matrices
self.valid_dict = {} # Quality control/Validity flags
self.fig_bytes_dict = {} # Cached plot images (serialized)
self.file_metadata = {} # AGE, GENDER, GROUP self.file_metadata = {} # AGE, GENDER, GROUP
self.metadata_cache = {} # Internal file/path information metadata cache self.metadata_cache = {} # Internal file/path information metadata cache
self.bubble_widgets = {} # References to the UI "Bubble" objects self.bubble_widgets = {} # References to the UI "Bubble" objects
@@ -878,15 +891,8 @@ class MainApplication(QMainWindow):
self.files_done = set() self.files_done = set()
self.files_failed = set() self.files_failed = set()
self.raw_haemo_dict = {} for item in DATA_SCHEMA:
self.config_dict = {} setattr(self, item["key"], {})
self.epochs_dict = {}
self.fig_bytes_dict = {}
self.cha_dict = {}
self.contrast_results_dict = {}
self.df_ind_dict = {}
self.design_matrix_dict = {}
self.valid_dict = {}
self.metadata_cache = {} self.metadata_cache = {}
@@ -1045,7 +1051,22 @@ class MainApplication(QMainWindow):
def open_launcher_window(self): def open_launcher_window(self):
self.launcher_window = ViewerLauncherWidget(self.raw_haemo_dict, self.config_dict, self.fig_bytes_dict, self.cha_dict, self.contrast_results_dict, self.df_ind_dict, self.design_matrix_dict, self.epochs_dict, self.folding_bypass) data_map = {item["key"]: getattr(self, item["key"]) for item in DATA_SCHEMA}
# 2. Extract values in the specific order the widget constructor expects
args = [
data_map["raw_haemo_dict"],
data_map["epochs_dict"],
data_map["cha_dict"],
data_map["df_ind_dict"],
data_map["design_matrix_dict"],
data_map["config_dict"],
data_map["fig_bytes_dict"],
data_map["contrast_results_dict"],
self.folding_bypass
]
self.launcher_window = ViewerLauncherWidget(*args)
self.launcher_window.show() self.launcher_window.show()
def copy_text(self): def copy_text(self):
@@ -1188,7 +1209,7 @@ class MainApplication(QMainWindow):
def open_folder_dialog(self): def open_folder_dialog(self):
folder_path = QFileDialog.getExistingDirectory(self, "Select Folder", "") folder_path = QFileDialog.getExistingDirectory(self, "Select Folder", "")
if folder_path: if folder_path:
snirf_files = [os.path.normpath(str(f)) for f in Path(folder_path).glob("*.snirf")] snirf_files = [os.path.normpath(str(f)) for f in Path(folder_path).rglob("*.snirf")]
self._load_files_into_pipeline(snirf_files) self._load_files_into_pipeline(snirf_files)
@@ -1297,7 +1318,10 @@ class MainApplication(QMainWindow):
has_param_changes = any(section.has_any_changes() for section in self.param_sections) has_param_changes = any(section.has_any_changes() for section in self.param_sections)
# Check if there is processed data # Check if there is processed data
has_processed_data = bool(getattr(self, 'raw_haemo_dict', None)) has_processed_data = any(
len(getattr(self, item["key"], {})) > 0
for item in DATA_SCHEMA
)
if not (has_processed_data or has_metadata or has_param_changes): if not (has_processed_data or has_metadata or has_param_changes):
if not onCrash: # Don't show popups during a crash/autosave if not onCrash: # Don't show popups during a crash/autosave
@@ -1368,23 +1392,17 @@ class MainApplication(QMainWindow):
current_params = self.config_dict[first_file] current_params = self.config_dict[first_file]
version = CURRENT_VERSION version = CURRENT_VERSION
project_data = {
project_data = {item["key"]: getattr(self, item["key"]) for item in DATA_SCHEMA}
project_data.update({
"version": version, "version": version,
"file_list": file_list, "file_list": file_list,
"progress_states": progress_states, "progress_states": progress_states,
"raw_haemo_dict": self.raw_haemo_dict,
"file_metadata": rel_metadata, "file_metadata": rel_metadata,
"file_parameters": rel_file_params, "file_parameters": rel_file_params,
"config_dict": self.config_dict,
"epochs_dict": self.epochs_dict,
"fig_bytes_dict": self.fig_bytes_dict,
"cha_dict": self.cha_dict,
"current_ui_params": current_params, "current_ui_params": current_params,
"contrast_results_dict": self.contrast_results_dict, })
"df_ind_dict": self.df_ind_dict,
"design_matrix_dict": self.design_matrix_dict,
"valid_dict": self.valid_dict,
}
def sanitize(obj): def sanitize(obj):
if isinstance(obj, Path): if isinstance(obj, Path):
@@ -1472,15 +1490,9 @@ class MainApplication(QMainWindow):
return return
self.raw_haemo_dict = data.get("raw_haemo_dict", {}) for item in DATA_SCHEMA:
self.config_dict = data.get("config_dict", {}) key = item["key"]
self.epochs_dict = data.get("epochs_dict", {}) setattr(self, key, data.get(key, {}))
self.fig_bytes_dict = data.get("fig_bytes_dict", {})
self.cha_dict = data.get("cha_dict", {})
self.contrast_results_dict = data.get("contrast_results_dict", {})
self.df_ind_dict = data.get("df_ind_dict", {})
self.design_matrix_dict = data.get("design_matrix_dict", {})
self.valid_dict = data.get("valid_dict", {})
project_dir = Path(filename).parent project_dir = Path(filename).parent
@@ -1530,7 +1542,7 @@ class MainApplication(QMainWindow):
first_file = next(iter(self.config_dict.keys())) first_file = next(iter(self.config_dict.keys()))
self.restore_sections_from_config(self.config_dict[first_file]) self.restore_sections_from_config(self.config_dict[first_file])
has_data = bool(self.raw_haemo_dict) has_data = any(len(getattr(self, item["key"], {})) > 0 for item in DATA_SCHEMA)
self.button1.setVisible(not has_data) self.button1.setVisible(not has_data)
self.button3.setVisible(has_data) self.button3.setVisible(has_data)
@@ -1955,15 +1967,8 @@ class MainApplication(QMainWindow):
self.button3.setVisible(False) self.button3.setVisible(False)
self.raw_haemo_dict = {} for item in DATA_SCHEMA:
self.config_dict = {} setattr(self, item["key"], {})
self.epochs_dict = {}
self.fig_bytes_dict = {}
self.cha_dict = {}
self.contrast_results_dict = {}
self.df_ind_dict = {}
self.design_matrix_dict = {}
self.valid_dict = {}
self.button1.clicked.disconnect(self.on_run_task) self.button1.clicked.disconnect(self.on_run_task)
self.button1.setText("Cancel") self.button1.setText("Cancel")
@@ -2091,27 +2096,13 @@ class MainApplication(QMainWindow):
# print(f"[DEBUG] Progress: {len(self.files_done)} / {self.files_total}") # print(f"[DEBUG] Progress: {len(self.files_done)} / {self.files_total}")
if msg.get("success"): if msg.get("success"):
# Unpack the massive tuple
raw_haemo, config, epochs, fig_bytes, cha, contrast, df_ind, design, valid = msg["result"]
# Initialize dictionaries once if needed results = msg["result"]
if not hasattr(self, 'raw_haemo_dict') or self.raw_haemo_dict is None: self.files_results[file_path] = results
attrs = ['raw_haemo_dict', 'config_dict', 'epochs_dict', 'fig_bytes_dict',
'cha_dict', 'contrast_results_dict', 'df_ind_dict',
'design_matrix_dict', 'valid_dict']
for attr in attrs:
setattr(self, attr, {})
self.files_results[file_path] = msg["result"] # Simple, clean assignment
self.raw_haemo_dict[file_path] = raw_haemo for item, value in zip(DATA_SCHEMA, results):
self.config_dict[file_path] = config getattr(self, item["key"])[file_path] = value
self.epochs_dict[file_path] = epochs
self.fig_bytes_dict[file_path] = fig_bytes
self.cha_dict[file_path] = cha
self.contrast_results_dict[file_path] = contrast
self.df_ind_dict[file_path] = df_ind
self.design_matrix_dict[file_path] = design
self.valid_dict[file_path] = valid
self.statusbar.showMessage(f"Processed: {os.path.basename(file_path)}") self.statusbar.showMessage(f"Processed: {os.path.basename(file_path)}")
+137
View File
@@ -0,0 +1,137 @@
"""
Filename: crossgroupbrainimage.py
Description: Logic for the Cross-Group Brain & Image analysis window
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# External library imports
import pandas as pd
from flares import aggregate_fnirs_group_geometry, plot_2d_3d_contrasts_between_groups
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
0: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner about the contrast.",
"default": "True",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
},
{
"key": "is_3d",
"label": "Should we display the results in a 3D interactive window?",
"default": "True",
"type": bool,
}
],
}
class CrossGroupBrainImageWidget(CrossGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict):
super().__init__("CrossGroupBrainImage")
self.setWindowTitle(f"Cross-Group Brain & Image Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
self.group_dict = group_dict
self.setup_cross_group_ui(["0 (Contrast Image)"])
def proccess_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
if request is None:
return
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, param_values,) = request
# Build group-level contrast DataFrames
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
group_df = pd.DataFrame()
for fp in file_paths:
print(f"Looking up contrast for: {fp}")
event_con_dict = self.contrast_results_dict.get(fp, {})
print("Available events for this file:", list(event_con_dict.keys()))
if event and event in event_con_dict:
df = event_con_dict[event]
print(f"Appending contrast df for event: {event}")
group_df = pd.concat([group_df, df], ignore_index=True)
else:
print(f"Event '{event}' not found for {fp}")
return group_df
print("Selected event:", selected_event)
print("File paths A:", file_paths_a)
print("File paths B:", file_paths_b)
contrast_df_a = concat_group_contrasts(file_paths_a, selected_event)
contrast_df_b = concat_group_contrasts(file_paths_b, selected_event)
print("contrast_df_a empty?", contrast_df_a.empty)
print("contrast_df_b empty?", contrast_df_b.empty)
all_raw_objs = [self.haemo_dict.get(fp) for fp in all_selected_paths if self.haemo_dict.get(fp)]
if len(all_raw_objs) > 1:
processed_raw = aggregate_fnirs_group_geometry(all_raw_objs)
else:
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
# Visualizations
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
t_or_theta = params.get("t_or_theta", None)
show_text = params.get("show_text", None)
brain_bounds = params.get("brain_bounds", None)
is_3d = params.get("is_3d", None)
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None or is_3d is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
if not contrast_df_a.empty and not contrast_df_b.empty and processed_raw:
plot_2d_3d_contrasts_between_groups(
contrast_df_a,
contrast_df_b,
raw_haemo=processed_raw,
group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(),
is_3d=is_3d,
t_or_theta=t_or_theta,
show_optodes=show_optodes,
show_text=show_text,
brain_bounds=brain_bounds
)
else:
print(f"No method defined for index {idx}")
+114
View File
@@ -0,0 +1,114 @@
"""
Filename: crossgroupstats.py
Description: Cross-Group stats analysis window
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# External library imports
import pandas as pd
from flares import run_cross_group_second_level_analysis
from src.shared.flaresbasewidget import CrossGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
0: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner about the contrast.",
"default": "True",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
},
{
"key": "is_3d",
"label": "Should we display the results in a 3D interactive window?",
"default": "True",
"type": bool,
}
],
}
class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict):
super().__init__("CrossGroupStats")
self.setWindowTitle(f"Cross-Group Stats Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha_dict = cha_dict
self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict
self.group_dict = group_dict
self.setup_cross_group_ui(["0 (Compute Statistics)"])
def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
if request is None:
return
(selected_event, file_paths_a, file_paths_b, all_selected_paths, selected_indexes, param_values,) = request
if isinstance(self.df_ind_dict, dict):
# Filter out empty entries and concatenate
valid_dfs = [df for df in self.df_ind_dict.values() if isinstance(df, pd.DataFrame) and not df.empty]
if valid_dfs:
df_ind_combined = pd.concat(valid_dfs, ignore_index=True)
else:
df_ind_combined = pd.DataFrame()
else:
df_ind_combined = self.df_ind_dict
if isinstance(self.cha_dict, dict):
valid_chas = [df for df in self.cha_dict.values() if isinstance(df, pd.DataFrame) and not df.empty]
cha_combined = pd.concat(valid_chas, ignore_index=True) if valid_chas else pd.DataFrame()
else:
cha_combined = self.cha_dict
sample_path = file_paths_a[0]
p_haemo = self.haemo_dict.get(sample_path)
# Visualizations
for idx in selected_indexes:
if idx == 0:
run_cross_group_second_level_analysis(
df_roi_all=df_ind_combined, # Individual stats dataframe
file_paths_a=file_paths_a,
file_paths_b=file_paths_b,
group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(),
df_cha_all=cha_combined,
raw_haemo=p_haemo,
p_threshold=0.05,
min_subjects=3,
correction_method='fdr_bh',
target_chroma='hbo',
selected_event=selected_event,
roi_config=r"C:\Users\tyler\Desktop\research\flares\regions.json",
threshold_topo=False # Shows the raw difference map (Unthresholded)
)
else:
print("no")
@@ -1,27 +1,28 @@
""" """
Filename: exportcsv.py Filename: exporttocsv.py
Description: Export data as csv analysis window for FLARES Description: Logic for the Export To CSV analysis window
Author: Tyler de Zeeuw Author: Tyler de Zeeuw
License: GPL-3.0 License: GPL-3.0
""" """
# Built-in imports
import os import os
# External library imports
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from PySide6.QtWidgets import QFileDialog, QGridLayout, QHBoxLayout, QMessageBox, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel from PySide6.QtWidgets import QFileDialog, QMessageBox
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget from src.shared.flaresbasewidget import CSVUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME from src.shared.shareddata import APP_NAME
class ExportDataAsCSVViewerWidget(FlaresBaseWidget): class ExportToCSVWidget(CSVUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict): def __init__(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
super().__init__("ExportDataAsCSVViewer") super().__init__("ExportToCSV")
self.setWindowTitle(f"Export Data As CSV Viewer - {APP_NAME.upper()}") self.setWindowTitle(f"Export To CSV Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict self.haemo_dict = haemo_dict
self.cha_dict = cha_dict self.cha_dict = cha_dict
self.df_ind = df_ind self.df_ind = df_ind
@@ -29,55 +30,11 @@ class ExportDataAsCSVViewerWidget(FlaresBaseWidget):
self.group = group self.group = group
self.contrast_results_dict = contrast_results_dict self.contrast_results_dict = contrast_results_dict
# Create mappings: file_path -> participant label and dropdown display text self.setup_csv_ui(["0 (Export Data to CSV)", "1 (CSV for SPARKS)",])
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.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)
self.index_texts = [
"0 (Export Data to CSV)",
"1 (CSV for SPARKS)",
# "2 (third image)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.generate_and_save_csv)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Export Type:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def generate_and_save_csv(self): def process_request(self):
# TODO: Move this into flares for the call?
selected_display_names = self._get_checked_items(self.participant_dropdown) selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = [] selected_file_paths = []
for display_name in selected_display_names: for display_name in selected_display_names:
-306
View File
@@ -1,306 +0,0 @@
"""
Filename: group.py
Description: Group analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
import pandas as pd
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
from src.shared.shareddata import APP_NAME
class GroupViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
super().__init__("GroupViewer")
self.setWindowTitle(f"Group Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha = cha
self.df_ind = df_ind
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.show_all_events = True
self._updating_checkstates = False
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.group_to_paths = {}
for file_path, group_name in self.group.items():
self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys())
self.group_dropdown = QComboBox()
self.group_dropdown.addItem("<None Selected>")
self.group_dropdown.addItems(self.group_names)
self.group_dropdown.setCurrentIndex(0)
self.group_dropdown.currentIndexChanged.connect(self.update_participant_list_for_group)
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
self.participant_dropdown.setEnabled(False)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.index_texts = [
"0 (GLM Results)",
"1 (Significance)",
"2 (Brain Activity Visualization)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.show_brain_images)
self.top_bar.addWidget(QLabel("Group:"))
self.top_bar.addWidget(self.group_dropdown)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def show_brain_images(self):
import flares as flares
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
for display_name in selected_display_names:
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
if display_name == expected_display:
selected_file_paths.append(fp)
break
if selected_event:
valid_paths = []
for fp in selected_file_paths:
raw = self.haemo_dict.get(fp)
# Check if this participant actually has the event in their annotations
if raw is not None and hasattr(raw, "annotations"):
if selected_event in raw.annotations.description:
valid_paths.append(fp)
selected_file_paths = valid_paths
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
if not selected_file_paths:
print("No participants selected.")
return
# Only keep indexes 0 and 1 that need parameters
parameterized_indexes = {
0: [
{
"key": "lower_bound",
"label": "Lower bound + <description>",
"default": "-0.3",
"type": float, # specify int here
},
{
"key": "upper_bound",
"label": "Upper bound + <description>",
"default": "0.8",
"type": float, # specify int here
}
],
1: [
{
"key": "p_value",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "graph_bounds",
"label": "Graph Upper/Lower Limit",
"default": "3.0",
"type": float,
}
],
2: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner. THIS DOES NOT WORK AND SHOULD BE LEFT AT FALSE",
"default": "False",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
}
],
}
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted:
param_values = dialog.get_values()
if param_values is None:
return
else:
return
all_cha = pd.DataFrame()
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if selected_event:
participant_events = set(haemo_obj.annotations.description)
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
continue
cha_df = self.cha.get(file_path)
if cha_df is not None:
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
# Pass the necessary arguments to each method
file_path = selected_file_paths[0]
p_haemo = self.haemo_dict.get(file_path)
p_design_matrix = self.design_matrix.get(file_path)
df_group = pd.DataFrame()
if selected_file_paths:
for file_path in selected_file_paths:
df = self.df_ind.get(file_path)
if df is not None:
df_group = pd.concat([df_group, df], ignore_index=True)
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
lower_bound = params.get("lower_bound", None)
upper_bound = params.get("upper_bound", None)
if lower_bound is None or upper_bound is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
flares.plot_fir_model_results(df_group, p_haemo, p_design_matrix, selected_event, lower_bound, upper_bound)
elif idx == 1:
params = param_values.get(idx, {})
p_val = params.get("p_value", None)
graph_bounds = params.get("graph_bounds", None)
if p_val is None or graph_bounds is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
all_contrasts = []
for fp in selected_file_paths:
condition_dfs = self.contrast_results.get(fp, {})
if selected_event in condition_dfs:
df = condition_dfs[selected_event].copy()
df["ID"] = fp
all_contrasts.append(df)
if not all_contrasts:
print("No contrast data found for selected participants and event.")
return
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
elif idx == 2:
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
t_or_theta = params.get("t_or_theta", None)
show_text = params.get("show_text", None)
brain_bounds = params.get("brain_bounds", None)
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
if len(selected_file_paths) > 1:
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
processed_raw = flares.aggregate_fnirs_group_geometry(raw_list)
else:
processed_raw = raw_list[0].copy().pick(picks="hbo")
flares.brain_3d_visualization(processed_raw, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
elif idx == 3:
pass
else:
print(f"No method defined for index {idx}")
-311
View File
@@ -1,311 +0,0 @@
"""
Filename: groupbrain.py
Description: Group brain analysis window for FLARES
Author: Tyler de Zeeuw
License: GPL-3.0
"""
import os
import pandas as pd
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
from src.shared.shareddata import APP_NAME
class GroupBrainViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict):
super().__init__("GroupBrainViewer")
self.setWindowTitle(f"Group Brain Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.df_ind = df_ind
self.design_matrix = design_matrix
self.group = group
self.contrast_results_dict = contrast_results_dict
self.group_to_paths = {}
for file_path, group_name in self.group.items():
self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys())
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.group_a_dropdown = QComboBox()
self.group_a_dropdown.addItem("<None Selected>")
self.group_a_dropdown.addItems(self.group_names)
self.group_a_dropdown.currentIndexChanged.connect(self._update_group_a_options)
self.group_b_dropdown = QComboBox()
self.group_b_dropdown.addItem("<None Selected>")
self.group_b_dropdown.addItems(self.group_names)
self.group_b_dropdown.currentIndexChanged.connect(self._update_group_b_options)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.participant_dropdown_a = self._create_multiselect_dropdown([])
self.participant_dropdown_a.lineEdit().setPlaceholderText("Select participants (Group A)")
self.participant_dropdown_a.model().itemChanged.connect(self._on_participants_changed)
self.participant_dropdown_b = self._create_multiselect_dropdown([])
self.participant_dropdown_b.lineEdit().setPlaceholderText("Select participants (Group B)")
self.participant_dropdown_b.model().itemChanged.connect(self._on_participants_changed)
self.index_texts = [
"0 (Contrast Image)",
# "1 (3D Brain Contrast)",
# "2 (third image)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.show_brain_images)
self.top_bar.addWidget(QLabel("Group A:"))
self.top_bar.addWidget(self.group_a_dropdown)
self.top_bar.addWidget(QLabel("Participants (Group A):"))
self.top_bar.addWidget(self.participant_dropdown_a)
self.top_bar.addWidget(QLabel("Group B:"))
self.top_bar.addWidget(self.group_b_dropdown)
self.top_bar.addWidget(QLabel("Participants (Group B):"))
self.top_bar.addWidget(self.participant_dropdown_b)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def _update_group_b_options(self):
"""Triggered when Group B changes: Update Group A to exclude B's choice"""
selected_b = self.group_b_dropdown.currentText()
# Refresh Group A and exclude what was just picked in Group B
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected_b)
# Update the participants for Group B
self.update_participant_list_for_group(selected_b, self.participant_dropdown_b)
self._update_event_dropdown()
def _update_group_a_options(self):
"""Triggered when Group A changes: Update Group B to exclude A's choice"""
selected_a = self.group_a_dropdown.currentText()
# Refresh Group B and exclude what was just picked in Group A
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected_a)
# Update the participants for Group A
self.update_participant_list_for_group(selected_a, self.participant_dropdown_a)
self._update_event_dropdown()
def _on_participants_changed(self, item=None):
self._update_event_dropdown()
def _refresh_group_dropdown(self, dropdown, exclude):
current = dropdown.currentText()
dropdown.blockSignals(True)
dropdown.clear()
dropdown.addItem("<None Selected>")
for group in self.group_names:
if group != exclude:
dropdown.addItem(group)
# Restore previous selection if still valid
if current != "<None Selected>" and current != exclude and dropdown.findText(current) != -1:
dropdown.setCurrentText(current)
else:
dropdown.setCurrentIndex(0) # Reset to "<None Selected>"
dropdown.blockSignals(False)
def _get_file_paths_from_labels(self, labels, group_name):
file_paths = []
if group_name == self.group_a_dropdown.currentText():
participant_map = self.participant_map_a
elif group_name == self.group_b_dropdown.currentText():
participant_map = self.participant_map_b
else:
return []
# Reverse map: display label -> file path
reverse_map = {
f"{label} ({os.path.basename(fp)})": fp
for fp, label in participant_map.items()
}
for label in labels:
file_path = reverse_map.get(label)
if file_path:
file_paths.append(file_path)
return file_paths
def show_brain_images(self):
import flares as flares
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
# Group A
participants_a = self._get_checked_items(self.participant_dropdown_a)
file_paths_a = self._get_file_paths_from_labels(participants_a, self.group_a_dropdown.currentText())
# Group B
participants_b = self._get_checked_items(self.participant_dropdown_b)
file_paths_b = self._get_file_paths_from_labels(participants_b, self.group_b_dropdown.currentText())
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
all_selected_paths = list(set(file_paths_a + file_paths_b))
if not all_selected_paths:
print("No participants selected.")
return
parameterized_indexes = {
0: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner about the contrast.",
"default": "True",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
},
{
"key": "is_3d",
"label": "Should we display the results in a 3D interactive window?",
"default": "True",
"type": bool,
}
],
}
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted:
param_values = dialog.get_values()
if param_values is None:
return
else:
return
# Build group-level contrast DataFrames
def concat_group_contrasts(file_paths: list[str], event: str | None) -> pd.DataFrame:
group_df = pd.DataFrame()
for fp in file_paths:
print(f"Looking up contrast for: {fp}")
event_con_dict = self.contrast_results_dict.get(fp, {})
print("Available events for this file:", list(event_con_dict.keys()))
if event and event in event_con_dict:
df = event_con_dict[event]
print(f"Appending contrast df for event: {event}")
group_df = pd.concat([group_df, df], ignore_index=True)
else:
print(f"Event '{event}' not found for {fp}")
return group_df
print("Selected event:", selected_event)
print("File paths A:", file_paths_a)
print("File paths B:", file_paths_b)
contrast_df_a = concat_group_contrasts(file_paths_a, selected_event)
contrast_df_b = concat_group_contrasts(file_paths_b, selected_event)
print("contrast_df_a empty?", contrast_df_a.empty)
print("contrast_df_b empty?", contrast_df_b.empty)
all_raw_objs = [self.haemo_dict.get(fp) for fp in all_selected_paths if self.haemo_dict.get(fp)]
if len(all_raw_objs) > 1:
processed_raw = flares.aggregate_fnirs_group_geometry(all_raw_objs)
else:
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
# Visualizations
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
t_or_theta = params.get("t_or_theta", None)
show_text = params.get("show_text", None)
brain_bounds = params.get("brain_bounds", None)
is_3d = params.get("is_3d", None)
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None or is_3d is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
if not contrast_df_a.empty and not contrast_df_b.empty and processed_raw:
flares.plot_2d_3d_contrasts_between_groups(
contrast_df_a,
contrast_df_b,
raw_haemo=processed_raw,
group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(),
is_3d=is_3d,
t_or_theta=t_or_theta,
show_optodes=show_optodes,
show_text=show_text,
brain_bounds=brain_bounds
)
else:
print("no")
+190
View File
@@ -0,0 +1,190 @@
"""
Filename: intergroupbrainimage.py
Description: Logic for the Inter-Group Brain & Image analysis window
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# External library imports
import pandas as pd
from flares import aggregate_fnirs_group_geometry, plot_fir_model_results, brain_3d_visualization
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
0: [
{
"key": "lower_bound",
"label": "Lower bound + <description>",
"default": "-0.3",
"type": float, # specify int here
},
{
"key": "upper_bound",
"label": "Upper bound + <description>",
"default": "0.8",
"type": float, # specify int here
}
],
1: [
{
"key": "p_value",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "graph_bounds",
"label": "Graph Upper/Lower Limit",
"default": "3.0",
"type": float,
}
],
2: [
{
"key": "show_optodes",
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
"default": "all",
"type": str,
},
{
"key": "t_or_theta",
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
"default": "theta",
"type": str,
},
{
"key": "show_text",
"label": "Display informative text on the top left corner. THIS DOES NOT WORK AND SHOULD BE LEFT AT FALSE",
"default": "False",
"type": bool,
},
{
"key": "brain_bounds",
"label": "Graph Upper/Lower Limit",
"default": "1.0",
"type": float,
}
],
}
class InterGroupBrainImageWidget(InterGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
super().__init__("InterGroupBrainImage")
self.setWindowTitle(f"Inter-Group Brain & Image Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha = cha
self.df_ind = df_ind
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.setup_inter_group_ui(["0 (GLM Results)", "1 (Significance)", "2 (Brain Activity Visualization)",])
def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
all_cha = pd.DataFrame()
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if selected_event:
participant_events = set(haemo_obj.annotations.description)
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
continue
cha_df = self.cha.get(file_path)
if cha_df is not None:
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
# Pass the necessary arguments to each method
file_path = selected_file_paths[0]
p_haemo = self.haemo_dict.get(file_path)
p_design_matrix = self.design_matrix.get(file_path)
df_group = pd.DataFrame()
if selected_file_paths:
for file_path in selected_file_paths:
df = self.df_ind.get(file_path)
if df is not None:
df_group = pd.concat([df_group, df], ignore_index=True)
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
lower_bound = params.get("lower_bound", None)
upper_bound = params.get("upper_bound", None)
if lower_bound is None or upper_bound is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
plot_fir_model_results(df_group, p_haemo, p_design_matrix, selected_event, lower_bound, upper_bound)
elif idx == 1:
params = param_values.get(idx, {})
p_val = params.get("p_value", None)
graph_bounds = params.get("graph_bounds", None)
if p_val is None or graph_bounds is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
all_contrasts = []
for fp in selected_file_paths:
condition_dfs = self.contrast_results.get(fp, {})
if selected_event in condition_dfs:
df = condition_dfs[selected_event].copy()
df["ID"] = fp
all_contrasts.append(df)
if not all_contrasts:
print("No contrast data found for selected participants and event.")
return
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
#flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
elif idx == 2:
params = param_values.get(idx, {})
show_optodes = params.get("show_optodes", None)
t_or_theta = params.get("t_or_theta", None)
show_text = params.get("show_text", None)
brain_bounds = params.get("brain_bounds", None)
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None:
print(f"Missing parameters for index {idx}, skipping.")
continue
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
if len(selected_file_paths) > 1:
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
processed_raw = aggregate_fnirs_group_geometry(raw_list)
else:
processed_raw = raw_list[0].copy().pick(picks="hbo")
brain_3d_visualization(processed_raw, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
elif idx == 3:
pass
else:
print(f"No method defined for index {idx}")
+130
View File
@@ -0,0 +1,130 @@
"""
Filename: intergroupstats.py
Description: Logic for the Inter-Group Stats analysis window
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# External library imports
import pandas as pd
from flares import run_roi_second_level_analysis
from src.shared.flaresbasewidget import InterGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES = {
0: [
{
"key": "p_value",
"label": "Significance threshold P-value (e.g. 0.05)",
"default": "0.05",
"type": float,
},
{
"key": "graph_bounds",
"label": "Graph Y-Limit (Optional, e.g. 1e-5)",
"default": "0.0", # Set to 0.0 to auto-scale
"type": float,
}
],
}
class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha, df_ind, design_matrix, contrast_results, group):
super().__init__("InterGroupStats")
self.setWindowTitle(f"Inter-Group Stats Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha = cha
self.df_ind = df_ind
self.design_matrix = design_matrix
self.contrast_results = contrast_results
self.group = group
self.setup_inter_group_ui(["0 (Significance)",])
def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
all_cha = pd.DataFrame()
for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path)
if selected_event:
participant_events = set(haemo_obj.annotations.description)
if selected_event not in participant_events:
print(f"Skipping {self.participant_map[file_path]}: Event '{selected_event}' not found.")
continue
if haemo_obj is None:
continue
cha_df = self.cha.get(file_path)
if cha_df is not None:
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
file_path = selected_file_paths[0]
p_haemo = self.haemo_dict.get(file_path)
# Concatenate individual ROI stats (df_ind) for all chosen subjects
df_group = pd.DataFrame()
if selected_file_paths:
for file_path in selected_file_paths:
df = self.df_ind.get(file_path)
if df is not None:
df_group = pd.concat([df_group, df], ignore_index=True)
for idx in selected_indexes:
if idx == 0:
params = param_values.get(idx, {})
p_val = params.get("p_value", 0.05)
graph_bounds = params.get("graph_bounds", 0.0)
if df_group.empty:
print("No ROI data (df_ind) found for selected participants.")
continue
# Filter down to the selected experimental event/condition
if selected_event:
if 'Condition' in df_group.columns:
df_filtered = df_group[df_group['Condition'] == selected_event]
else:
print("Warning: 'Condition' column not found in ROI data.")
df_filtered = df_group
else:
df_filtered = df_group
if df_filtered.empty:
print(f"No ROI data matches the condition '{selected_event}'.")
continue
all_cha_filtered = pd.DataFrame()
if not all_cha.empty:
if selected_event and 'Condition' in all_cha.columns:
all_cha_filtered = all_cha[all_cha['Condition'] == selected_event]
else:
all_cha_filtered = all_cha
# Call your new custom group ROI method!
run_roi_second_level_analysis(
df_roi_all=df_filtered,
df_cha_all=all_cha_filtered,
raw_haemo=p_haemo,
p_threshold=p_val,
min_subjects=len(selected_file_paths),
correction_method='fdr_bh',
target_chroma='hbo',
graph_bounds=graph_bounds if graph_bounds > 0.0 else None,
roi_config=r"C:\Users\tyler\Desktop\research\flares\regions.json"
)
else:
print(f"No method defined for index {idx}")
+23 -106
View File
@@ -1,102 +1,18 @@
""" """
Filename: participantbrain.py Filename: participantbrain.py
Description: Participant brain analysis window for FLARES Description: Logic for the Participant Brain analysis window
Author: Tyler de Zeeuw Author: Tyler de Zeeuw
License: GPL-3.0 License: GPL-3.0
""" """
import os # External library imports
from flares import brain_3d_visualization, brain_landmarks_3d
from PySide6.QtWidgets import QComboBox, QDialog, QGridLayout, QHBoxLayout, QPushButton, QScrollArea, QWidget, QVBoxLayout, QLabel from src.shared.flaresbasewidget import ParticipantUIMixin, FlaresBaseWidget
from PySide6.QtCore import QSize
from src.shared.flaresbasewidget import FlaresBaseWidget, ParameterInputDialog
from src.shared.shareddata import APP_NAME from src.shared.shareddata import APP_NAME
class ParticipantBrainViewerWidget(FlaresBaseWidget): PARAMETERIZED_INDEXES = {
def __init__(self, haemo_dict, cha_dict):
super().__init__("ParticipantBrainViewer")
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha_dict = cha_dict
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.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)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.index_texts = [
"0 (Brain Landmarks)",
"1 (Brain Activity Visualization)",
# "2 (third image)",
# "3 (fourth image)",
]
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.show_brain_images)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def show_brain_images(self):
import flares as flares
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
for display_name in selected_display_names:
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
if display_name == expected_display:
selected_file_paths.append(fp)
break
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
parameterized_indexes = {
0: [ 0: [
{ {
"key": "show_optodes", "key": "show_optodes",
@@ -137,26 +53,27 @@ class ParticipantBrainViewerWidget(FlaresBaseWidget):
"type": float, "type": float,
} }
], ],
} }
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes} class ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
def __init__(self, haemo_dict, cha_dict):
super().__init__("ParticipantBrainViewer")
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.cha_dict = cha_dict
param_values = {} self.setup_participant_ui(["0 (Brain Landmarks)", "1 (Brain Activity Visualization)",])
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted: def process_request(self):
param_values = dialog.get_values()
if param_values is None: request = self.get_common_request_data(PARAMETERIZED_INDEXES)
return if request is None:
else:
return return
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
# Pass the necessary arguments to each method # Pass the necessary arguments to each method
for file_path in selected_file_paths: for file_path in selected_file_paths:
haemo_obj = self.haemo_dict.get(file_path) haemo_obj = self.haemo_dict.get(file_path)
@@ -183,7 +100,7 @@ class ParticipantBrainViewerWidget(FlaresBaseWidget):
print(f"Missing parameters for index {idx}, skipping.") print(f"Missing parameters for index {idx}, skipping.")
continue continue
flares.brain_landmarks_3d(haemo_obj, show_optodes, show_brodmann) brain_landmarks_3d(haemo_obj, show_optodes, show_brodmann)
elif idx == 1: elif idx == 1:
params = param_values.get(idx, {}) params = param_values.get(idx, {})
@@ -196,7 +113,7 @@ class ParticipantBrainViewerWidget(FlaresBaseWidget):
print(f"Missing parameters for index {idx}, skipping.") print(f"Missing parameters for index {idx}, skipping.")
continue continue
flares.brain_3d_visualization(haemo_obj, cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds) brain_3d_visualization(haemo_obj, cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
else: else:
print(f"No method defined for index {idx}") print(f"No method defined for index {idx}")
@@ -18,9 +18,9 @@ from src.shared.flaresbasewidget import ClickableLabel, FlaresBaseWidget
from src.shared.shareddata import APP_NAME from src.shared.shareddata import APP_NAME
class ParticipantViewerWidget(FlaresBaseWidget): class ParticipantImageViewerWidget(FlaresBaseWidget):
def __init__(self, haemo_dict, fig_bytes_dict): def __init__(self, haemo_dict, fig_bytes_dict):
super().__init__("ParticipantViewer") super().__init__("ParticipantImage")
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.setWindowTitle(f"Participant Viewer - {APP_NAME.upper()}") self.setWindowTitle(f"Participant Viewer - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict self.haemo_dict = haemo_dict
+480 -4
View File
@@ -8,9 +8,9 @@ License: GPL-3.0
import os import os
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QVBoxLayout, QWidget, QFrame, QSpinBox from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QScrollArea, QVBoxLayout, QWidget, QFrame, QSpinBox
from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator from PySide6.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator
from PySide6.QtCore import QEvent, Qt from PySide6.QtCore import QEvent, QSize, Qt
from src.shared.shareddata import APP_NAME from src.shared.shareddata import APP_NAME
@@ -836,9 +836,9 @@ class FlaresBaseWidget(QWidget):
# 3. Conditional trigger for event updates # 3. Conditional trigger for event updates
# We only update events if we aren't in one of the excluded viewers # We only update events if we aren't in one of the excluded viewers
excluded_viewers = { excluded_viewers = {
"ParticipantViewer", "ParticipantImage",
"ParticipantFoldChannels", "ParticipantFoldChannels",
"ExportDataAsCSVViewer", "ExportToCSV",
} }
if getattr(self, "caller", None) not in excluded_viewers: if getattr(self, "caller", None) not in excluded_viewers:
@@ -1051,3 +1051,479 @@ class FlaresBaseWidget(QWidget):
self._connect_select_all_toggle(toggle_ref, model) self._connect_select_all_toggle(toggle_ref, model)
self.update_participant_dropdown_label(combo=target_combo) self.update_participant_dropdown_label(combo=target_combo)
class CrossGroupUIMixin:
def setup_cross_group_ui(self, index_texts):
self.group_to_paths = {}
for file_path, group_name in self.group_dict.items():
self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys())
self.main_layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.main_layout.addLayout(self.top_bar)
self.group_a_dropdown = QComboBox()
self.group_a_dropdown.addItem("<None Selected>")
self.group_a_dropdown.addItems(self.group_names)
self.group_a_dropdown.currentIndexChanged.connect(self._update_group_a_options)
self.group_b_dropdown = QComboBox()
self.group_b_dropdown.addItem("<None Selected>")
self.group_b_dropdown.addItems(self.group_names)
self.group_b_dropdown.currentIndexChanged.connect(self._update_group_b_options)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.participant_dropdown_a = self._create_multiselect_dropdown([])
line_edit = self.participant_dropdown_a.lineEdit()
assert line_edit is not None, "Dropdown A must be editable to have a lineEdit"
line_edit.setPlaceholderText("Select participants (Group A)")
model = self.participant_dropdown_a.model()
assert isinstance(model, QStandardItemModel), "Model must be QStandardItemModel"
model.itemChanged.connect(self._on_participants_changed)
self.participant_dropdown_b = self._create_multiselect_dropdown([])
line_edit = self.participant_dropdown_b.lineEdit()
assert line_edit is not None, "Dropdown B must be editable to have a lineEdit"
line_edit.setPlaceholderText("Select participants (Group B)")
model = self.participant_dropdown_b.model()
assert isinstance(model, QStandardItemModel), "Model must be QStandardItemModel"
model.itemChanged.connect(self._on_participants_changed)
self.index_texts = index_texts
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.proccess_request)
self.top_bar.addWidget(QLabel("Group A:"))
self.top_bar.addWidget(self.group_a_dropdown)
self.top_bar.addWidget(QLabel("Participants (Group A):"))
self.top_bar.addWidget(self.participant_dropdown_a)
self.top_bar.addWidget(QLabel("Group B:"))
self.top_bar.addWidget(self.group_b_dropdown)
self.top_bar.addWidget(QLabel("Participants (Group B):"))
self.top_bar.addWidget(self.participant_dropdown_b)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll_area.setWidget(self.scroll_content)
self.main_layout.addWidget(self.scroll_area)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def _update_group_b_options(self):
"""Triggered when Group B changes: Update Group A to exclude B's choice"""
selected_b = self.group_b_dropdown.currentText()
# Refresh Group A and exclude what was just picked in Group B
self._refresh_group_dropdown(self.group_a_dropdown, exclude=selected_b)
# Update the participants for Group B
self.update_participant_list_for_group(selected_b, self.participant_dropdown_b)
self._update_event_dropdown()
def _update_group_a_options(self):
"""Triggered when Group A changes: Update Group B to exclude A's choice"""
selected_a = self.group_a_dropdown.currentText()
# Refresh Group B and exclude what was just picked in Group A
self._refresh_group_dropdown(self.group_b_dropdown, exclude=selected_a)
# Update the participants for Group A
self.update_participant_list_for_group(selected_a, self.participant_dropdown_a)
self._update_event_dropdown()
def _on_participants_changed(self, item=None):
self._update_event_dropdown()
def _refresh_group_dropdown(self, dropdown, exclude):
current = dropdown.currentText()
dropdown.blockSignals(True)
dropdown.clear()
dropdown.addItem("<None Selected>")
for group in self.group_names:
if group != exclude:
dropdown.addItem(group)
# Restore previous selection if still valid
if current != "<None Selected>" and current != exclude and dropdown.findText(current) != -1:
dropdown.setCurrentText(current)
else:
dropdown.setCurrentIndex(0) # Reset to "<None Selected>"
dropdown.blockSignals(False)
def _get_file_paths_from_labels(self, labels, group_name):
file_paths = []
if group_name == self.group_a_dropdown.currentText():
participant_map = self.participant_map_a
elif group_name == self.group_b_dropdown.currentText():
participant_map = self.participant_map_b
else:
return []
# Reverse map: display label -> file path
reverse_map = {
f"{label} ({os.path.basename(fp)})": fp
for fp, label in participant_map.items()
}
for label in labels:
file_path = reverse_map.get(label)
if file_path:
file_paths.append(file_path)
return file_paths
def get_common_request_data(self, parameterized_indexes):
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
participants_a = self._get_checked_items(self.participant_dropdown_a)
file_paths_a = self._get_file_paths_from_labels(
participants_a, self.group_a_dropdown.currentText()
)
participants_b = self._get_checked_items(self.participant_dropdown_b)
file_paths_b = self._get_file_paths_from_labels(
participants_b, self.group_b_dropdown.currentText()
)
selected_indexes = [
int(s.split(" ")[0])
for s in self._get_checked_items(self.image_index_dropdown)
]
all_selected_paths = list(set(file_paths_a + file_paths_b))
if not all_selected_paths:
print("No participants selected.")
return None
# Inject full_text
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx]
for param in params_list:
param["full_text"] = full_text
indexes_needing_params = {
idx: parameterized_indexes[idx]
for idx in selected_indexes
if idx in parameterized_indexes
}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec() != QDialog.DialogCode.Accepted:
return None
param_values = dialog.get_values()
if param_values is None:
return None
return (
selected_event,
file_paths_a,
file_paths_b,
all_selected_paths,
selected_indexes,
param_values,
)
class CSVUIMixin:
def setup_csv_ui(self, index_texts):
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.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)
self.index_texts = index_texts
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.process_request)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Export Type:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
class InterGroupUIMixin:
def setup_inter_group_ui(self, index_texts):
self.show_all_events = True
self._updating_checkstates = False
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.layout.addLayout(self.top_bar)
self.group_to_paths = {}
for file_path, group_name in self.group.items():
self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys())
self.group_dropdown = QComboBox()
self.group_dropdown.addItem("<None Selected>")
self.group_dropdown.addItems(self.group_names)
self.group_dropdown.setCurrentIndex(0)
self.group_dropdown.currentIndexChanged.connect(self.update_participant_list_for_group)
self.participant_dropdown = self._create_multiselect_dropdown(self.participant_dropdown_items)
self.participant_dropdown.currentIndexChanged.connect(self.update_participant_dropdown_label)
self.participant_dropdown.setEnabled(False)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.index_texts = index_texts
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.process_request)
self.top_bar.addWidget(QLabel("Group:"))
self.top_bar.addWidget(self.group_dropdown)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def get_common_request_data(self, parameterized_indexes):
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
for display_name in selected_display_names:
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
if display_name == expected_display:
selected_file_paths.append(fp)
break
if selected_event:
valid_paths = []
for fp in selected_file_paths:
raw = self.haemo_dict.get(fp)
# Check if this participant actually has the event in their annotations
if raw is not None and hasattr(raw, "annotations"):
if selected_event in raw.annotations.description:
valid_paths.append(fp)
selected_file_paths = valid_paths
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
if not selected_file_paths:
print("No participants selected.")
return
# Only keep indexes 0 and 1 that need parameters
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted:
param_values = dialog.get_values()
if param_values is None:
return
else:
return
return (
selected_event,
selected_file_paths,
selected_indexes,
param_values,
)
class ParticipantUIMixin:
def setup_participant_ui(self, index_texts):
# Create mappings: file_path -> participant label and dropdown display text
self.participant_map = {} # file_path -> "Participant 1"
self.participant_dropdown_items = [] # "Participant 1 (filename)"
for i, file_path in enumerate(self.haemo_dict.keys(), start=1):
short_label = f"Participant {i}"
display_label = f"{short_label} ({os.path.basename(file_path)})"
self.participant_map[file_path] = short_label
self.participant_dropdown_items.append(display_label)
self.layout = QVBoxLayout(self)
self.top_bar = QHBoxLayout()
self.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)
self.event_dropdown = QComboBox()
self.event_dropdown.addItem("<None Selected>")
self.index_texts = index_texts
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.process_request)
self.top_bar.addWidget(QLabel("Participants:"))
self.top_bar.addWidget(self.participant_dropdown)
self.top_bar.addWidget(QLabel("Event:"))
self.top_bar.addWidget(self.event_dropdown)
self.top_bar.addWidget(QLabel("Image Indexes:"))
self.top_bar.addWidget(self.image_index_dropdown)
self.top_bar.addWidget(self.submit_button)
self.scroll = QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll_content = QWidget()
self.grid_layout = QGridLayout(self.scroll_content)
self.scroll.setWidget(self.scroll_content)
self.layout.addWidget(self.scroll)
self.thumb_size = QSize(280, 180)
self.showMaximized()
def get_common_request_data(self, parameterized_indexes):
selected_event = self.event_dropdown.currentText()
if selected_event == "<None Selected>":
selected_event = None
selected_display_names = self._get_checked_items(self.participant_dropdown)
selected_file_paths = []
for display_name in selected_display_names:
for fp, short_label in self.participant_map.items():
expected_display = f"{short_label} ({os.path.basename(fp)})"
if display_name == expected_display:
selected_file_paths.append(fp)
break
selected_indexes = [
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
# Inject full_text from index_texts
for idx, params_list in parameterized_indexes.items():
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
for param_info in params_list:
param_info["full_text"] = full_text
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
param_values = {}
if indexes_needing_params:
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
if dialog.exec_() == QDialog.Accepted:
param_values = dialog.get_values()
if param_values is None:
return
else:
return
return (
selected_event,
selected_file_paths,
selected_indexes,
param_values,
)
+37 -84
View File
@@ -1,19 +1,22 @@
""" """
Filename: viewerlauncher.py Filename: viewerlauncher.py
Description: Analysis options launcher for FLARES Description: Viewer launcher window
Author: Tyler de Zeeuw Author: Tyler de Zeeuw
License: GPL-3.0 License: GPL-3.0
""" """
# External library imports
from PySide6.QtWidgets import QPushButton, QWidget, QVBoxLayout from PySide6.QtWidgets import QPushButton, QWidget, QVBoxLayout
from PySide6.QtCore import QTimer from PySide6.QtCore import QTimer
from src.analysis.exportcsv import ExportDataAsCSVViewerWidget from src.analysis.exporttocsv import ExportToCSVWidget
from src.analysis.group import GroupViewerWidget from src.analysis.intergroupbrainimage import InterGroupBrainImageWidget
from src.analysis.groupbrain import GroupBrainViewerWidget from src.analysis.crossgroupbrainimage import CrossGroupBrainImageWidget
from src.analysis.groupfunctionalconnectivity import GroupFunctionalConnectivityWidget from src.analysis.intergroupfunctionalconnectivity import InterGroupFunctionalConnectivityWidget
from src.analysis.participant import ParticipantViewerWidget from src.analysis.intergroupstats import InterGroupStatsWidget
from src.analysis.crossgroupstats import CrossGroupStatsWidget
from src.analysis.participantimage import ParticipantImageViewerWidget
from src.analysis.participantbrain import ParticipantBrainViewerWidget from src.analysis.participantbrain import ParticipantBrainViewerWidget
from src.analysis.participantfoldchannels import ParticipantFoldChannelsWidget from src.analysis.participantfoldchannels import ParticipantFoldChannelsWidget
from src.analysis.participantfunctionalconnectivity import ParticipantFunctionalConnectivityWidget from src.analysis.participantfunctionalconnectivity import ParticipantFunctionalConnectivityWidget
@@ -21,92 +24,42 @@ from src.shared.shareddata import APP_NAME
class ViewerLauncherWidget(QWidget): class ViewerLauncherWidget(QWidget):
def __init__(self, haemo_dict, config_dict, fig_bytes_dict, cha_dict, contrast_results_dict, df_ind, design_matrix, epochs_dict, folding_bypass): def __init__(self, haemo_dict, epochs_dict, cha_dict, df_ind_dict, design_matrix_dict, config_dict, fig_bytes_dict, contrast_results_dict, folding_bypass):
super().__init__() super().__init__()
self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}") self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}")
group_dict = { group_dict = {f: c.get("GROUP", "Unknown") for f, c in config_dict.items()}
file_path: config.get("GROUP", "Unknown")
for file_path, config in config_dict.items()
}
def launch(func, btn, *args): btn_data = [
func(*args) ("Participant Image Viewer", ParticipantImageViewerWidget, [haemo_dict, fig_bytes_dict], True),
self._trigger_success(btn) ("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),
("Inter-Group Functional Connectivity Viewer [BETA]", InterGroupFunctionalConnectivityWidget, [haemo_dict, group_dict, config_dict], True),
("Inter-Group Stats Viewer", InterGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Cross-Group Stats Viewer", CrossGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Inter-Group Brain & Image Viewer", InterGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Cross-Group Brain & Image Viewer", CrossGroupBrainImageWidget, [haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, group_dict, contrast_results_dict], True)
]
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
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.setEnabled(not (requires_bypass and folding_bypass))
layout.addWidget(btn)
btn1 = QPushButton("Open Participant Viewer") def _open_viewer(self, widget_class, btn, *args):
btn1.clicked.connect(lambda: launch(self.open_participant_viewer, btn1, haemo_dict, fig_bytes_dict)) # Instantiate and show dynamically
btn1.setEnabled(not folding_bypass) self.active_viewer = widget_class(*args)
self.active_viewer.show()
self._trigger_success(btn)
btn2 = QPushButton("Open Participant Brain Viewer") def _launch(self, func, btn, *args):
btn2.clicked.connect(lambda: launch(self.open_participant_brain_viewer, btn2, haemo_dict, cha_dict)) func(*args)
btn2.setEnabled(not folding_bypass) self._trigger_success(btn)
btn3 = QPushButton("Open Participant Fold Channels Viewer")
btn3.clicked.connect(lambda: launch(self.open_participant_fold_channels_viewer, btn3, haemo_dict, cha_dict))
btn7 = QPushButton("Open Functional Connectivity Viewer [BETA]")
btn7.clicked.connect(lambda: launch(self.open_participant_functional_connectivity_viewer, btn7, haemo_dict, epochs_dict))
btn7.setEnabled(not folding_bypass)
btn8 = QPushButton("Open Group Functional Connectivity Viewer [BETA]")
btn8.clicked.connect(lambda: launch(self.open_group_functional_connectivity_viewer, btn8, haemo_dict, group_dict, config_dict))
btn8.setEnabled(not folding_bypass)
btn4 = QPushButton("Open Inter-Group Viewer")
btn4.clicked.connect(lambda: launch(self.open_group_viewer, btn4, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group_dict))
btn4.setEnabled(not folding_bypass)
btn5 = QPushButton("Open Cross Group Brain Viewer")
btn5.clicked.connect(lambda: launch(self.open_group_brain_viewer, btn5, haemo_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
btn5.setEnabled(not folding_bypass)
btn6 = QPushButton("Open Export Data As CSV Viewer")
btn6.clicked.connect(lambda: launch(self.open_export_data_as_csv_viewer, btn6, haemo_dict, cha_dict, df_ind, design_matrix, group_dict, contrast_results_dict))
btn6.setEnabled(not folding_bypass)
layout.addWidget(btn1)
layout.addWidget(btn2)
layout.addWidget(btn3)
layout.addWidget(btn7)
layout.addWidget(btn8)
layout.addWidget(btn4)
layout.addWidget(btn5)
layout.addWidget(btn6)
def open_participant_viewer(self, haemo_dict, fig_bytes_dict):
self.participant_viewer = ParticipantViewerWidget(haemo_dict, fig_bytes_dict)
self.participant_viewer.show()
def open_participant_brain_viewer(self, haemo_dict, cha_dict):
self.participant_brain_viewer = ParticipantBrainViewerWidget(haemo_dict, cha_dict)
self.participant_brain_viewer.show()
def open_participant_fold_channels_viewer(self, haemo_dict, cha_dict):
self.participant_fold_channels_viewer = ParticipantFoldChannelsWidget(haemo_dict, cha_dict)
self.participant_fold_channels_viewer.show()
def open_participant_functional_connectivity_viewer(self, haemo_dict, epochs_dict):
self.participant_brain_viewer = ParticipantFunctionalConnectivityWidget(haemo_dict, epochs_dict)
self.participant_brain_viewer.show()
def open_group_functional_connectivity_viewer(self, haemo_dict, group, config_dict):
self.participant_brain_viewer = GroupFunctionalConnectivityWidget(haemo_dict, group, config_dict)
self.participant_brain_viewer.show()
def open_group_viewer(self, haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group):
self.participant_brain_viewer = GroupViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, contrast_results_dict, group)
self.participant_brain_viewer.show()
def open_group_brain_viewer(self, haemo_dict, df_ind, design_matrix, group, contrast_results_dict):
self.participant_brain_viewer = GroupBrainViewerWidget(haemo_dict, df_ind, design_matrix, group, contrast_results_dict)
self.participant_brain_viewer.show()
def open_export_data_as_csv_viewer(self, haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict):
self.export_data_as_csv_viewer = ExportDataAsCSVViewerWidget(haemo_dict, cha_dict, df_ind, design_matrix, group, contrast_results_dict)
self.export_data_as_csv_viewer.show()
def _trigger_success(self, button): def _trigger_success(self, button):
"""Temporarily adds a green checkmark to the button text.""" """Temporarily adds a green checkmark to the button text."""