fix to stats when no json file is defined
This commit is contained in:
+5
-1
@@ -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
|
||||||
|
|||||||
@@ -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,36 +3921,19 @@ 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)
|
|
||||||
|
|
||||||
n_unassigned = df['ROI'].isna().sum()
|
def lookup(row):
|
||||||
if n_unassigned:
|
m = roi_channel_maps.get(row['ID'], {})
|
||||||
logger.warning(
|
return m.get(row['ch_name'], m.get(row['ch_base']))
|
||||||
f"{n_unassigned} channel-rows did not match any ROI in "
|
|
||||||
f"'{roi_json_path}' and will be excluded."
|
df['ROI'] = df.apply(lookup, axis=1)
|
||||||
)
|
|
||||||
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'):
|
||||||
df['se'] = df['effect'] / df['stat']
|
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 ''
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -318,11 +324,26 @@ class CrossGroupStatsWidget(CrossGroupUIMixin, FlaresBaseWidget):
|
|||||||
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,
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -359,10 +358,19 @@ class InterGroupStatsWidget(InterGroupUIMixin, FlaresBaseWidget):
|
|||||||
|
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
if roi_set:
|
||||||
regions_list = regions_data.get("regions_of_interest", [])
|
dynamic_rois = sorted(list(roi_set))
|
||||||
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}")
|
|
||||||
|
|
||||||
# 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,19 +1677,15 @@ 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"
|
if roi_set:
|
||||||
regions_list = regions_data.get("regions_of_interest", [])
|
dynamic_rois = sorted(list(roi_set))
|
||||||
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}")
|
|
||||||
|
|
||||||
# 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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user