diff --git a/.spyproject/config/backups/codestyle.ini.bak b/.spyproject/config/backups/codestyle.ini.bak new file mode 100644 index 0000000..0f54b4c --- /dev/null +++ b/.spyproject/config/backups/codestyle.ini.bak @@ -0,0 +1,8 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + +[main] +version = 0.2.0 + diff --git a/.spyproject/config/backups/encoding.ini.bak b/.spyproject/config/backups/encoding.ini.bak new file mode 100644 index 0000000..a17aced --- /dev/null +++ b/.spyproject/config/backups/encoding.ini.bak @@ -0,0 +1,6 @@ +[encoding] +text_encoding = utf-8 + +[main] +version = 0.2.0 + diff --git a/.spyproject/config/backups/vcs.ini.bak b/.spyproject/config/backups/vcs.ini.bak new file mode 100644 index 0000000..fd66eae --- /dev/null +++ b/.spyproject/config/backups/vcs.ini.bak @@ -0,0 +1,7 @@ +[vcs] +use_version_control = False +version_control_system = + +[main] +version = 0.2.0 + diff --git a/.spyproject/config/backups/workspace.ini.bak b/.spyproject/config/backups/workspace.ini.bak new file mode 100644 index 0000000..e4c8489 --- /dev/null +++ b/.spyproject/config/backups/workspace.ini.bak @@ -0,0 +1,12 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False +project_type = 'empty-project-type' +recent_files = ['examples/dFC_methods_demo.py', '../test_comp.py'] + +[main] +version = 0.2.0 +recent_files = [] + diff --git a/.spyproject/config/codestyle.ini b/.spyproject/config/codestyle.ini new file mode 100644 index 0000000..0f54b4c --- /dev/null +++ b/.spyproject/config/codestyle.ini @@ -0,0 +1,8 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + +[main] +version = 0.2.0 + diff --git a/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini b/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini new file mode 100644 index 0000000..0b95e5c --- /dev/null +++ b/.spyproject/config/defaults/defaults-codestyle-0.2.0.ini @@ -0,0 +1,5 @@ +[codestyle] +indentation = True +edge_line = True +edge_line_columns = 79 + diff --git a/.spyproject/config/defaults/defaults-encoding-0.2.0.ini b/.spyproject/config/defaults/defaults-encoding-0.2.0.ini new file mode 100644 index 0000000..0ce193c --- /dev/null +++ b/.spyproject/config/defaults/defaults-encoding-0.2.0.ini @@ -0,0 +1,3 @@ +[encoding] +text_encoding = utf-8 + diff --git a/.spyproject/config/defaults/defaults-vcs-0.2.0.ini b/.spyproject/config/defaults/defaults-vcs-0.2.0.ini new file mode 100644 index 0000000..ee25483 --- /dev/null +++ b/.spyproject/config/defaults/defaults-vcs-0.2.0.ini @@ -0,0 +1,4 @@ +[vcs] +use_version_control = False +version_control_system = + diff --git a/.spyproject/config/defaults/defaults-workspace-0.2.0.ini b/.spyproject/config/defaults/defaults-workspace-0.2.0.ini new file mode 100644 index 0000000..2a73ab7 --- /dev/null +++ b/.spyproject/config/defaults/defaults-workspace-0.2.0.ini @@ -0,0 +1,6 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False + diff --git a/.spyproject/config/encoding.ini b/.spyproject/config/encoding.ini new file mode 100644 index 0000000..a17aced --- /dev/null +++ b/.spyproject/config/encoding.ini @@ -0,0 +1,6 @@ +[encoding] +text_encoding = utf-8 + +[main] +version = 0.2.0 + diff --git a/.spyproject/config/vcs.ini b/.spyproject/config/vcs.ini new file mode 100644 index 0000000..fd66eae --- /dev/null +++ b/.spyproject/config/vcs.ini @@ -0,0 +1,7 @@ +[vcs] +use_version_control = False +version_control_system = + +[main] +version = 0.2.0 + diff --git a/.spyproject/config/workspace.ini b/.spyproject/config/workspace.ini new file mode 100644 index 0000000..43f664a --- /dev/null +++ b/.spyproject/config/workspace.ini @@ -0,0 +1,12 @@ +[workspace] +restore_data_on_startup = True +save_data_on_exit = True +save_history = True +save_non_project_files = False +project_type = 'empty-project-type' +recent_files = ['../test_comp.py', 'examples/dFC_methods_demo.py', 'task_dFC/ML.py', 'pydfc/__init__.py', 'pydfc/dfc_utils.py', 'pydfc/ml_utils.py', 'pydfc/task_utils.py'] + +[main] +version = 0.2.0 +recent_files = [] + diff --git a/pydfc/dfc_methods/__init__.py b/pydfc/dfc_methods/__init__.py index 25c5164..30b88ba 100644 --- a/pydfc/dfc_methods/__init__.py +++ b/pydfc/dfc_methods/__init__.py @@ -62,6 +62,36 @@ from .time_reversal_asymmetry import TIME_REVERSAL_ASYMMETRY from .volatility_weighted import VOLATILITY_WEIGHTED from .windowless import WINDOWLESS +from .adaptive_dcc_random_hybrid_ensemble import ADAPTIVE_DCC_RANDOM_HYBRID_ENSEMBLE +from .adaptive_edge_kalman_hybrid import ADAPTIVE_EDGE_KALMAN_HYBRID +from .adaptive_quantum_reservoir_hybrid import ADAPTIVE_QUANTUM_RESERVOIR_HYBRID +from .adaptive_random_sparse_hybrid import ADAPTIVE_RANDOM_SPARSE_HYBRID +from .adaptive_robust_differential_hybrid_ensemble import ADAPTIVE_ROBUST_DIFFERENTIAL_HYBRID_ENSEMBLE +from .changepoint_multiscale_exp_hybrid_ensemble import CHANGEPOINT_MULTISCALE_EXP_HYBRID_ENSEMBLE +from .changepoint_robust_exp_hybrid import CHANGEPOINT_ROBUST_EXP_HYBRID +from .dcc_changepoint_sparse_hybrid import DCC_CHANGEPOINT_SPARSE_HYBRID +from .dcc_edge_exponential_hybrid import DCC_EDGE_EXPONENTIAL_HYBRID +from .dcc_kalman_volatility_hybrid_ensemble import DCC_KALMAN_VOLATILITY_HYBRID_ENSEMBLE +from .differential_edge_kalman_hybrid import DIFFERENTIAL_EDGE_KALMAN_HYBRID +from .edge_kalman_exp_hybrid_ensemble import EDGE_KALMAN_EXP_HYBRID_ENSEMBLE +from .edge_random_reservoir_hybrid_ensemble import EDGE_RANDOM_RESERVOIR_HYBRID_ENSEMBLE +from .edge_stft_quantum_hybrid import EDGE_STFT_QUANTUM_HYBRID +from .kalman_reservoir_multiscale_hybrid import KALMAN_RESERVOIR_MULTISCALE_HYBRID +from .multiscale_dcc_kalman_hybrid import MULTISCALE_DCC_KALMAN_HYBRID +from .quantum_multiscale_kalman_hybrid_ensemble import QUANTUM_MULTISCALE_KALMAN_HYBRID_ENSEMBLE +from .quantum_random_exp_hybrid import QUANTUM_RANDOM_EXP_HYBRID +from .random_fourier_kalman_hybrid import RANDOM_FOURIER_KALMAN_HYBRID +from .reservoir_changepoint_robust_hybrid_ensemble import RESERVOIR_CHANGEPOINT_ROBUST_HYBRID_ENSEMBLE +from .reservoir_edge_dcc_hybrid import RESERVOIR_EDGE_DCC_HYBRID +from .robust_differential_stft_hybrid import ROBUST_DIFFERENTIAL_STFT_HYBRID +from .robust_sparse_edge_hybrid import ROBUST_SPARSE_EDGE_HYBRID +from .robust_volatility_sliding_hybrid_ensemble import ROBUST_VOLATILITY_SLIDING_HYBRID_ENSEMBLE +from .sliding_volatility_dcc_hybrid import SLIDING_VOLATILITY_DCC_HYBRID +from .sparse_dcc_exp_hybrid_ensemble import SPARSE_DCC_EXP_HYBRID_ENSEMBLE +from .stft_edge_adaptive_hybrid_ensemble import STFT_EDGE_ADAPTIVE_HYBRID_ENSEMBLE +from .stft_exp_kalman_hybrid import STFT_EXP_KALMAN_HYBRID +from .stft_quantum_sparse_hybrid_ensemble import STFT_QUANTUM_SPARSE_HYBRID_ENSEMBLE +from .volatility_adaptive_edge_hybrid import VOLATILITY_ADAPTIVE_EDGE_HYBRID __all__ = [ "BaseDFCMethod", @@ -126,4 +156,34 @@ "RESERVOIR_ECHO_STATE", "LOCAL_JACOBIAN_COUPLING", "SPARSE_COACTIVATION_CODE", + "ADAPTIVE_DCC_RANDOM_HYBRID_ENSEMBLE", + "ADAPTIVE_EDGE_KALMAN_HYBRID", + "ADAPTIVE_QUANTUM_RESERVOIR_HYBRID", + "ADAPTIVE_RANDOM_SPARSE_HYBRID", + "ADAPTIVE_ROBUST_DIFFERENTIAL_HYBRID_ENSEMBLE", + "CHANGEPOINT_MULTISCALE_EXP_HYBRID_ENSEMBLE", + "CHANGEPOINT_ROBUST_EXP_HYBRID", + "DCC_CHANGEPOINT_SPARSE_HYBRID", + "DCC_EDGE_EXPONENTIAL_HYBRID", + "DCC_KALMAN_VOLATILITY_HYBRID_ENSEMBLE", + "DIFFERENTIAL_EDGE_KALMAN_HYBRID", + "EDGE_KALMAN_EXP_HYBRID_ENSEMBLE", + "EDGE_RANDOM_RESERVOIR_HYBRID_ENSEMBLE", + "EDGE_STFT_QUANTUM_HYBRID", + "KALMAN_RESERVOIR_MULTISCALE_HYBRID", + "MULTISCALE_DCC_KALMAN_HYBRID", + "QUANTUM_MULTISCALE_KALMAN_HYBRID_ENSEMBLE", + "QUANTUM_RANDOM_EXP_HYBRID", + "RANDOM_FOURIER_KALMAN_HYBRID", + "RESERVOIR_CHANGEPOINT_ROBUST_HYBRID_ENSEMBLE", + "RESERVOIR_EDGE_DCC_HYBRID", + "ROBUST_DIFFERENTIAL_STFT_HYBRID", + "ROBUST_SPARSE_EDGE_HYBRID", + "ROBUST_VOLATILITY_SLIDING_HYBRID_ENSEMBLE", + "SLIDING_VOLATILITY_DCC_HYBRID", + "SPARSE_DCC_EXP_HYBRID_ENSEMBLE", + "STFT_EDGE_ADAPTIVE_HYBRID_ENSEMBLE", + "STFT_EXP_KALMAN_HYBRID", + "STFT_QUANTUM_SPARSE_HYBRID_ENSEMBLE", + "VOLATILITY_ADAPTIVE_EDGE_HYBRID", ] diff --git a/pydfc/dfc_methods/adaptive_dcc_random_hybrid_ensemble.py b/pydfc/dfc_methods/adaptive_dcc_random_hybrid_ensemble.py new file mode 100644 index 0000000..c93c173 --- /dev/null +++ b/pydfc/dfc_methods/adaptive_dcc_random_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +AdaptiveDccRandom_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ADAPTIVE_DCC_RANDOM_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of AdaptiveExponentialWindow, DCCConnectivity, RandomFourierDependence.""" + + MEASURE_NAME = "AdaptiveDccRandom_hybrid_ensemble" + COMPONENTS = ['AdaptiveExponentialWindow', 'DCCConnectivity', 'RandomFourierDependence'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/adaptive_edge_kalman_hybrid.py b/pydfc/dfc_methods/adaptive_edge_kalman_hybrid.py new file mode 100644 index 0000000..0aa7c7f --- /dev/null +++ b/pydfc/dfc_methods/adaptive_edge_kalman_hybrid.py @@ -0,0 +1,286 @@ +""" +AdaptiveEdgeKalman_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ADAPTIVE_EDGE_KALMAN_HYBRID(BaseDFCMethod): + """State-free hybrid of AdaptiveExponentialWindow, EdgeCoactivation, KalmanCovariance.""" + + MEASURE_NAME = "AdaptiveEdgeKalman_hybrid" + COMPONENTS = ['AdaptiveExponentialWindow', 'EdgeCoactivation', 'KalmanCovariance'] + HYBRID_RULE = "adaptive_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/adaptive_quantum_reservoir_hybrid.py b/pydfc/dfc_methods/adaptive_quantum_reservoir_hybrid.py new file mode 100644 index 0000000..7f5e6b1 --- /dev/null +++ b/pydfc/dfc_methods/adaptive_quantum_reservoir_hybrid.py @@ -0,0 +1,286 @@ +""" +AdaptiveQuantumReservoir_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ADAPTIVE_QUANTUM_RESERVOIR_HYBRID(BaseDFCMethod): + """State-free hybrid of AdaptiveExponentialWindow, QuantumMutualInformationFC, ReservoirEchoStateFC.""" + + MEASURE_NAME = "AdaptiveQuantumReservoir_hybrid" + COMPONENTS = ['AdaptiveExponentialWindow', 'QuantumMutualInformationFC', 'ReservoirEchoStateFC'] + HYBRID_RULE = "information_state_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/adaptive_random_sparse_hybrid.py b/pydfc/dfc_methods/adaptive_random_sparse_hybrid.py new file mode 100644 index 0000000..bde2455 --- /dev/null +++ b/pydfc/dfc_methods/adaptive_random_sparse_hybrid.py @@ -0,0 +1,286 @@ +""" +AdaptiveRandomSparse_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ADAPTIVE_RANDOM_SPARSE_HYBRID(BaseDFCMethod): + """State-free hybrid of AdaptiveExponentialWindow, RandomFourierDependence, SparseCoactivationCodeFC.""" + + MEASURE_NAME = "AdaptiveRandomSparse_hybrid" + COMPONENTS = ['AdaptiveExponentialWindow', 'RandomFourierDependence', 'SparseCoactivationCodeFC'] + HYBRID_RULE = "sparse_kernel_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/adaptive_robust_differential_hybrid_ensemble.py b/pydfc/dfc_methods/adaptive_robust_differential_hybrid_ensemble.py new file mode 100644 index 0000000..fca9de1 --- /dev/null +++ b/pydfc/dfc_methods/adaptive_robust_differential_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +AdaptiveRobustDifferential_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ADAPTIVE_ROBUST_DIFFERENTIAL_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of AdaptiveExponentialWindow, RobustSlidingWindow, DifferentialCoactivationFC.""" + + MEASURE_NAME = "AdaptiveRobustDifferential_hybrid_ensemble" + COMPONENTS = ['AdaptiveExponentialWindow', 'RobustSlidingWindow', 'DifferentialCoactivationFC'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/changepoint_multiscale_exp_hybrid_ensemble.py b/pydfc/dfc_methods/changepoint_multiscale_exp_hybrid_ensemble.py new file mode 100644 index 0000000..2a3c71d --- /dev/null +++ b/pydfc/dfc_methods/changepoint_multiscale_exp_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +ChangepointMultiscaleExp_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class CHANGEPOINT_MULTISCALE_EXP_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of ChangepointResetWindow, MultiscaleWindow, ExponentialWindow.""" + + MEASURE_NAME = "ChangepointMultiscaleExp_hybrid_ensemble" + COMPONENTS = ['ChangepointResetWindow', 'MultiscaleWindow', 'ExponentialWindow'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/changepoint_robust_exp_hybrid.py b/pydfc/dfc_methods/changepoint_robust_exp_hybrid.py new file mode 100644 index 0000000..8b71748 --- /dev/null +++ b/pydfc/dfc_methods/changepoint_robust_exp_hybrid.py @@ -0,0 +1,286 @@ +""" +ChangepointRobustExp_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class CHANGEPOINT_ROBUST_EXP_HYBRID(BaseDFCMethod): + """State-free hybrid of ChangepointResetWindow, RobustSlidingWindow, ExponentialWindow.""" + + MEASURE_NAME = "ChangepointRobustExp_hybrid" + COMPONENTS = ['ChangepointResetWindow', 'RobustSlidingWindow', 'ExponentialWindow'] + HYBRID_RULE = "change_reset_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/dcc_changepoint_sparse_hybrid.py b/pydfc/dfc_methods/dcc_changepoint_sparse_hybrid.py new file mode 100644 index 0000000..61180d5 --- /dev/null +++ b/pydfc/dfc_methods/dcc_changepoint_sparse_hybrid.py @@ -0,0 +1,286 @@ +""" +DccChangepointSparse_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class DCC_CHANGEPOINT_SPARSE_HYBRID(BaseDFCMethod): + """State-free hybrid of DCCConnectivity, ChangepointResetWindow, SparseCoactivationCodeFC.""" + + MEASURE_NAME = "DccChangepointSparse_hybrid" + COMPONENTS = ['DCCConnectivity', 'ChangepointResetWindow', 'SparseCoactivationCodeFC'] + HYBRID_RULE = "change_sparse_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/dcc_edge_exponential_hybrid.py b/pydfc/dfc_methods/dcc_edge_exponential_hybrid.py new file mode 100644 index 0000000..52f8f19 --- /dev/null +++ b/pydfc/dfc_methods/dcc_edge_exponential_hybrid.py @@ -0,0 +1,286 @@ +""" +DccEdgeExponential_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class DCC_EDGE_EXPONENTIAL_HYBRID(BaseDFCMethod): + """State-free hybrid of DCCConnectivity, EdgeCoactivation, ExponentialWindow.""" + + MEASURE_NAME = "DccEdgeExponential_hybrid" + COMPONENTS = ['DCCConnectivity', 'EdgeCoactivation', 'ExponentialWindow'] + HYBRID_RULE = "conditional_residual" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/dcc_kalman_volatility_hybrid_ensemble.py b/pydfc/dfc_methods/dcc_kalman_volatility_hybrid_ensemble.py new file mode 100644 index 0000000..0e535b1 --- /dev/null +++ b/pydfc/dfc_methods/dcc_kalman_volatility_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +DccKalmanVolatility_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class DCC_KALMAN_VOLATILITY_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of DCCConnectivity, KalmanCovariance, VolatilityWeightedFC.""" + + MEASURE_NAME = "DccKalmanVolatility_hybrid_ensemble" + COMPONENTS = ['DCCConnectivity', 'KalmanCovariance', 'VolatilityWeightedFC'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/differential_edge_kalman_hybrid.py b/pydfc/dfc_methods/differential_edge_kalman_hybrid.py new file mode 100644 index 0000000..88e8f1a --- /dev/null +++ b/pydfc/dfc_methods/differential_edge_kalman_hybrid.py @@ -0,0 +1,286 @@ +""" +DifferentialEdgeKalman_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class DIFFERENTIAL_EDGE_KALMAN_HYBRID(BaseDFCMethod): + """State-free hybrid of DifferentialCoactivationFC, EdgeCoactivation, KalmanCovariance.""" + + MEASURE_NAME = "DifferentialEdgeKalman_hybrid" + COMPONENTS = ['DifferentialCoactivationFC', 'EdgeCoactivation', 'KalmanCovariance'] + HYBRID_RULE = "derivative_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/edge_kalman_exp_hybrid_ensemble.py b/pydfc/dfc_methods/edge_kalman_exp_hybrid_ensemble.py new file mode 100644 index 0000000..b89835b --- /dev/null +++ b/pydfc/dfc_methods/edge_kalman_exp_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +EdgeKalmanExp_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class EDGE_KALMAN_EXP_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of EdgeCoactivation, KalmanCovariance, ExponentialWindow.""" + + MEASURE_NAME = "EdgeKalmanExp_hybrid_ensemble" + COMPONENTS = ['EdgeCoactivation', 'KalmanCovariance', 'ExponentialWindow'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/edge_random_reservoir_hybrid_ensemble.py b/pydfc/dfc_methods/edge_random_reservoir_hybrid_ensemble.py new file mode 100644 index 0000000..612e7d7 --- /dev/null +++ b/pydfc/dfc_methods/edge_random_reservoir_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +EdgeRandomReservoir_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class EDGE_RANDOM_RESERVOIR_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of EdgeCoactivation, RandomFourierDependence, ReservoirEchoStateFC.""" + + MEASURE_NAME = "EdgeRandomReservoir_hybrid_ensemble" + COMPONENTS = ['EdgeCoactivation', 'RandomFourierDependence', 'ReservoirEchoStateFC'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/edge_stft_quantum_hybrid.py b/pydfc/dfc_methods/edge_stft_quantum_hybrid.py new file mode 100644 index 0000000..96202cc --- /dev/null +++ b/pydfc/dfc_methods/edge_stft_quantum_hybrid.py @@ -0,0 +1,286 @@ +""" +EdgeStftQuantum_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class EDGE_STFT_QUANTUM_HYBRID(BaseDFCMethod): + """State-free hybrid of EdgeCoactivation, STFTCoherence, QuantumMutualInformationFC.""" + + MEASURE_NAME = "EdgeStftQuantum_hybrid" + COMPONENTS = ['EdgeCoactivation', 'Time-Freq', 'QuantumMutualInformationFC'] + HYBRID_RULE = "spectral_information_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/kalman_reservoir_multiscale_hybrid.py b/pydfc/dfc_methods/kalman_reservoir_multiscale_hybrid.py new file mode 100644 index 0000000..4e221e4 --- /dev/null +++ b/pydfc/dfc_methods/kalman_reservoir_multiscale_hybrid.py @@ -0,0 +1,286 @@ +""" +KalmanReservoirMultiscale_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class KALMAN_RESERVOIR_MULTISCALE_HYBRID(BaseDFCMethod): + """State-free hybrid of KalmanCovariance, ReservoirEchoStateFC, MultiscaleWindow.""" + + MEASURE_NAME = "KalmanReservoirMultiscale_hybrid" + COMPONENTS = ['KalmanCovariance', 'ReservoirEchoStateFC', 'MultiscaleWindow'] + HYBRID_RULE = "state_multiscale_residual" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/multiscale_dcc_kalman_hybrid.py b/pydfc/dfc_methods/multiscale_dcc_kalman_hybrid.py new file mode 100644 index 0000000..232ffde --- /dev/null +++ b/pydfc/dfc_methods/multiscale_dcc_kalman_hybrid.py @@ -0,0 +1,286 @@ +""" +MultiscaleDccKalman_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class MULTISCALE_DCC_KALMAN_HYBRID(BaseDFCMethod): + """State-free hybrid of MultiscaleWindow, DCCConnectivity, KalmanCovariance.""" + + MEASURE_NAME = "MultiscaleDccKalman_hybrid" + COMPONENTS = ['MultiscaleWindow', 'DCCConnectivity', 'KalmanCovariance'] + HYBRID_RULE = "multiscale_residual" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/quantum_multiscale_kalman_hybrid_ensemble.py b/pydfc/dfc_methods/quantum_multiscale_kalman_hybrid_ensemble.py new file mode 100644 index 0000000..f1f9fd8 --- /dev/null +++ b/pydfc/dfc_methods/quantum_multiscale_kalman_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +QuantumMultiscaleKalman_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class QUANTUM_MULTISCALE_KALMAN_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of QuantumMutualInformationFC, MultiscaleWindow, KalmanCovariance.""" + + MEASURE_NAME = "QuantumMultiscaleKalman_hybrid_ensemble" + COMPONENTS = ['QuantumMutualInformationFC', 'MultiscaleWindow', 'KalmanCovariance'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/quantum_random_exp_hybrid.py b/pydfc/dfc_methods/quantum_random_exp_hybrid.py new file mode 100644 index 0000000..1b86d86 --- /dev/null +++ b/pydfc/dfc_methods/quantum_random_exp_hybrid.py @@ -0,0 +1,286 @@ +""" +QuantumRandomExp_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class QUANTUM_RANDOM_EXP_HYBRID(BaseDFCMethod): + """State-free hybrid of QuantumMutualInformationFC, RandomFourierDependence, ExponentialWindow.""" + + MEASURE_NAME = "QuantumRandomExp_hybrid" + COMPONENTS = ['QuantumMutualInformationFC', 'RandomFourierDependence', 'ExponentialWindow'] + HYBRID_RULE = "information_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/random_fourier_kalman_hybrid.py b/pydfc/dfc_methods/random_fourier_kalman_hybrid.py new file mode 100644 index 0000000..962e4e7 --- /dev/null +++ b/pydfc/dfc_methods/random_fourier_kalman_hybrid.py @@ -0,0 +1,286 @@ +""" +RandomFourierKalman_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class RANDOM_FOURIER_KALMAN_HYBRID(BaseDFCMethod): + """State-free hybrid of RandomFourierDependence, KalmanCovariance, ExponentialWindow.""" + + MEASURE_NAME = "RandomFourierKalman_hybrid" + COMPONENTS = ['RandomFourierDependence', 'KalmanCovariance', 'ExponentialWindow'] + HYBRID_RULE = "kernel_modulated" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/reservoir_changepoint_robust_hybrid_ensemble.py b/pydfc/dfc_methods/reservoir_changepoint_robust_hybrid_ensemble.py new file mode 100644 index 0000000..9b2c834 --- /dev/null +++ b/pydfc/dfc_methods/reservoir_changepoint_robust_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +ReservoirChangepointRobust_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class RESERVOIR_CHANGEPOINT_ROBUST_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of ReservoirEchoStateFC, ChangepointResetWindow, RobustSlidingWindow.""" + + MEASURE_NAME = "ReservoirChangepointRobust_hybrid_ensemble" + COMPONENTS = ['ReservoirEchoStateFC', 'ChangepointResetWindow', 'RobustSlidingWindow'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/reservoir_edge_dcc_hybrid.py b/pydfc/dfc_methods/reservoir_edge_dcc_hybrid.py new file mode 100644 index 0000000..10e5e1c --- /dev/null +++ b/pydfc/dfc_methods/reservoir_edge_dcc_hybrid.py @@ -0,0 +1,286 @@ +""" +ReservoirEdgeDcc_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class RESERVOIR_EDGE_DCC_HYBRID(BaseDFCMethod): + """State-free hybrid of ReservoirEchoStateFC, EdgeCoactivation, DCCConnectivity.""" + + MEASURE_NAME = "ReservoirEdgeDcc_hybrid" + COMPONENTS = ['ReservoirEchoStateFC', 'EdgeCoactivation', 'DCCConnectivity'] + HYBRID_RULE = "state_modulated" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/robust_differential_stft_hybrid.py b/pydfc/dfc_methods/robust_differential_stft_hybrid.py new file mode 100644 index 0000000..d22e51b --- /dev/null +++ b/pydfc/dfc_methods/robust_differential_stft_hybrid.py @@ -0,0 +1,286 @@ +""" +RobustDifferentialStft_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ROBUST_DIFFERENTIAL_STFT_HYBRID(BaseDFCMethod): + """State-free hybrid of RobustSlidingWindow, DifferentialCoactivationFC, STFTCoherence.""" + + MEASURE_NAME = "RobustDifferentialStft_hybrid" + COMPONENTS = ['RobustSlidingWindow', 'DifferentialCoactivationFC', 'Time-Freq'] + HYBRID_RULE = "robust_spectral_derivative" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/robust_sparse_edge_hybrid.py b/pydfc/dfc_methods/robust_sparse_edge_hybrid.py new file mode 100644 index 0000000..60ec416 --- /dev/null +++ b/pydfc/dfc_methods/robust_sparse_edge_hybrid.py @@ -0,0 +1,286 @@ +""" +RobustSparseEdge_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ROBUST_SPARSE_EDGE_HYBRID(BaseDFCMethod): + """State-free hybrid of RobustSlidingWindow, SparseCoactivationCodeFC, EdgeCoactivation.""" + + MEASURE_NAME = "RobustSparseEdge_hybrid" + COMPONENTS = ['RobustSlidingWindow', 'SparseCoactivationCodeFC', 'EdgeCoactivation'] + HYBRID_RULE = "sparse_residual" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/robust_volatility_sliding_hybrid_ensemble.py b/pydfc/dfc_methods/robust_volatility_sliding_hybrid_ensemble.py new file mode 100644 index 0000000..812456e --- /dev/null +++ b/pydfc/dfc_methods/robust_volatility_sliding_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +RobustVolatilitySliding_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class ROBUST_VOLATILITY_SLIDING_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of RobustSlidingWindow, VolatilityWeightedFC, SlidingWindow.""" + + MEASURE_NAME = "RobustVolatilitySliding_hybrid_ensemble" + COMPONENTS = ['RobustSlidingWindow', 'VolatilityWeightedFC', 'SlidingWindow'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/sliding_volatility_dcc_hybrid.py b/pydfc/dfc_methods/sliding_volatility_dcc_hybrid.py new file mode 100644 index 0000000..7e8a191 --- /dev/null +++ b/pydfc/dfc_methods/sliding_volatility_dcc_hybrid.py @@ -0,0 +1,286 @@ +""" +SlidingVolatilityDcc_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class SLIDING_VOLATILITY_DCC_HYBRID(BaseDFCMethod): + """State-free hybrid of SlidingWindow, VolatilityWeightedFC, DCCConnectivity.""" + + MEASURE_NAME = "SlidingVolatilityDcc_hybrid" + COMPONENTS = ['SlidingWindow', 'VolatilityWeightedFC', 'DCCConnectivity'] + HYBRID_RULE = "volatility_residual" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/sparse_dcc_exp_hybrid_ensemble.py b/pydfc/dfc_methods/sparse_dcc_exp_hybrid_ensemble.py new file mode 100644 index 0000000..8c3a19d --- /dev/null +++ b/pydfc/dfc_methods/sparse_dcc_exp_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +SparseDccExp_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class SPARSE_DCC_EXP_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of SparseCoactivationCodeFC, DCCConnectivity, ExponentialWindow.""" + + MEASURE_NAME = "SparseDccExp_hybrid_ensemble" + COMPONENTS = ['SparseCoactivationCodeFC', 'DCCConnectivity', 'ExponentialWindow'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/stft_edge_adaptive_hybrid_ensemble.py b/pydfc/dfc_methods/stft_edge_adaptive_hybrid_ensemble.py new file mode 100644 index 0000000..5e14272 --- /dev/null +++ b/pydfc/dfc_methods/stft_edge_adaptive_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +StftEdgeAdaptive_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class STFT_EDGE_ADAPTIVE_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of STFTCoherence, EdgeCoactivation, AdaptiveExponentialWindow.""" + + MEASURE_NAME = "StftEdgeAdaptive_hybrid_ensemble" + COMPONENTS = ['Time-Freq', 'EdgeCoactivation', 'AdaptiveExponentialWindow'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/stft_exp_kalman_hybrid.py b/pydfc/dfc_methods/stft_exp_kalman_hybrid.py new file mode 100644 index 0000000..5712c6d --- /dev/null +++ b/pydfc/dfc_methods/stft_exp_kalman_hybrid.py @@ -0,0 +1,286 @@ +""" +StftExpKalman_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class STFT_EXP_KALMAN_HYBRID(BaseDFCMethod): + """State-free hybrid of STFTCoherence, ExponentialWindow, KalmanCovariance.""" + + MEASURE_NAME = "StftExpKalman_hybrid" + COMPONENTS = ['STFTCoherence', 'ExponentialWindow', 'KalmanCovariance'] + HYBRID_RULE = "spectral_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/stft_quantum_sparse_hybrid_ensemble.py b/pydfc/dfc_methods/stft_quantum_sparse_hybrid_ensemble.py new file mode 100644 index 0000000..87956fe --- /dev/null +++ b/pydfc/dfc_methods/stft_quantum_sparse_hybrid_ensemble.py @@ -0,0 +1,286 @@ +""" +StftQuantumSparse_hybrid_ensemble. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class STFT_QUANTUM_SPARSE_HYBRID_ENSEMBLE(BaseDFCMethod): + """State-free hybrid of STFTCoherence, QuantumMutualInformationFC, SparseCoactivationCodeFC.""" + + MEASURE_NAME = "StftQuantumSparse_hybrid_ensemble" + COMPONENTS = ['Time-Freq', 'QuantumMutualInformationFC', 'SparseCoactivationCodeFC'] + HYBRID_RULE = "ensemble_mean" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/pydfc/dfc_methods/volatility_adaptive_edge_hybrid.py b/pydfc/dfc_methods/volatility_adaptive_edge_hybrid.py new file mode 100644 index 0000000..fb345da --- /dev/null +++ b/pydfc/dfc_methods/volatility_adaptive_edge_hybrid.py @@ -0,0 +1,286 @@ +""" +VolatilityAdaptiveEdge_hybrid. + +Hybrid dFC estimator generated from selected threshold_70_filtered_methods.npy +methods. The method combines at least two selected source-method elements and +returns a standard state-free PydFC DFC object. +""" + +import time + +import numpy as np + +from ..dfc import DFC +from ..time_series import TIME_SERIES +from .base_dfc_method import BaseDFCMethod + + +class VOLATILITY_ADAPTIVE_EDGE_HYBRID(BaseDFCMethod): + """State-free hybrid of VolatilityWeightedFC, AdaptiveExponentialWindow, EdgeCoactivation.""" + + MEASURE_NAME = "VolatilityAdaptiveEdge_hybrid" + COMPONENTS = ['VolatilityWeightedFC', 'AdaptiveExponentialWindow', 'EdgeCoactivation'] + HYBRID_RULE = "volatility_gate" + + def __init__(self, **params): + self.logs_ = "" + self.TPM = [] + self.FCS_ = [] + self.FCS_fit_time_ = None + self.dFC_assess_time_ = None + self.params_name_lst = [ + "measure_name", + "is_state_based", + "min_periods", + "half_life", + "window_length", + "n_overlap", + "nonlinear_gain", + "normalization", + "num_select_nodes", + "num_time_point", + "Fs_ratio", + "noise_ratio", + "num_realization", + "session", + ] + self.params = {} + for params_name in self.params_name_lst: + self.params[params_name] = params.get(params_name, None) + self.params["measure_name"] = self.MEASURE_NAME + self.params["is_state_based"] = False + if self.params["min_periods"] is None: + self.params["min_periods"] = 12 + if self.params["half_life"] is None: + self.params["half_life"] = 30 + if self.params["window_length"] is None: + self.params["window_length"] = 30 + if self.params["n_overlap"] is None: + self.params["n_overlap"] = 0.0 + if self.params["nonlinear_gain"] is None: + self.params["nonlinear_gain"] = 1.0 + + @property + def measure_name(self): + return self.params["measure_name"] + + def _alpha_from_half_life(self, Fs): + half_life_samples = max(float(self.params["half_life"]) * Fs, 1.0) + return 1.0 - np.exp(np.log(0.5) / half_life_samples) + + @staticmethod + def _corr_from_cov(covariance): + variance = np.diag(covariance) + scale = np.sqrt(np.outer(variance, variance)) + corr = np.divide( + covariance, + scale, + out=np.zeros_like(covariance, dtype=float), + where=scale > 1e-12, + ) + corr[np.isnan(corr)] = 0.0 + corr = (corr + corr.T) / 2.0 + np.fill_diagonal(corr, 1.0) + return np.clip(corr, -1.0, 1.0) + + def _weighted_corr(self, samples, weights): + weights = np.asarray(weights, dtype=float) + weights = np.maximum(weights, 0.0) + if np.sum(weights) <= 1e-12: + weights = np.ones(samples.shape[1], dtype=float) + weights = weights / np.sum(weights) + mean = np.sum(samples * weights[None, :], axis=1, keepdims=True) + centered = samples - mean + covariance = (centered * weights[None, :]) @ centered.T + return self._corr_from_cov(covariance) + + @staticmethod + def _rank_rows(samples): + ranks = np.zeros_like(samples, dtype=float) + for i, row in enumerate(samples): + order = np.argsort(row, kind="mergesort") + rank = np.empty_like(order, dtype=float) + rank[order] = np.arange(row.size, dtype=float) + ranks[i] = rank + return ranks + + def _component_matrix(self, name, data, Fs, tr, state): + n_regions = data.shape[0] + min_periods = int(self.params["min_periods"]) + start = max(0, tr - min_periods + 1) + window = data[:, start : tr + 1] + alpha = self._alpha_from_half_life(Fs) + age = tr - np.arange(tr + 1) + + if name == "SlidingWindow": + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "ExponentialWindow": + return self._weighted_corr(data[:, : tr + 1], alpha * np.power(1.0 - alpha, age)) + if name == "AdaptiveExponentialWindow": + diff = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1))) if tr > 0 else 0.0 + adapt = diff / (diff + np.std(window) + 1e-8) + local_alpha = 0.02 + 0.33 * adapt + local_weights = local_alpha * np.power(1.0 - local_alpha, age) + return self._weighted_corr(data[:, : tr + 1], local_weights) + if name == "ChangepointResetWindow": + if tr > min_periods: + diffs = np.mean(np.abs(np.diff(data[:, max(0, tr - min_periods) : tr + 1], axis=1)), axis=0) + threshold = np.median(diffs) + 2.0 * np.std(diffs) + hits = np.where(diffs > threshold)[0] + if hits.size: + window = data[:, start + hits[-1] + 1 : tr + 1] + return self._weighted_corr(window, np.ones(window.shape[1])) + if name == "DCCConnectivity": + eps = data[:, : tr + 1] - np.mean(data[:, : tr + 1], axis=1, keepdims=True) + lam = 0.94 + sigma2 = np.var(eps, axis=1) + 1e-8 + q = state.get("dcc_q", np.eye(n_regions)) + z = eps[:, -1] / np.sqrt(sigma2) + q_bar = self._weighted_corr(eps, np.ones(eps.shape[1])) + q = 0.10 * q_bar + 0.05 * np.outer(z, z) + 0.85 * q + state["dcc_q"] = q + return self._corr_from_cov(q + (1.0 - lam) * np.diag(sigma2)) + if name == "DifferentialCoactivationFC": + if window.shape[1] < 2: + return np.eye(n_regions) + return self._weighted_corr(np.diff(window, axis=1), np.ones(window.shape[1] - 1)) + if name == "EdgeCoactivation": + mean = np.mean(window, axis=1, keepdims=True) + std = np.std(window, axis=1, keepdims=True) + 1e-8 + z = (data[:, tr : tr + 1] - mean)[:, 0] / std[:, 0] + matrix = np.outer(z, z) + scale = np.sqrt(np.outer(np.diag(matrix) ** 2, np.diag(matrix) ** 2)) + 1e-8 + return self._corr_from_cov(matrix / scale) + if name == "KalmanCovariance": + mean = state.get("kalman_mean", data[:, 0].copy()) + cov = state.get("kalman_cov", np.eye(n_regions)) + innovation = data[:, tr] - mean + mean = mean + alpha * innovation + cov = (1.0 - alpha) * cov + alpha * np.outer(innovation, innovation) + 1e-4 * np.eye(n_regions) + state["kalman_mean"] = mean + state["kalman_cov"] = cov + return self._corr_from_cov(cov) + if name == "MultiscaleWindow": + mats = [] + for scale in (0.5, 1.0, 1.5): + width = max(4, int(min_periods * scale)) + seg = data[:, max(0, tr - width + 1) : tr + 1] + mats.append(self._weighted_corr(seg, np.ones(seg.shape[1]))) + return np.mean(mats, axis=0) + if name == "QuantumMutualInformationFC": + corr = self._weighted_corr(window, np.ones(window.shape[1])) + qmi = -np.log(np.maximum(1.0 - corr**2, 1e-6)) + qmi = qmi / (np.max(qmi) + 1e-8) + return np.sign(corr) * qmi + if name == "RandomFourierDependence": + omega = np.linspace(0.5, 2.5, 16) + phase = np.linspace(0.0, np.pi, 16) + phi = np.cos(window[:, :, None] * omega + phase) + feature = np.mean(phi, axis=1) + feature -= np.mean(feature, axis=1, keepdims=True) + norm = np.linalg.norm(feature, axis=1, keepdims=True) + 1e-8 + feature = feature / norm + matrix = feature @ feature.T + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "ReservoirEchoStateFC": + reservoir = state.get("reservoir", np.zeros(n_regions)) + recurrent = state.get("reservoir_w", np.eye(n_regions) * 0.45) + reservoir = np.tanh(0.55 * data[:, tr] + recurrent @ reservoir) + state["reservoir"] = reservoir + matrix = 0.5 * self._weighted_corr(window, np.ones(window.shape[1])) + 0.5 * np.outer(reservoir, reservoir) + return self._corr_from_cov(matrix) + if name == "RobustSlidingWindow": + ranked = self._rank_rows(window) + return self._weighted_corr(ranked, np.ones(ranked.shape[1])) + if name == "SparseCoactivationCodeFC": + edge = self._component_matrix("EdgeCoactivation", data, Fs, tr, state) + threshold = np.quantile(np.abs(edge), 0.75) + sparse = np.where(np.abs(edge) >= threshold, edge, 0.0) + np.fill_diagonal(sparse, 1.0) + return sparse + if name == "STFTCoherence": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + coeff = spectrum[:, 1:] + cross = np.abs(coeff @ coeff.conj().T) + power = np.sum(np.abs(coeff) ** 2, axis=1) + denom = np.sqrt(np.outer(power, power)) + 1e-8 + matrix = np.real(cross / denom) + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, 0.0, 1.0) + if name == "Time-Freq": + centered = window - np.mean(window, axis=1, keepdims=True) + spectrum = np.fft.rfft(centered, axis=1) + if spectrum.shape[1] <= 1: + return np.eye(n_regions) + phase = spectrum[:, 1:] / (np.abs(spectrum[:, 1:]) + 1e-8) + matrix = np.real(phase @ phase.conj().T) / phase.shape[1] + np.fill_diagonal(matrix, 1.0) + return np.clip(matrix, -1.0, 1.0) + if name == "VolatilityWeightedFC": + residuals = window - np.mean(window, axis=1, keepdims=True) + weights = np.sum(residuals**2, axis=0) + return self._weighted_corr(window, weights) + raise ValueError(f"Unsupported hybrid component: {name}") + + def _combine(self, matrices): + mats = np.array(matrices, dtype=float) + rule = self.HYBRID_RULE + gain = float(self.params["nonlinear_gain"]) + if rule == "ensemble_mean": + combined = np.mean(mats, axis=0) + else: + base = mats[0] + auxiliary = np.mean(mats[1:], axis=0) + contrast = mats[1] - mats[-1] + gate = 1.0 / (1.0 + np.exp(-gain * auxiliary)) + if "residual" in rule: + combined = base + gate * contrast + elif "sparse" in rule: + threshold = np.quantile(np.abs(auxiliary), 0.65) + combined = base * (np.abs(auxiliary) >= threshold) + 0.5 * contrast + elif "kernel" in rule or "information" in rule or "spectral" in rule: + combined = np.sign(base) * np.sqrt(np.abs(base * auxiliary) + 1e-8) + 0.25 * contrast + elif "change" in rule or "derivative" in rule or "volatility" in rule: + combined = (1.0 - gate) * base + gate * (base * auxiliary) + else: + combined = (1.0 - gate) * base + gate * auxiliary + combined = (combined + combined.T) / 2.0 + combined[np.isnan(combined)] = 0.0 + np.fill_diagonal(combined, 1.0) + return np.clip(combined, -1.0, 1.0) + + def dFC(self, time_series, Fs): + min_periods = int(self.params["min_periods"]) + if time_series.shape[1] < min_periods: + raise ValueError("time_series has fewer samples than min_periods.") + state = {} + FCSs = [] + TR_array = [] + for tr in range(min_periods - 1, time_series.shape[1]): + matrices = [ + self._component_matrix(component, time_series, Fs, tr, state) + for component in self.COMPONENTS + ] + FCSs.append(self._combine(matrices)) + TR_array.append(tr) + return np.array(FCSs), np.array(TR_array) + + def estimate_FCS(self, time_series): + return self + + def estimate_dFC(self, time_series): + assert ( + len(time_series.subj_id_lst) == 1 + ), "this function takes only one subject as input." + assert type(time_series) is TIME_SERIES, "time_series must be of TIME_SERIES class." + time_series = self.manipulate_time_series4dFC(time_series) + tic = time.time() + FCSs, TR_array = self.dFC(time_series=time_series.data, Fs=time_series.Fs) + self.set_dFC_assess_time(time.time() - tic) + dFC = DFC(measure=self) + dFC.set_dFC(FCSs=FCSs, TR_array=TR_array, TS_info=time_series.info_dict) + return dFC diff --git a/task_dFC/run_scripts_slurm/achillev@narval.alliancecan b/task_dFC/run_scripts_slurm/achillev@narval.alliancecan new file mode 100644 index 0000000..3247942 --- /dev/null +++ b/task_dFC/run_scripts_slurm/achillev@narval.alliancecan @@ -0,0 +1,32 @@ +#!/bin/bash +# +#SBATCH --job-name=assess_dfc_job +#SBATCH --output=logs/dfc_out_%A_%a.txt +#SBATCH --error=logs/dfc_err_%A_%a.txt +#SBATCH --time=24:00:00 +#SBATCH --mem=32G +#SBATCH --requeue + +SUBJECT_LIST="./subj_list.txt" +DATASET_INFO="./dataset_info.json" +METHODS_CONFIG="./methods_config.json" + +echo "Number subjects found: $(cat $SUBJECT_LIST | wc -l)" + +SUBJECT_ID=$(sed -n "${SLURM_ARRAY_TASK_ID}p" $SUBJECT_LIST) +echo "Subject ID: $SUBJECT_ID" + +# ---- Cluster configuration (set these for your system) ---- +VENV_PATH="/home/achillev/projects/def-jbpoline/achillev/pydfc_env/bin/activate" +PYDFC_CODE_DIR="/home/achillev/scratch/Git_repo" +# ----------------------------------------------------------- + +# Activate virtual environment +source "$VENV_PATH" + +python "$PYDFC_CODE_DIR/task_dFC/dFC_assessment.py" \ +--dataset_info $DATASET_INFO \ +--methods_config $METHODS_CONFIG \ +--participant_id $SUBJECT_ID + +deactivate diff --git a/task_dFC/run_scripts_slurm/dataset_info.json b/task_dFC/run_scripts_slurm/dataset_info.json index b01dbda..ca61229 100644 --- a/task_dFC/run_scripts_slurm/dataset_info.json +++ b/task_dFC/run_scripts_slurm/dataset_info.json @@ -1,27 +1,36 @@ { - "dataset" : "", - "main_root" : "/path/to/your/data/{dataset}", - "bids_root" : "/path/to/your/data/{dataset}/bids", - "fmriprep_root" : "/path/to/your/data/{dataset}/derivatives/fmriprep/23.1.3/output", + "dataset" : "ds003465", + "main_root" : "/home/achillev/scratch/ds003465/{dataset}", + "bids_root" : null, + "fmriprep_root" : null, "roi_root" : "{main_root}/derivatives/ROI_timeseries", "fitted_measures_root" : "{main_root}/derivatives/fitted_MEASURES", "dFC_root" : "{main_root}/derivatives/dFC_assessed", "ML_root" : "{main_root}/derivatives/ML", "reports_root" : "{main_root}/derivatives/reports", "bold_suffix" : "_space-MNI152NLin2009cAsym_res-2_desc-preproc_bold.nii.gz", - "SESSIONS" : [ - "ses-1" - ], - "TASKS" : [ - "task-A" - ], - "RUNS" : { - "task-A": ["run-01", "run-02", "run-03", "run-04", "run-05", "run-06"] - }, - "trial_type_label" : { - "task-A": "trial_type" - }, - "rest_labels" : { - "task-A": ["rest", "Rest"] - } + "SESSIONS" : [ + "ses-wave1bas" + ], + "TASKS" : [ + "task-Axcpt", "task-Cuedts", "task-Stern", "task-Stroop" + ], + "RUNS" : { + "task-Axcpt": ["run-1", "run-2"], + "task-Cuedts": ["run-1", "run-2"], + "task-Stern": ["run-1", "run-2"], + "task-Stroop": ["run-1", "run-2"] + }, + "trial_type_label" : { + "task-Axcpt": "trial_type", + "task-Cuedts": "trial_type", + "task-Stern": "trial_type", + "task-Stroop": "trial_type" + }, + "rest_labels" : { + "task-Axcpt": ["rest", "Rest"], + "task-Cuedts": ["rest", "Rest"], + "task-Stern": ["rest", "Rest"], + "task-Stroop": ["rest", "Rest"] + } } diff --git a/task_dFC/run_scripts_slurm/methods_config.json b/task_dFC/run_scripts_slurm/methods_config.json index 9edf7a8..ea411eb 100644 --- a/task_dFC/run_scripts_slurm/methods_config.json +++ b/task_dFC/run_scripts_slurm/methods_config.json @@ -47,67 +47,36 @@ "dict_alpha": 0.1 }, "MEASURES_name_lst": [ - "SlidingWindow", - "Time-Freq", - "CAP", - "ContinuousHMM", - "Windowless", - "Clustering", - "DiscreteHMM", - "ExponentialWindow", - "AdaptiveExponentialWindow", - "MultiscaleWindow", - "EdgeCoactivation", - "PhaseLockingWindow", - "DerivativeWeightedWindow", - "TemporalDerivativeMultiplication", - "ChangepointResetWindow", - "KalmanCovariance", - "LaggedMaxCorrelation", - "PrecisionShrinkageWindow", - "RecurrenceKernelDependence", - "RandomFourierDependence", - "EventSynchronization", - "CopulaTailDependence", - "OjaSubspaceConnectivity", - "GraphDiffusionCoactivation", - "PooledKMeansStates", - "MiniBatchKMeansStates", - "GaussianMixtureStates", - "BayesianGaussianMixtureStates", - "BirchStates", - "AgglomerativeStates", - "SpectralStates", - "LaggedKMeansStates", - "MarkovSmoothedKMeansStates", - "MarkovSmoothedGMMStates", - "AmplitudeEnvelopeCorrelation", - "LeadingEigenvectorDynamics", - "InstantaneousPhaseCoherence", - "DynamicPartialCorrelation", - "PhaseLagIndexWindow", - "PointProcessConnectivity", - "STFTCoherence", - "RobustSlidingWindow", - "SynchronyLikelihoodWindow", - "NMFStates", - "DifferentialCoactivationFC", - "CurvatureCorrelationFC", - "VolatilityWeightedFC", - "TemporalAsymmetryFC", - "PhaseAmplitudeCrossFC", - "MutualCompressionFC", - "TangentSpaceFC", - "SpectralSimilarityFC", - "PositiveNegativeAsymmetryFC", - "StateSpaceNeighborhoodFC", - "DCCConnectivity", - "PersistentHomologyFC", - "TimeReversalAsymmetryFC", - "QuantumMutualInformationFC", - "ReservoirEchoStateFC", - "LocalJacobianCouplingFC", - "SparseCoactivationCodeFC" + "ADAPTIVE_DCC_RANDOM_HYBRID_ENSEMBLE", + "ADAPTIVE_EDGE_KALMAN_HYBRID", + "ADAPTIVE_QUANTUM_RESERVOIR_HYBRID", + "ADAPTIVE_RANDOM_SPARSE_HYBRID", + "ADAPTIVE_ROBUST_DIFFERENTIAL_HYBRID_ENSEMBLE", + "CHANGEPOINT_MULTISCALE_EXP_HYBRID_ENSEMBLE", + "CHANGEPOINT_ROBUST_EXP_HYBRID", + "DCC_CHANGEPOINT_SPARSE_HYBRID", + "DCC_EDGE_EXPONENTIAL_HYBRID", + "DCC_KALMAN_VOLATILITY_HYBRID_ENSEMBLE", + "DIFFERENTIAL_EDGE_KALMAN_HYBRID", + "EDGE_KALMAN_EXP_HYBRID_ENSEMBLE", + "EDGE_RANDOM_RESERVOIR_HYBRID_ENSEMBLE", + "EDGE_STFT_QUANTUM_HYBRID", + "KALMAN_RESERVOIR_MULTISCALE_HYBRID", + "MULTISCALE_DCC_KALMAN_HYBRID", + "QUANTUM_MULTISCALE_KALMAN_HYBRID_ENSEMBLE", + "QUANTUM_RANDOM_EXP_HYBRID", + "RANDOM_FOURIER_KALMAN_HYBRID", + "RESERVOIR_CHANGEPOINT_ROBUST_HYBRID_ENSEMBLE", + "RESERVOIR_EDGE_DCC_HYBRID", + "ROBUST_DIFFERENTIAL_STFT_HYBRID", + "ROBUST_SPARSE_EDGE_HYBRID", + "ROBUST_VOLATILITY_SLIDING_HYBRID_ENSEMBLE", + "SLIDING_VOLATILITY_DCC_HYBRID", + "SPARSE_DCC_EXP_HYBRID_ENSEMBLE", + "STFT_EDGE_ADAPTIVE_HYBRID_ENSEMBLE", + "STFT_EXP_KALMAN_HYBRID", + "STFT_QUANTUM_SPARSE_HYBRID_ENSEMBLE", + "VOLATILITY_ADAPTIVE_EDGE_HYBRID" ], "alter_hparams": [], "params_multi_analysis": { diff --git a/task_dFC/run_scripts_slurm/multi_dataset_info.json b/task_dFC/run_scripts_slurm/multi_dataset_info.json index de0cf2b..e0411df 100644 --- a/task_dFC/run_scripts_slurm/multi_dataset_info.json +++ b/task_dFC/run_scripts_slurm/multi_dataset_info.json @@ -1,38 +1,22 @@ { - "output_root": "/path/to/your/data/multi_dataset_analysis/results", + "output_root": "/home/achillev/scratch/ds003465/multi_dataset_analysis/results", "real_data": { - "main_root": "/path/to/your/data/openneuro", + "main_root": "/home/achillev/scratch/ds003465/", "DATASETS": [ - "ds001242", "ds002236", "ds002647", - "ds002843", "ds002994", - "ds003465", "ds003612", "ds003823", - "ds004044", "ds004349", "ds004359", - "ds004556", "ds004746", "ds004791", - "ds004848", "ds005038" + "ds003465" ], "TASKS_to_include": [ - "task-arithmetic", "task-AudSem", "task-Axcpt", - "task-Cuedts", "task-emotionRegulation", "task-execution","task-expo", - "task-fearlearning", "task-feedback", "task-fribBids", "task-IHG", - "task-imagery", "task-itc", "task-localiser", "task-Localizer", - "task-matching", "task-motor", "task-paingen", "task-ppalocalizer", - "task-recall", "task-risk", "task-ST", "task-Stern", - "task-Stroop", "task-VisRhyme", "task-VisSem", "task-VisSpell", - "task-vswm" + "task-Axcpt", + "task-Cuedts", + "task-Stern", + "task-Stroop" ] }, "simulated_data": { - "main_root": "/path/to/your/data/simulated", + "main_root": "/home/achillev/scratch/ds003465/", "DATASETS": [ - "ds000001", "ds000002", "ds000003", "ds000004", "ds000005", "ds000006" ], "TASKS_to_include": [ - "task-Axcpt", "task-Cuedts", "task-Stern", "task-Stroop", - "task-lowFreqLongRest", "task-lowFreqShortRest", "task-lowFreqShortTask", - "task-imagery", "task-execution", - "task-itc", "task-risk", - "task-Localizer", - "task-ppalocalizer" ] } } diff --git a/task_dFC/run_scripts_slurm/run_FCS.sh b/task_dFC/run_scripts_slurm/run_FCS.sh index e980103..d6d3a30 100644 --- a/task_dFC/run_scripts_slurm/run_FCS.sh +++ b/task_dFC/run_scripts_slurm/run_FCS.sh @@ -17,8 +17,8 @@ export OPENBLAS_NUM_THREADS=1 export NUMEXPR_NUM_THREADS=1 # ---- Cluster configuration (set these for your system) ---- -VENV_PATH="/path/to/your/venv/bin/activate" -PYDFC_CODE_DIR="/path/to/pydfc" +VENV_PATH="/home/achillev/projects/def-jbpoline/achillev/pydfc_env/bin/activate" +PYDFC_CODE_DIR="/home/achillev/scratch/Git_repo" # ----------------------------------------------------------- # Activate virtual environment diff --git a/task_dFC/run_scripts_slurm/run_ML.sh b/task_dFC/run_scripts_slurm/run_ML.sh index 4c74011..b16c2a2 100644 --- a/task_dFC/run_scripts_slurm/run_ML.sh +++ b/task_dFC/run_scripts_slurm/run_ML.sh @@ -3,14 +3,15 @@ #SBATCH --cpus-per-task=8 #SBATCH --output=logs/ML_out_%A_%a.txt # %A = array job ID, %a = task ID #SBATCH --error=logs/ML_err_%A_%a.txt +#SBATCH --time=24:00:00 #SBATCH --mem=128G #SBATCH --requeue DATASET_INFO="./dataset_info.json" # ---- Cluster configuration (set these for your system) ---- -VENV_PATH="/path/to/your/venv/bin/activate" -PYDFC_CODE_DIR="/path/to/pydfc" +VENV_PATH="/home/achillev/projects/def-jbpoline/achillev/pydfc_env/bin/activate" +PYDFC_CODE_DIR="/home/achillev/scratch/Git_repo" # ----------------------------------------------------------- # Activate virtual environment diff --git a/task_dFC/run_scripts_slurm/run_across_dataset_analysis.sh b/task_dFC/run_scripts_slurm/run_across_dataset_analysis.sh index 49ae370..23d9169 100644 --- a/task_dFC/run_scripts_slurm/run_across_dataset_analysis.sh +++ b/task_dFC/run_scripts_slurm/run_across_dataset_analysis.sh @@ -9,8 +9,8 @@ # #SBATCH --chdir=/path/to/multi_dataset_analysis/codes # ---- Cluster configuration (set these for your system) ---- -VENV_PATH="/path/to/your/venv/bin/activate" -PYDFC_CODE_DIR="/path/to/pydfc" +VENV_PATH="/home/achillev/projects/def-jbpoline/achillev/pydfc_env/bin/activate" +PYDFC_CODE_DIR="/home/achillev/scratch/Git_repo" # ----------------------------------------------------------- set -euo pipefail diff --git a/task_dFC/run_scripts_slurm/run_dFC.sh b/task_dFC/run_scripts_slurm/run_dFC.sh index 70d424a..3247942 100644 --- a/task_dFC/run_scripts_slurm/run_dFC.sh +++ b/task_dFC/run_scripts_slurm/run_dFC.sh @@ -17,8 +17,8 @@ SUBJECT_ID=$(sed -n "${SLURM_ARRAY_TASK_ID}p" $SUBJECT_LIST) echo "Subject ID: $SUBJECT_ID" # ---- Cluster configuration (set these for your system) ---- -VENV_PATH="/path/to/your/venv/bin/activate" -PYDFC_CODE_DIR="/path/to/pydfc" +VENV_PATH="/home/achillev/projects/def-jbpoline/achillev/pydfc_env/bin/activate" +PYDFC_CODE_DIR="/home/achillev/scratch/Git_repo" # ----------------------------------------------------------- # Activate virtual environment diff --git a/task_dFC/run_scripts_slurm/run_report.sh b/task_dFC/run_scripts_slurm/run_report.sh index 12c6ebc..0ee92f1 100644 --- a/task_dFC/run_scripts_slurm/run_report.sh +++ b/task_dFC/run_scripts_slurm/run_report.sh @@ -10,8 +10,8 @@ DATASET_INFO="./dataset_info.json" SUBJ_LIST="./subj_list.txt" # ---- Cluster configuration (set these for your system) ---- -VENV_PATH="/path/to/your/venv/bin/activate" -PYDFC_CODE_DIR="/path/to/pydfc" +VENV_PATH="/home/achillev/projects/def-jbpoline/achillev/pydfc_env/bin/activate" +PYDFC_CODE_DIR="/home/achillev/scratch/Git_repo" # ----------------------------------------------------------- # Activate virtual environment diff --git a/threshold_70_filtered_methods.npy b/threshold_70_filtered_methods.npy new file mode 100644 index 0000000..9da8620 Binary files /dev/null and b/threshold_70_filtered_methods.npy differ