fix to stats when no json file is defined

This commit is contained in:
2026-08-03 00:01:21 -07:00
parent 61a9ea2f34
commit 7c456e31e7
7 changed files with 122 additions and 135 deletions
+5 -1
View File
@@ -1,8 +1,12 @@
# Verison 1.5.3 # Verison 1.6.0
- This is potentially a save-changing release due to adding more data into the save file. Please update your project files to ensure compatibility
- It is still possible to load older saves by enabling 'Incompatible Save Bypass' from the Preferences menu, but your mileage may vary
- Optimized calculations being performed when calculating the heart rate to speed up step 5 by up to ~35% on a per-file basis - Optimized calculations being performed when calculating the heart rate to speed up step 5 by up to ~35% on a per-file basis
- Optimized calculations being performed when running the General Linear Model to speed up step 5 by ~35% on a per-file basis - Optimized calculations being performed when running the General Linear Model to speed up step 5 by ~35% on a per-file basis
- Fixed an issue where participants could be skipped when processing multiple particants at one which could prevent overall processing from completing - Fixed an issue where participants could be skipped when processing multiple particants at one which could prevent overall processing from completing
- Fixed the two Group Stats Viewer windows crashing the application once opened when JSON_LOCATION was not set
- The Group Stats Viewer windows will now properly load the Right/Left or Front/Back fallback ROIs if JSON_LOCATION is not set
# Version 1.5.2 # Version 1.5.2
+46 -84
View File
@@ -2650,7 +2650,6 @@ def run_roi_second_level_analysis(
correction_method: str | None = "fdr_bh", correction_method: str | None = "fdr_bh",
target_chroma: str = "hbo", target_chroma: str = "hbo",
graph_bounds: float | None = None, graph_bounds: float | None = None,
roi_config: str | Path | None = None,
threshold_topo: bool = False, threshold_topo: bool = False,
) -> DataFrame: ) -> DataFrame:
@@ -2805,35 +2804,13 @@ def run_roi_second_level_analysis(
con_model_df = statsmodels_to_results(con_model, order=raw_picked.ch_names) con_model_df = statsmodels_to_results(con_model, order=raw_picked.ch_names)
# --- DYNAMIC ROI PARSING --- # --- DYNAMIC ROI PARSING ---
roi_mapping = {} if 'ROI' not in con_summary.columns or con_summary['ROI'].dropna().empty:
if roi_config is not None: if df_roi_all is not None and 'ROI' in df_roi_all.columns and ch_col in df_roi_all.columns:
raw_json = None # Create channel -> ROI mapping from df_roi_all
if isinstance(roi_config, str) and os.path.exists(roi_config): ch_to_roi = df_roi_all.dropna(subset=['ROI', ch_col]).set_index(ch_col)['ROI'].to_dict()
with open(roi_config, 'r') as f: con_summary['ROI'] = con_summary[ch_col].apply(
raw_json = json.load(f) lambda x: ch_to_roi.get(x, ch_to_roi.get(x.split()[0], None) if isinstance(x, str) else None)
elif isinstance(roi_config, dict): )
raw_json = roi_config
if raw_json:
if "regions_of_interest" in raw_json:
for roi_item in raw_json["regions_of_interest"]:
roi_name = roi_item.get("name")
channels = roi_item.get("channels", [])
if roi_name and channels:
roi_mapping[roi_name] = channels
else:
roi_mapping = raw_json
if roi_mapping:
ch_to_roi = {}
for roi_name, channels in roi_mapping.items():
for ch in channels:
ch_to_roi[ch] = roi_name
ch_to_roi[ch.split()[0]] = roi_name
con_summary['ROI'] = con_summary[ch_col].apply(
lambda x: ch_to_roi.get(x, ch_to_roi.get(x.split()[0], None) if isinstance(x, str) else None)
)
unique_rois = [] unique_rois = []
if 'ROI' in con_summary.columns: if 'ROI' in con_summary.columns:
@@ -2925,7 +2902,7 @@ def run_cross_group_second_level_analysis(
target_chroma: str = "hbo", target_chroma: str = "hbo",
selected_event: str | None = None, selected_event: str | None = None,
graph_bounds: tuple[float, float] | list[float] | None = None, graph_bounds: tuple[float, float] | list[float] | None = None,
roi_config: Path | str | None = None, roi_channel_maps: dict[str, dict[str, str]] | None = None,
threshold_topo: bool = False, threshold_topo: bool = False,
) -> DataFrame: ) -> DataFrame:
@@ -3129,21 +3106,13 @@ def run_cross_group_second_level_analysis(
con_model_df = pd.DataFrame(contrast_data) con_model_df = pd.DataFrame(contrast_data)
# --- DYNAMIC ROI PARSING --- # --- DYNAMIC ROI PARSING ---
roi_mapping = {} if roi_channel_maps:
if roi_config is not None and os.path.exists(roi_config): def _lookup_roi(row):
with open(roi_config, 'r') as f: m = roi_channel_maps.get(row['clean_ID'], {})
raw_json = json.load(f) ch = row[ch_col]
if "regions_of_interest" in raw_json: return m.get(ch, m.get(ch.split()[0]) if isinstance(ch, str) else None)
for roi_item in raw_json["regions_of_interest"]:
roi_mapping[roi_item.get("name")] = roi_item.get("channels", [])
if roi_mapping: con_summary['ROI'] = con_summary.apply(_lookup_roi, axis=1)
ch_to_roi = {}
for roi_name, channels in roi_mapping.items():
for ch in channels:
ch_to_roi[ch] = roi_name
ch_to_roi[ch.split()[0]] = roi_name
con_summary['ROI'] = con_summary[ch_col].apply(lambda x: ch_to_roi.get(x, ch_to_roi.get(x.split()[0], None) if isinstance(x, str) else None))
unique_rois = [r for r in con_summary['ROI'].dropna().unique() if r != ""] if 'ROI' in con_summary.columns else ['All_Channels'] unique_rois = [r for r in con_summary['ROI'].dropna().unique() if r != ""] if 'ROI' in con_summary.columns else ['All_Channels']
@@ -3454,7 +3423,8 @@ def run_cross_group_contrast_analysis(
df_contrasts_a: DataFrame, df_contrasts_a: DataFrame,
df_contrasts_b: DataFrame, df_contrasts_b: DataFrame,
contrast_name: str, contrast_name: str,
roi_json_path: str | Path | None, roi_channel_maps_a: dict[str, dict[str, str]],
roi_channel_maps_b: dict[str, dict[str, str]],
group_a_name: str = "Group A", group_a_name: str = "Group A",
group_b_name: str = "Group B", group_b_name: str = "Group B",
target_chroma: str = "hbo", target_chroma: str = "hbo",
@@ -3547,8 +3517,8 @@ def run_cross_group_contrast_analysis(
print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_b_name}'s data.") print(f"[ERROR] Contrast '{contrast_name}' not found anywhere in {group_b_name}'s data.")
return DataFrame() return DataFrame()
roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_json_path, weighted=weighted) roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_channel_maps_a, weighted=weighted)
roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_json_path, weighted=weighted) roi_b = aggregate_channel_contrasts_to_roi(df_b_filt, roi_channel_maps_b, weighted=weighted)
roi_a = roi_a[roi_a['Chroma'] == target_chroma] roi_a = roi_a[roi_a['Chroma'] == target_chroma]
roi_b = roi_b[roi_b['Chroma'] == target_chroma] roi_b = roi_b[roi_b['Chroma'] == target_chroma]
@@ -3897,7 +3867,7 @@ def run_roi_paired_contrast_analysis(
def aggregate_channel_contrasts_to_roi( def aggregate_channel_contrasts_to_roi(
df_contrasts: DataFrame, df_contrasts: DataFrame,
roi_json_path: str | Path | None, roi_channel_maps: dict[str, dict[str, str]],
weighted: bool = True weighted: bool = True
) -> DataFrame: ) -> DataFrame:
""" """
@@ -3924,11 +3894,14 @@ def aggregate_channel_contrasts_to_roi(
['ch_name', 'effect', 'stat', 'Chroma', 'contrast_name', 'ID'] ['ch_name', 'effect', 'stat', 'Chroma', 'contrast_name', 'ID']
`stat` must be the t-statistic (ContrastType == 't'), since standard `stat` must be the t-statistic (ContrastType == 't'), since standard
error is recovered as effect / stat. error is recovered as effect / stat.
roi_json_path : str roi_channel_maps : dict[str, dict[str, str]]
Path to the same regions.json used elsewhere in the pipeline, with Per-subject channel-to-ROI mapping, keyed by subject ID (the same
the structure: {"regions_of_interest": [{"name": ..., "channels": [...]}]} ID values used in df_contrasts['ID']), e.g.
`channels` entries should be bare source-detector names (e.g. "S1_D1"), {"sub-01": {"S1_D1 hbo": "Left", "S1_D1 hbr": "Left", ...}, ...}.
matching the convention already used for the GLM-level ROI loading. This is the actual mapping generate_roi_results used for that
subject (whichever tier produced it — JSON, geometric split, or
per-channel fallback) — not re-derived here, so ROI assignments
stay consistent with df_ind_dict for the same subject.
weighted : bool, default True weighted : bool, default True
If True, combine channels within an ROI using inverse-variance If True, combine channels within an ROI using inverse-variance
weighting (weight = 1 / se^2), matching MNE-NIRS's own default weighting (weight = 1 / se^2), matching MNE-NIRS's own default
@@ -3948,35 +3921,18 @@ def aggregate_channel_contrasts_to_roi(
if not all(col in df_contrasts.columns for col in required_cols): if not all(col in df_contrasts.columns for col in required_cols):
raise ValueError(f"Input contrast DataFrame must include: {required_cols}") raise ValueError(f"Input contrast DataFrame must include: {required_cols}")
# --- Load ROI definitions and build a channel-base -> ROI lookup ---
# Channel base names (e.g. "S1_D1") map to both hbo/hbr rows via the
# ch_name column ("S1_D1 hbo" / "S1_D1 hbr"), so we key on the base name.
with open(roi_json_path, 'r') as f:
roi_data = json.load(f)
ch_base_to_roi = {}
for region in roi_data.get("regions_of_interest", []):
roi_name = region["name"]
for ch_base in region["channels"]:
if ch_base in ch_base_to_roi:
logger.warning(
f"Channel '{ch_base}' assigned to multiple ROIs "
f"('{ch_base_to_roi[ch_base]}' and '{roi_name}') — "
f"using '{roi_name}' (last one wins)."
)
ch_base_to_roi[ch_base] = roi_name
df = df_contrasts.copy() df = df_contrasts.copy()
df['ch_base'] = df['ch_name'].str.split().str[0] # "S1_D1 hbo" -> "S1_D1" df['ch_base'] = df['ch_name'].str.split().str[0]
df['ROI'] = df['ch_base'].map(ch_base_to_roi)
def lookup(row):
n_unassigned = df['ROI'].isna().sum() m = roi_channel_maps.get(row['ID'], {})
if n_unassigned: return m.get(row['ch_name'], m.get(row['ch_base']))
logger.warning(
f"{n_unassigned} channel-rows did not match any ROI in " df['ROI'] = df.apply(lookup, axis=1)
f"'{roi_json_path}' and will be excluded."
)
df = df.dropna(subset=['ROI']) df = df.dropna(subset=['ROI'])
if df.empty:
raise ValueError("No channel contrasts matched any subject's ROI mapping.")
# Recover standard error from the t-statistic: t = effect / se -> se = effect / t # Recover standard error from the t-statistic: t = effect / se -> se = effect / t
with np.errstate(divide='ignore', invalid='ignore'): with np.errstate(divide='ignore', invalid='ignore'):
@@ -5029,6 +4985,12 @@ def generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_l
subject_id = sub['ID'].iloc[0] if 'ID' in sub.columns else '' subject_id = sub['ID'].iloc[0] if 'ID' in sub.columns else ''
n_conditions = sub['Condition'].nunique() n_conditions = sub['Condition'].nunique()
roi_channel_map = {
raw_haemo.ch_names[idx]: roi_name
for roi_name, indices in rois_formatted.items()
for idx in indices
}
sns.set_theme(style="whitegrid") sns.set_theme(style="whitegrid")
fig, ax = plt.subplots(figsize=(max(6, 1.5 * sub['ROI'].nunique()), 5)) fig, ax = plt.subplots(figsize=(max(6, 1.5 * sub['ROI'].nunique()), 5))
@@ -5048,7 +5010,7 @@ def generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_l
plt.tight_layout() plt.tight_layout()
plt.close(fig) plt.close(fig)
return df_roi, fig return df_roi, roi_channel_map, fig
@@ -5495,7 +5457,7 @@ def process_participant(file_path, file_start, progress_callback=None):
step_start = lap(step_start, timings, "Step 25") step_start = lap(step_start, timings, "Step 25")
# Step 26: Generate Region of Interest Results # Step 26: Generate Region of Interest Results
df_roi, fig_roi = generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_location=JSON_LOCATION) df_roi, roi_channel_map, fig_roi = generate_roi_results(raw_haemo, df_design_matrix, glm_est, file_path, json_location=JSON_LOCATION)
_enqueue("Region of Interest", fig_roi, png_queue) _enqueue("Region of Interest", fig_roi, png_queue)
if progress_callback: progress_callback(26) if progress_callback: progress_callback(26)
logger.info("26") logger.info("26")
@@ -5523,7 +5485,7 @@ def process_participant(file_path, file_start, progress_callback=None):
logger.info(f" {name:<25} {elapsed:7.3f}s") logger.info(f" {name:<25} {elapsed:7.3f}s")
logger.info(f"Total processing time: {sum(timings.values()):.3f}s") logger.info(f"Total processing time: {sum(timings.values()):.3f}s")
return raw_haemo, epochs, df_cha, df_roi, df_design_matrix, config_dict, fig_bytes_dict, contrast_results_dict, True return raw_haemo, epochs, df_cha, df_roi, df_design_matrix, config_dict, fig_bytes_dict, contrast_results_dict, roi_channel_map, True
+2 -4
View File
@@ -329,6 +329,7 @@ DATA_SCHEMA = [
{"key": "config_dict", "help": "Dict[file_path, dict]: Processing configuration parameters"}, {"key": "config_dict", "help": "Dict[file_path, dict]: Processing configuration parameters"},
{"key": "fig_bytes_dict", "help": "Dict[file_path, dict]: Serialized figure data"}, {"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": "contrast_results_dict", "help": "Dict[file_path, dict]: Calculated contrast statistical results"},
{"key": "roi_channel_map_dict", "help": "Dict[file_path, dict]: Calculated contrast statistical results"},
{"key": "valid_dict", "help": "Dict[file_path, bool]: Boolean validity status per file"} {"key": "valid_dict", "help": "Dict[file_path, bool]: Boolean validity status per file"}
] ]
@@ -677,7 +678,6 @@ class MainApplication(QMainWindow):
self.analysis_clearing_bypass = False self.analysis_clearing_bypass = False
self.folding_bypass = False self.folding_bypass = False
self.advanced_parameters = False self.advanced_parameters = False
self.json_location = ""
# Initialization to ensure that saving can occur # Initialization to ensure that saving can occur
@@ -1268,8 +1268,8 @@ class MainApplication(QMainWindow):
data_map["config_dict"], data_map["config_dict"],
data_map["fig_bytes_dict"], data_map["fig_bytes_dict"],
data_map["contrast_results_dict"], data_map["contrast_results_dict"],
data_map["roi_channel_map_dict"],
self.folding_bypass, self.folding_bypass,
self.json_location
] ]
self.launcher_window = ViewerLauncherWidget(*args) self.launcher_window = ViewerLauncherWidget(*args)
@@ -2349,8 +2349,6 @@ class MainApplication(QMainWindow):
if self.folding_bypass: if self.folding_bypass:
all_params['FOLDING_BYP'] = True all_params['FOLDING_BYP'] = True
self.json_location = all_params['JSON_LOCATION']
collected_data = { collected_data = {
"SNIRF_FILES": snirf_files, "SNIRF_FILES": snirf_files,
"PARAMS": all_params, # add this line "PARAMS": all_params, # add this line
+28 -7
View File
@@ -152,8 +152,8 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
df_ind_dict: dict[str, DataFrame], df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame], design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]], contrast_results_dict: dict[str, dict[str, Any]],
roi_channel_map_dict: dict[str, dict[str, str]],
group_dict: dict[str, str], group_dict: dict[str, str],
json_location: str | Path
) -> None: ) -> None:
super().__init__("CrossGroupStats") super().__init__("CrossGroupStats")
@@ -163,14 +163,14 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
self.df_ind_dict = df_ind_dict self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict self.contrast_results_dict = contrast_results_dict
# self.group_dict = group_dict self.roi_channel_map_dict = roi_channel_map_dict
self.json_location = json_location self.group_dict = group_dict
self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION) self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION)
def process_request(self): def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results_dict) request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.df_ind_dict, self.contrast_results_dict)
if request is None: if request is None:
return return
@@ -200,6 +200,12 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
target_chroma = params.get("target_chroma", "hbo") target_chroma = params.get("target_chroma", "hbo")
threshold_topo = params.get("threshold_topo", False) threshold_topo = params.get("threshold_topo", False)
selected_roi_maps = {
fp: self.roi_channel_map_dict[fp]
for fp in (file_paths_a + file_paths_b)
if fp in self.roi_channel_map_dict
}
run_cross_group_second_level_analysis( run_cross_group_second_level_analysis(
df_roi_all=df_ind_combined, # Individual stats dataframe df_roi_all=df_ind_combined, # Individual stats dataframe
file_paths_a=file_paths_a, file_paths_a=file_paths_a,
@@ -213,7 +219,7 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
correction_method=correction_method, correction_method=correction_method,
target_chroma=target_chroma, target_chroma=target_chroma,
selected_event=selected_event, selected_event=selected_event,
roi_config=self.json_location, roi_channel_maps=selected_roi_maps,
threshold_topo=threshold_topo # Shows the raw difference map (Unthresholded) threshold_topo=threshold_topo # Shows the raw difference map (Unthresholded)
) )
elif idx == 1: elif idx == 1:
@@ -317,12 +323,27 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
if df_contrasts_a.empty or df_contrasts_b.empty: if df_contrasts_a.empty or df_contrasts_b.empty:
print("No contrast data found for one or both groups.") print("No contrast data found for one or both groups.")
continue continue
roi_maps_a = {
fp: self.roi_channel_map_dict[fp]
for fp in file_paths_a
if fp in self.roi_channel_map_dict
}
roi_maps_b = {
fp: self.roi_channel_map_dict[fp]
for fp in file_paths_b
if fp in self.roi_channel_map_dict
}
if not roi_maps_a or not roi_maps_b:
print("No channel-to-ROI mapping available for one or both groups.")
continue
run_cross_group_contrast_analysis( run_cross_group_contrast_analysis(
df_contrasts_a=df_contrasts_a, df_contrasts_a=df_contrasts_a,
df_contrasts_b=df_contrasts_b, df_contrasts_b=df_contrasts_b,
contrast_name=contrast_name, contrast_name=contrast_name,
roi_json_path=self.json_location, roi_channel_maps_a=roi_maps_a,
roi_channel_maps_b=roi_maps_b,
group_a_name=self.group_a_dropdown.currentText(), group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(), group_b_name=self.group_b_dropdown.currentText(),
target_chroma=target_chroma, target_chroma=target_chroma,
+14 -6
View File
@@ -165,8 +165,8 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
df_ind_dict: dict[str, DataFrame], df_ind_dict: dict[str, DataFrame],
design_matrix_dict: dict[str, DataFrame], design_matrix_dict: dict[str, DataFrame],
contrast_results_dict: dict[str, dict[str, Any]], contrast_results_dict: dict[str, dict[str, Any]],
roi_channel_map_dict: dict[str, dict[str, str]],
group_dict: dict[str, str], group_dict: dict[str, str],
json_location: str | Path
) -> None: ) -> None:
super().__init__("InterGroupStats") super().__init__("InterGroupStats")
@@ -176,14 +176,14 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
self.df_ind_dict = df_ind_dict self.df_ind_dict = df_ind_dict
self.design_matrix_dict = design_matrix_dict self.design_matrix_dict = design_matrix_dict
self.contrast_results_dict = contrast_results_dict self.contrast_results_dict = contrast_results_dict
self.roi_channel_map_dict = roi_channel_map_dict
self.group_dict = group_dict self.group_dict = group_dict
self.json_location = json_location
self.setup_inter_group_ui(["0 (ROI vs. Zero)", "1 (Paired ROI Contrast)", "2 (Joint Contrast, ROI-Aggregated)"], placeholder_text=DESCRIPTION) self.setup_inter_group_ui(["0 (ROI vs. Zero)", "1 (Paired ROI Contrast)", "2 (Joint Contrast, ROI-Aggregated)"], placeholder_text=DESCRIPTION)
def process_request(self): def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.json_location, self.contrast_results_dict) request = self.get_common_request_data(PARAMETERIZED_INDEXES, self.df_ind_dict, self.contrast_results_dict)
if request is None: if request is None:
return return
@@ -276,7 +276,6 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
correction_method=correction_method, correction_method=correction_method,
target_chroma=target_chroma, target_chroma=target_chroma,
graph_bounds=graph_bounds if graph_bounds > 0.0 else None, graph_bounds=graph_bounds if graph_bounds > 0.0 else None,
roi_config=self.json_location
) )
elif idx == 1: elif idx == 1:
@@ -358,11 +357,20 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
continue continue
df_contrasts = pd.concat(all_contrasts, ignore_index=True) df_contrasts = pd.concat(all_contrasts, ignore_index=True)
selected_roi_maps = {
fp: self.roi_channel_map_dict[fp]
for fp in selected_file_paths
if fp in self.roi_channel_map_dict
}
if not selected_roi_maps:
print("No channel-to-ROI mapping available for selected participants.")
continue
try: try:
roi_theta = aggregate_channel_contrasts_to_roi( roi_theta = aggregate_channel_contrasts_to_roi(
df_contrasts, df_contrasts,
roi_json_path=self.json_location, roi_channel_maps=selected_roi_maps,
weighted=weighted, weighted=weighted,
) )
+24 -30
View File
@@ -11,6 +11,8 @@ import json
from pathlib import Path from pathlib import Path
from typing import Sequence, Any from typing import Sequence, Any
import pandas as pd
from pandas import DataFrame
from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListView, QMessageBox, QPushButton, QScrollArea, 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, QSize, Qt from PySide6.QtCore import QEvent, QSize, Qt
@@ -1380,7 +1382,7 @@ class CrossGroupUIMixin:
def get_common_request_data( def get_common_request_data(
self, self,
parameterized_indexes: dict[int, list[dict[str, Any]]], parameterized_indexes: dict[int, list[dict[str, Any]]],
json_location: str | Path | None = None, df_ind_dict: dict[str, DataFrame] | None = None,
contrast_dfs: dict[str, dict[str, Any]] | None = None, contrast_dfs: dict[str, dict[str, Any]] | None = None,
) -> tuple[str | None, list[str], list[str], list[str], list[int], dict[str, Any]] | None: ) -> tuple[str | None, list[str], list[str], list[str], list[int], dict[str, Any]] | None:
@@ -1423,19 +1425,15 @@ class CrossGroupUIMixin:
dynamic_rois = [] dynamic_rois = []
# 1. Check for the JSON file and parse ROI names if df_ind_dict:
if os.path.exists(json_location): roi_set = set()
try: for fp in all_selected_paths:
with open(json_location, 'r', encoding='utf-8') as f: df_roi = df_ind_dict.get(fp)
regions_data = json.load(f) if isinstance(df_roi, pd.DataFrame) and "ROI" in df_roi.columns:
roi_set.update(df_roi["ROI"].dropna().unique())
# Extract "name" from each region under "regions_of_interest"
regions_list = regions_data.get("regions_of_interest", []) if roi_set:
dynamic_rois = [region["name"] for region in regions_list if "name" in region] dynamic_rois = sorted(list(roi_set))
except Exception as e:
# Safe log if JSON is corrupted or unreadable
print(f"Error reading ROI configurations from {json_location}: {e}")
# Fallback to prevent UI crashes if JSON file doesn't exist or is empty # Fallback to prevent UI crashes if JSON file doesn't exist or is empty
if not dynamic_rois: if not dynamic_rois:
@@ -1580,7 +1578,7 @@ class InterGroupUIMixin:
self.layout.addLayout(self.top_bar) self.layout.addLayout(self.top_bar)
self.group_to_paths = {} self.group_to_paths = {}
for file_path, group_name in self.group.items(): for file_path, group_name in self.group_dict.items():
self.group_to_paths.setdefault(group_name, []).append(file_path) self.group_to_paths.setdefault(group_name, []).append(file_path)
self.group_names = sorted(self.group_to_paths.keys()) self.group_names = sorted(self.group_to_paths.keys())
@@ -1632,7 +1630,7 @@ class InterGroupUIMixin:
def get_common_request_data( def get_common_request_data(
self, self,
parameterized_indexes: dict[int, list[dict[str, Any]]], parameterized_indexes: dict[int, list[dict[str, Any]]],
json_location: str | Path | None = None, df_ind_dict: dict[str, DataFrame] | None = None,
contrast_dfs: dict[str, dict[str, Any]] | None = None, contrast_dfs: dict[str, dict[str, Any]] | None = None,
) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None: ) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None:
@@ -1679,20 +1677,16 @@ class InterGroupUIMixin:
dynamic_rois = [] dynamic_rois = []
# 1. Check for the JSON file and parse ROI names if df_ind_dict:
if json_location is not None and os.path.exists(json_location): roi_set = set()
try: for fp in selected_file_paths:
with open(json_location, 'r', encoding='utf-8') as f: df_roi = df_ind_dict.get(fp)
regions_data = json.load(f) if isinstance(df_roi, pd.DataFrame) and "ROI" in df_roi.columns:
roi_set.update(df_roi["ROI"].dropna().unique())
# Extract "name" from each region under "regions_of_interest"
regions_list = regions_data.get("regions_of_interest", []) if roi_set:
dynamic_rois = [region["name"] for region in regions_list if "name" in region] dynamic_rois = sorted(list(roi_set))
except Exception as e:
# Safe log if JSON is corrupted or unreadable
print(f"Error reading ROI configurations from {json_location}: {e}")
# Fallback to prevent UI crashes if JSON file doesn't exist or is empty # Fallback to prevent UI crashes if JSON file doesn't exist or is empty
if not dynamic_rois: if not dynamic_rois:
dynamic_rois = ["Option 1", "Option 2"] dynamic_rois = ["Option 1", "Option 2"]
+3 -3
View File
@@ -24,7 +24,7 @@ from src.shared.shareddata import APP_NAME
class ViewerLauncherWidget(QWidget): class ViewerLauncherWidget(QWidget):
def __init__(self, haemo_dict, epochs_dict, cha_dict, df_ind_dict, design_matrix_dict, config_dict, fig_bytes_dict, contrast_results_dict, folding_bypass, json_location): def __init__(self, haemo_dict, epochs_dict, cha_dict, df_ind_dict, design_matrix_dict, config_dict, fig_bytes_dict, contrast_results_dict, roi_channel_map_dict, folding_bypass):
super().__init__() super().__init__()
self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}") self.setWindowTitle(f"Viewer Launcher - {APP_NAME.upper()}")
@@ -36,8 +36,8 @@ class ViewerLauncherWidget(QWidget):
("Participant Fold Channels Viewer", ParticipantFoldChannelsWidget, [haemo_dict, cha_dict], False), ("Participant Fold Channels Viewer", ParticipantFoldChannelsWidget, [haemo_dict, cha_dict], False),
("Participant Functional Connectivity Viewer [BETA]", ParticipantFunctionalConnectivityWidget, [haemo_dict, epochs_dict], True), ("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 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, json_location], True), ("Inter-Group Stats Viewer", InterGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, roi_channel_map_dict, group_dict], True),
("Cross-Group Stats Viewer", CrossGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, json_location], True), ("Cross-Group Stats Viewer", CrossGroupStatsWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, roi_channel_map_dict, group_dict], True),
("Inter-Group Brain and Image Viewer", InterGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True), ("Inter-Group Brain and Image Viewer", InterGroupBrainImageWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True),
("Cross-Group Brain and Image Viewer", CrossGroupBrainImageWidget, [haemo_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict], True), ("Cross-Group Brain and 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, contrast_results_dict, group_dict, config_dict], True) ("Export To CSV Viewer", ExportToCSVWidget, [haemo_dict, cha_dict, df_ind_dict, design_matrix_dict, contrast_results_dict, group_dict, config_dict], True)