small rewrite to impose dry principles
This commit is contained in:
@@ -268,7 +268,13 @@ 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": [
|
||||||
# Intentionally empty (TODO)
|
# Intentionally empty (TODO)
|
||||||
@@ -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)}")
|
||||||
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -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:
|
||||||
@@ -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}")
|
|
||||||
@@ -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")
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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}")
|
||||||
@@ -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}")
|
||||||
@@ -1,161 +1,78 @@
|
|||||||
"""
|
"""
|
||||||
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 = {
|
||||||
|
0: [
|
||||||
|
{
|
||||||
|
"key": "show_optodes",
|
||||||
|
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
|
||||||
|
"default": "all",
|
||||||
|
"type": str,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "show_brodmann",
|
||||||
|
"label": "Show common brodmann areas on the brain.",
|
||||||
|
"default": "True",
|
||||||
|
"type": bool,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
1: [
|
||||||
|
{
|
||||||
|
"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 ParticipantBrainViewerWidget(ParticipantUIMixin, FlaresBaseWidget):
|
||||||
def __init__(self, haemo_dict, cha_dict):
|
def __init__(self, haemo_dict, cha_dict):
|
||||||
super().__init__("ParticipantBrainViewer")
|
super().__init__("ParticipantBrainViewer")
|
||||||
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
|
self.setWindowTitle(f"Participant Brain Viewer - {APP_NAME.upper()}")
|
||||||
self.haemo_dict = haemo_dict
|
self.haemo_dict = haemo_dict
|
||||||
self.cha_dict = cha_dict
|
self.cha_dict = cha_dict
|
||||||
|
|
||||||
# Create mappings: file_path -> participant label and dropdown display text
|
self.setup_participant_ui(["0 (Brain Landmarks)", "1 (Brain Activity Visualization)",])
|
||||||
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 = [
|
def process_request(self):
|
||||||
"0 (Brain Landmarks)",
|
|
||||||
"1 (Brain Activity Visualization)",
|
|
||||||
# "2 (third image)",
|
|
||||||
# "3 (fourth image)",
|
|
||||||
]
|
|
||||||
|
|
||||||
self.image_index_dropdown = self._create_multiselect_dropdown(self.index_texts)
|
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
|
||||||
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
|
if request is None:
|
||||||
|
return
|
||||||
|
|
||||||
self.submit_button = QPushButton("Submit")
|
(selected_event, selected_file_paths, selected_indexes, param_values,) = request
|
||||||
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: [
|
|
||||||
{
|
|
||||||
"key": "show_optodes",
|
|
||||||
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
|
|
||||||
"default": "all",
|
|
||||||
"type": str,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "show_brodmann",
|
|
||||||
"label": "Show common brodmann areas on the brain.",
|
|
||||||
"default": "True",
|
|
||||||
"type": bool,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
1: [
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user