functional connectivity, pylance, and other improvements
This commit is contained in:
@@ -14,70 +14,122 @@ 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
|
||||
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: [
|
||||
{
|
||||
"key": "n_lines",
|
||||
"label": "<Description>",
|
||||
"default": "20",
|
||||
"type": int,
|
||||
},
|
||||
{
|
||||
"key": "vmin",
|
||||
"label": "<Description>",
|
||||
"default": "0.9",
|
||||
"type": float,
|
||||
},
|
||||
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],
|
||||
config_dict: dict[str, dict[str, Any]]
|
||||
) -> None:
|
||||
|
||||
super().__init__("IntraGroupFunctionalConnectivity")
|
||||
self.setWindowTitle(f"Intra-Group Functional Connectivity Viewer [BETA] - {APP_NAME.upper()}")
|
||||
self.haemo_dict = haemo_dict
|
||||
#self.group_dict = group_dict
|
||||
self.config_dict = config_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 development and the results should currently be taken with a grain of salt. "
|
||||
"By clicking OK, you accept that the images generated may not be factual.")
|
||||
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 (Betas)",])
|
||||
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
|
||||
|
||||
(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:
|
||||
if idx == 0:
|
||||
params = param_values.get(idx, {})
|
||||
n_lines = params.get("n_lines", None)
|
||||
vmin = params.get("vmin", None)
|
||||
|
||||
if n_lines is None or vmin is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
run_group_functional_connectivity(self.haemo_dict, self.config_dict, selected_file_paths, selected_event, 50, 0.5)
|
||||
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}")
|
||||
Reference in New Issue
Block a user