From 7c456e31e72a877d685e3c6b4c491797d5c11f4a Mon Sep 17 00:00:00 2001 From: tyler Date: Mon, 3 Aug 2026 00:01:21 -0700 Subject: [PATCH] fix to stats when no json file is defined --- changelog.md | 6 +- flares.py | 130 +++++++++++--------------------- main.py | 6 +- src/analysis/crossgroupstats.py | 35 +++++++-- src/analysis/intergroupstats.py | 20 +++-- src/shared/flaresbasewidget.py | 54 ++++++------- src/window/viewerlauncher.py | 6 +- 7 files changed, 122 insertions(+), 135 deletions(-) diff --git a/changelog.md b/changelog.md index 5bebbdb..fd1a820 100644 --- a/changelog.md +++ b/changelog.md @@ -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 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 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 diff --git a/flares.py b/flares.py index 1c99138..e902141 100644 --- a/flares.py +++ b/flares.py @@ -2650,7 +2650,6 @@ def run_roi_second_level_analysis( correction_method: str | None = "fdr_bh", target_chroma: str = "hbo", graph_bounds: float | None = None, - roi_config: str | Path | None = None, threshold_topo: bool = False, ) -> DataFrame: @@ -2805,35 +2804,13 @@ def run_roi_second_level_analysis( con_model_df = statsmodels_to_results(con_model, order=raw_picked.ch_names) # --- DYNAMIC ROI PARSING --- - roi_mapping = {} - if roi_config is not None: - raw_json = None - if isinstance(roi_config, str) and os.path.exists(roi_config): - with open(roi_config, 'r') as f: - raw_json = json.load(f) - 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) - ) + if 'ROI' not in con_summary.columns or con_summary['ROI'].dropna().empty: + if df_roi_all is not None and 'ROI' in df_roi_all.columns and ch_col in df_roi_all.columns: + # Create channel -> ROI mapping from df_roi_all + ch_to_roi = df_roi_all.dropna(subset=['ROI', ch_col]).set_index(ch_col)['ROI'].to_dict() + 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 = [] if 'ROI' in con_summary.columns: @@ -2925,7 +2902,7 @@ def run_cross_group_second_level_analysis( target_chroma: str = "hbo", selected_event: str | 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, ) -> DataFrame: @@ -3129,21 +3106,13 @@ def run_cross_group_second_level_analysis( con_model_df = pd.DataFrame(contrast_data) # --- DYNAMIC ROI PARSING --- - roi_mapping = {} - if roi_config is not None and os.path.exists(roi_config): - with open(roi_config, 'r') as f: - raw_json = json.load(f) - if "regions_of_interest" in raw_json: - for roi_item in raw_json["regions_of_interest"]: - roi_mapping[roi_item.get("name")] = roi_item.get("channels", []) + if roi_channel_maps: + def _lookup_roi(row): + m = roi_channel_maps.get(row['clean_ID'], {}) + ch = row[ch_col] + return m.get(ch, m.get(ch.split()[0]) if isinstance(ch, str) else None) - 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)) + con_summary['ROI'] = con_summary.apply(_lookup_roi, axis=1) 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_b: DataFrame, 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_b_name: str = "Group B", 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.") return DataFrame() - roi_a = aggregate_channel_contrasts_to_roi(df_a_filt, roi_json_path, weighted=weighted) - roi_b = aggregate_channel_contrasts_to_roi(df_b_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_channel_maps_b, weighted=weighted) roi_a = roi_a[roi_a['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( df_contrasts: DataFrame, - roi_json_path: str | Path | None, + roi_channel_maps: dict[str, dict[str, str]], weighted: bool = True ) -> DataFrame: """ @@ -3924,11 +3894,14 @@ def aggregate_channel_contrasts_to_roi( ['ch_name', 'effect', 'stat', 'Chroma', 'contrast_name', 'ID'] `stat` must be the t-statistic (ContrastType == 't'), since standard error is recovered as effect / stat. - roi_json_path : str - Path to the same regions.json used elsewhere in the pipeline, with - the structure: {"regions_of_interest": [{"name": ..., "channels": [...]}]} - `channels` entries should be bare source-detector names (e.g. "S1_D1"), - matching the convention already used for the GLM-level ROI loading. + roi_channel_maps : dict[str, dict[str, str]] + Per-subject channel-to-ROI mapping, keyed by subject ID (the same + ID values used in df_contrasts['ID']), e.g. + {"sub-01": {"S1_D1 hbo": "Left", "S1_D1 hbr": "Left", ...}, ...}. + 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 If True, combine channels within an ROI using inverse-variance 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): 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['ch_base'] = df['ch_name'].str.split().str[0] # "S1_D1 hbo" -> "S1_D1" - df['ROI'] = df['ch_base'].map(ch_base_to_roi) - - n_unassigned = df['ROI'].isna().sum() - if n_unassigned: - logger.warning( - f"{n_unassigned} channel-rows did not match any ROI in " - f"'{roi_json_path}' and will be excluded." - ) + df['ch_base'] = df['ch_name'].str.split().str[0] + + def lookup(row): + m = roi_channel_maps.get(row['ID'], {}) + return m.get(row['ch_name'], m.get(row['ch_base'])) + + df['ROI'] = df.apply(lookup, axis=1) 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 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 '' 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") 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.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 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) if progress_callback: progress_callback(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"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 diff --git a/main.py b/main.py index 7b5acc6..369f4a1 100644 --- a/main.py +++ b/main.py @@ -329,6 +329,7 @@ DATA_SCHEMA = [ {"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": "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"} ] @@ -677,7 +678,6 @@ class MainApplication(QMainWindow): self.analysis_clearing_bypass = False self.folding_bypass = False self.advanced_parameters = False - self.json_location = "" # Initialization to ensure that saving can occur @@ -1268,8 +1268,8 @@ class MainApplication(QMainWindow): data_map["config_dict"], data_map["fig_bytes_dict"], data_map["contrast_results_dict"], + data_map["roi_channel_map_dict"], self.folding_bypass, - self.json_location ] self.launcher_window = ViewerLauncherWidget(*args) @@ -2349,8 +2349,6 @@ class MainApplication(QMainWindow): if self.folding_bypass: all_params['FOLDING_BYP'] = True - self.json_location = all_params['JSON_LOCATION'] - collected_data = { "SNIRF_FILES": snirf_files, "PARAMS": all_params, # add this line diff --git a/src/analysis/crossgroupstats.py b/src/analysis/crossgroupstats.py index 2cb7b9f..09d8371 100644 --- a/src/analysis/crossgroupstats.py +++ b/src/analysis/crossgroupstats.py @@ -152,8 +152,8 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget): df_ind_dict: dict[str, DataFrame], design_matrix_dict: dict[str, DataFrame], contrast_results_dict: dict[str, dict[str, Any]], + roi_channel_map_dict: dict[str, dict[str, str]], group_dict: dict[str, str], - json_location: str | Path ) -> None: super().__init__("CrossGroupStats") @@ -163,14 +163,14 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget): 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.json_location = json_location + self.roi_channel_map_dict = roi_channel_map_dict + self.group_dict = group_dict self.setup_cross_group_ui(["0 (Raw ROI Comparison)", "1 (Laterality Comparison)", "2 (Contrast Comparison)",], placeholder_text=DESCRIPTION) 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: return @@ -200,6 +200,12 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget): target_chroma = params.get("target_chroma", "hbo") 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( df_roi_all=df_ind_combined, # Individual stats dataframe file_paths_a=file_paths_a, @@ -213,7 +219,7 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget): correction_method=correction_method, target_chroma=target_chroma, 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) ) elif idx == 1: @@ -317,12 +323,27 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget): if df_contrasts_a.empty or df_contrasts_b.empty: print("No contrast data found for one or both groups.") 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( df_contrasts_a=df_contrasts_a, df_contrasts_b=df_contrasts_b, 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_b_name=self.group_b_dropdown.currentText(), target_chroma=target_chroma, diff --git a/src/analysis/intergroupstats.py b/src/analysis/intergroupstats.py index 6f101c5..f29aca9 100644 --- a/src/analysis/intergroupstats.py +++ b/src/analysis/intergroupstats.py @@ -165,8 +165,8 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget): df_ind_dict: dict[str, DataFrame], design_matrix_dict: dict[str, DataFrame], contrast_results_dict: dict[str, dict[str, Any]], + roi_channel_map_dict: dict[str, dict[str, str]], group_dict: dict[str, str], - json_location: str | Path ) -> None: super().__init__("InterGroupStats") @@ -176,14 +176,14 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget): self.df_ind_dict = df_ind_dict self.design_matrix_dict = design_matrix_dict self.contrast_results_dict = contrast_results_dict + self.roi_channel_map_dict = roi_channel_map_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) 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: return @@ -276,7 +276,6 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget): correction_method=correction_method, target_chroma=target_chroma, graph_bounds=graph_bounds if graph_bounds > 0.0 else None, - roi_config=self.json_location ) elif idx == 1: @@ -358,11 +357,20 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget): continue 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: roi_theta = aggregate_channel_contrasts_to_roi( df_contrasts, - roi_json_path=self.json_location, + roi_channel_maps=selected_roi_maps, weighted=weighted, ) diff --git a/src/shared/flaresbasewidget.py b/src/shared/flaresbasewidget.py index 1d3c1ee..36f491b 100644 --- a/src/shared/flaresbasewidget.py +++ b/src/shared/flaresbasewidget.py @@ -11,6 +11,8 @@ import json from pathlib import Path 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.QtGui import QStandardItemModel, QStandardItem, QPixmap, QIntValidator, QDoubleValidator from PySide6.QtCore import QEvent, QSize, Qt @@ -1380,7 +1382,7 @@ class CrossGroupUIMixin: def get_common_request_data( self, 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, ) -> tuple[str | None, list[str], list[str], list[str], list[int], dict[str, Any]] | None: @@ -1423,19 +1425,15 @@ class CrossGroupUIMixin: dynamic_rois = [] - # 1. Check for the JSON file and parse ROI names - if os.path.exists(json_location): - try: - with open(json_location, 'r', encoding='utf-8') as f: - regions_data = json.load(f) - - # Extract "name" from each region under "regions_of_interest" - regions_list = regions_data.get("regions_of_interest", []) - dynamic_rois = [region["name"] for region in regions_list if "name" in region] - - except Exception as e: - # Safe log if JSON is corrupted or unreadable - print(f"Error reading ROI configurations from {json_location}: {e}") + if df_ind_dict: + roi_set = set() + for fp in all_selected_paths: + df_roi = df_ind_dict.get(fp) + if isinstance(df_roi, pd.DataFrame) and "ROI" in df_roi.columns: + roi_set.update(df_roi["ROI"].dropna().unique()) + + if roi_set: + dynamic_rois = sorted(list(roi_set)) # Fallback to prevent UI crashes if JSON file doesn't exist or is empty if not dynamic_rois: @@ -1580,7 +1578,7 @@ class InterGroupUIMixin: self.layout.addLayout(self.top_bar) 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_names = sorted(self.group_to_paths.keys()) @@ -1632,7 +1630,7 @@ class InterGroupUIMixin: def get_common_request_data( self, 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, ) -> tuple[str | None, list[str], list[int], dict[str, Any]] | None: @@ -1679,20 +1677,16 @@ class InterGroupUIMixin: dynamic_rois = [] - # 1. Check for the JSON file and parse ROI names - if json_location is not None and os.path.exists(json_location): - try: - with open(json_location, 'r', encoding='utf-8') as f: - regions_data = json.load(f) - - # Extract "name" from each region under "regions_of_interest" - regions_list = regions_data.get("regions_of_interest", []) - dynamic_rois = [region["name"] for region in regions_list if "name" in region] - - except Exception as e: - # Safe log if JSON is corrupted or unreadable - print(f"Error reading ROI configurations from {json_location}: {e}") - + if df_ind_dict: + roi_set = set() + for fp in selected_file_paths: + df_roi = df_ind_dict.get(fp) + if isinstance(df_roi, pd.DataFrame) and "ROI" in df_roi.columns: + roi_set.update(df_roi["ROI"].dropna().unique()) + + if roi_set: + dynamic_rois = sorted(list(roi_set)) + # Fallback to prevent UI crashes if JSON file doesn't exist or is empty if not dynamic_rois: dynamic_rois = ["Option 1", "Option 2"] diff --git a/src/window/viewerlauncher.py b/src/window/viewerlauncher.py index c688eb3..98afbea 100644 --- a/src/window/viewerlauncher.py +++ b/src/window/viewerlauncher.py @@ -24,7 +24,7 @@ from src.shared.shareddata import APP_NAME class ViewerLauncherWidget(QWidget): - def __init__(self, haemo_dict, epochs_dict, cha_dict, df_ind_dict, design_matrix_dict, config_dict, fig_bytes_dict, contrast_results_dict, 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__() 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 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, json_location], True), - ("Cross-Group Stats Viewer", CrossGroupStatsWidget, [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, 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), ("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)