Files
flares/src/analysis/intragroupfunctionalconnectivity.py
T

135 lines
8.7 KiB
Python

"""
Filename: intragroupfunctionalconnectivity.py
Description: Logic for the Intra-Group Functional Connectivity analysis window
Note: Compliant with pylance strict type checking
Author: Tyler de Zeeuw
License: GPL-3.0
"""
# Built-in imports
from pathlib import Path
from typing import Any, cast
# External library imports
from PySide6.QtWidgets import QMessageBox
from mne import Epochs
from mne.io.base import BaseRaw
from flares import run_group_functional_connectivity_betas, run_group_functional_connectivity_epochs
from src.shared.flaresbasewidget import IntraGroupUIMixin, FlaresBaseWidget
from src.shared.shareddata import APP_NAME
PARAMETERIZED_INDEXES: dict[int, list[dict[str, Any]]] = {
0: [ # Beta-Series Correlation
{"key": "n_lines", "label": "Number of strongest connections to draw", "default": "20", "type": int},
{"key": "vmin", "label": "Minimum |r| to display (group average)", "default": "0.5", "type": float},
{"key": "drift_model", "label": "Drift model", "default": "cosine", "type": list, "options": ["cosine", "polynomial"]},
{"key": "drift_order", "label": "Drift order", "default": "1", "type": int},
{"key": "hrf_model", "label": "HRF model", "default": "glover", "type": list, "options": ["glover", "spm", "fir"]},
{"key": "apply_gsr", "label": "Apply Global Signal Regression", "default": "True", "type": bool},
{"key": "resample_freq", "label": "Resample rate before GLM fit (Hz) - lower is much faster", "default": "4.0", "type": float},
{"key": "alpha", "label": "FDR significance threshold (group-level)", "default": "0.05", "type": float},
{"key": "min_participants", "label": "Minimum participants required to run the group test", "default": "3", "type": int},
],
1: [ # Spectral Coherence
{"key": "method", "label": "Connectivity method", "default": "wpli2_debiased", "type": list, "options": ["coh", "pli", "wpli2_debiased"]},
{"key": "n_lines", "label": "Number of strongest connections to draw", "default": "20", "type": int},
{"key": "vmin", "label": "Minimum |r| to display (group average)", "default": "0.5", "type": float},
{"key": "fmin", "label": "Lower frequency bound (Hz)", "default": "0.04", "type": float},
{"key": "fmax", "label": "Upper frequency bound (Hz)", "default": "0.2", "type": float},
{"key": "alpha", "label": "FDR significance threshold (group-level)", "default": "0.05", "type": float},
{"key": "min_participants", "label": "Minimum participants required to run the group test", "default": "3", "type": int},
],
}
DESCRIPTION = """0. Beta-Series Correlation (run_group_functional_connectivity_betas)
\nFor each selected participant, resamples to resample_freq (default 4 Hz - well above what's needed to resolve trial-level GLM amplitudes, but far cheaper than running the fit at full acquisition rate) and computes trial-level GLM betas per channel, correlating them within-subject WITHOUT thresholding at the individual level. Those raw per-subject correlation matrices are Fisher-Z transformed and combined across the group using a one-sample t-test (against zero) per channel pair, then FDR-corrected (q < alpha) across all pairs. A significant connection means the group, on average, shows consistent trial-evoked co-activation between two channels - not that every individual participant showed it.
\nRequires at least min_participants (default 3, more is stronger) participants with usable data - each needs enough trials of the selected event to compute their own beta series. Participants with channel sets that don't overlap with the rest of the group are excluded from the shared channel set before analysis.
\nWith a small number of participants and many channel pairs, FDR correction is often the limiting factor even when there's a real underlying effect - check the p-value histogram and top-pairs report generated alongside the main plot: a cluster of small (but not FDR-significant) p-values well below what's expected by chance suggests a real but underpowered effect, worth revisiting with more participants, rather than a true null result.
\n1. Spectral Coherence (run_group_functional_connectivity_epochs)
\nFor each selected participant, computes spectral connectivity between HbO channels using the selected method: coherence ('coh'), Phase Lag Index ('pli'), or debiased weighted PLI squared ('wpli2_debiased', default). PLI/wPLI-family methods discount zero-lag contributions to connectivity, making them substantially more robust to shared systemic/vascular signal (which tends to hit multiple channels near-simultaneously) than plain coherence - recommended over 'coh' unless you have a specific reason to want raw coherence. Raw per-subject matrices are combined across the group the same way as the Beta-Series method: Fisher-Z, one-sample t-test per channel pair, FDR correction.
\nfmin must satisfy at least 5 full oscillation cycles within your epoch length (epoch_duration x fmin >= 5) for a reliable estimate - if it doesn't, the analysis will refuse to run with an error stating the minimum viable fmin for your epoch length, rather than silently producing an unreliable result. Shorter epochs require a higher fmin, which moves you out of the classic 0.04-0.2 Hz "low-frequency oscillation" band used in longer resting-state recordings - this is a real trade-off in what the analysis measures, not just a technical constraint.
\nSame minimum-participant, channel-alignment, and underpowered-vs-null-result caveats apply as the Beta-Series method above.
"""
class IntraGroupFunctionalConnectivityWidget(IntraGroupUIMixin, FlaresBaseWidget):
def __init__(
self,
haemo_dict: dict[str | Path, BaseRaw],
epochs_dict: dict[str, Epochs],
group_dict: dict[str, str],
) -> None:
super().__init__("IntraGroupFunctionalConnectivity")
self.setWindowTitle(f"Intra-Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
self.haemo_dict = haemo_dict
self.epochs_dict = epochs_dict
self.group_dict = group_dict
QMessageBox.warning(self, f"Warning - {APP_NAME.upper()}", f"Functional Connectivity is still in beta. While the results are now almost finalized, the processing is slow and it WILL hang the application for HOURS.")
self.setup_intra_group_ui(["0 (Beta-Series Correlation)", "1 (Spectral Coherence)"], placeholder_text=DESCRIPTION)
def process_request(self):
request = self.get_common_request_data(PARAMETERIZED_INDEXES)
if request is None:
return
(selected_event, selected_file_paths, selected_indexes, raw_params) = request
param_values = cast(dict[int | str, dict[str, Any]], raw_params)
for idx in selected_indexes:
params = param_values.get(idx, {})
if idx == 0:
n_lines = params.get("n_lines", 20)
vmin = params.get("vmin", 0.5)
drift_model = params.get("drift_model", "cosine")
drift_order = params.get("drift_order", 1)
hrf_model = params.get("hrf_model", "glover")
apply_gsr = params.get("apply_gsr", True)
resample_freq = params.get("resample_freq", 4.0)
alpha = params.get("alpha", 0.05)
min_participants = params.get("min_participants", 3)
run_group_functional_connectivity_betas(
self.haemo_dict, selected_file_paths, selected_event, n_lines, vmin,
drift_model=drift_model,
drift_order=drift_order,
hrf_model=hrf_model,
apply_gsr=apply_gsr,
resample_freq=resample_freq,
alpha=alpha,
min_participants=min_participants,
)
elif idx == 1:
method = params.get("method", "wpli2_debiased")
n_lines = params.get("n_lines", 20)
vmin = params.get("vmin", 0.5)
fmin = params.get("fmin", 0.04)
fmax = params.get("fmax", 0.2)
alpha = params.get("alpha", 0.05)
min_participants = params.get("min_participants", 3)
run_group_functional_connectivity_epochs(
self.epochs_dict,
selected_file_paths,
event_name=selected_event,
n_lines=n_lines,
vmin=vmin,
fmin=fmin,
method=method,
fmax=fmax,
alpha=alpha,
min_participants=min_participants,
)
else:
print(f"No method defined for index {idx}")