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 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
+45 -83
View File
@@ -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,36 +3921,19 @@ 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)
df['ch_base'] = df['ch_name'].str.split().str[0]
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."
)
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'):
df['se'] = df['effect'] / df['stat']
@@ -5030,6 +4986,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
+2 -4
View File
@@ -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
+27 -6
View File
@@ -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:
@@ -318,11 +324,26 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
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,
+13 -5
View File
@@ -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:
@@ -359,10 +358,19 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
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,
)
+21 -27
View File
@@ -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)
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())
# 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 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,19 +1677,15 @@ 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)
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())
# 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 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:
+3 -3
View File
@@ -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)