cortical-motor-engram-decoding / 04_decoding / wsc.py
wsc.py
Raw
#!/usr/bin/env python
# coding: utf-8


# ### Within-subject classification


"""
Within-subject searchlight classification with manual permutation testing
(no-error trials, leave-one-run-out CV)

This script performs within-subject decoding of sequence identity using
surface-based searchlight SVM classification. A leave-one-run-out
cross-validation scheme is used, with permutation-based p-values
computed per searchlight.

Author: Theo Marins
"""

# ============================================================================
# Imports
# ============================================================================

import os
import gc
import time
import pickle
import warnings
import psutil

import numpy as np
import neuroboros as nb
from tqdm import tqdm
from sklearn.svm import LinearSVC
from sklearn.exceptions import ConvergenceWarning
from sklearn.base import clone

warnings.filterwarnings("ignore", category=ConvergenceWarning)

# ============================================================================
# Paths
# ============================================================================

PERM_DIR = os.path.join(BASE_DIR, "permutations_ws")
os.makedirs(PERM_DIR, exist_ok=True)

subject_data_file = os.path.join(PKL_DIR, "darrays.pkl")
run_label_file = os.path.join(PKL_DIR, "run_label.pkl")
subject_label_file = os.path.join(PKL_DIR, "subject_label.pkl")
session_label_file = os.path.join(PKL_DIR, "session_label.pkl")
sequence_label_file = os.path.join(PKL_DIR, "sequence_label.pkl")

# ============================================================================
# Load data
# ============================================================================

with open(subject_data_file, "rb") as f:
    subject_data = pickle.load(f)

with open(run_label_file, "rb") as f:
    run_label = pickle.load(f)

with open(subject_label_file, "rb") as f:
    subject_label = pickle.load(f)

with open(session_label_file, "rb") as f:
    session_label = pickle.load(f)

with open(sequence_label_file, "rb") as f:
    sequence_label = pickle.load(f)

print("Loaded sessions:", subject_data.keys())

# ============================================================================
# Parameters
# ============================================================================

decoder = LinearSVC(penalty="l2", dual="auto", max_iter=5000)

radius = 10
batch_size = 60
n_permutations = 100

memory_limit_gb = 200
memory_limit_bytes = memory_limit_gb * 1024**3

# ============================================================================
# Utilities
# ============================================================================

def check_memory_usage():
    """Abort if memory usage exceeds threshold."""
    process = psutil.Process(os.getpid())
    memory_usage = process.memory_info().rss
    if memory_usage > memory_limit_bytes:
        raise MemoryError(
            f"Memory usage {memory_usage / 1024**3:.2f} GB "
            f"exceeds limit {memory_limit_gb} GB"
        )

# ============================================================================
# Permutation test (within-subject)
# ============================================================================

def permutation_test_searchlight_within(
    batch_indices,
    sls,
    train_data,
    test_data,
    train_labels,
    test_labels,
    decoder,
    n_permutations
):
    """
    Manual permutation test for a batch of searchlights (within-subject).
    """

    rng = np.random.default_rng(42)
    results = []

    # concatenate training runs
    X_train_all = np.concatenate(train_data, axis=0)
    y_train_all = train_labels

    for idx in batch_indices:
        sl = sls[idx]

        X_train = X_train_all[:, sl]
        X_test = test_data[:, sl]

        clf = clone(decoder)
        clf.fit(X_train, y_train_all)
        score = clf.score(X_test, test_labels)

        perm_scores = []
        for _ in range(n_permutations):
            y_perm = rng.permutation(y_train_all)
            clf = clone(decoder)
            clf.fit(X_train, y_perm)
            perm_scores.append(clf.score(X_test, test_labels))

        perm_scores = np.array(perm_scores)
        pval = (np.sum(perm_scores >= score) + 1) / (n_permutations + 1)

        results.append((idx, score, perm_scores, pval))

    return results

# ============================================================================
# Main loop
# ============================================================================

subjects = list(subject_data["01"].keys())

scores = {}
permutation_scores = {}
pvalues = {}

for hemi in ["right", "left"]:

    print("\n" + "=" * 60)
    print(f"Processing hemisphere: {hemi}")
    print("=" * 60)

    sls = nb.sls(hemi[0], radius, space="fsavg-ico32", mask=True)
    valid_indices = list(range(len(sls)))

    scores[hemi] = {}
    permutation_scores[hemi] = {}
    pvalues[hemi] = {}

    for session in subject_data.keys():
        for subject in subjects:

            print(f"\nSubject {subject} | Session {session}")

            # ------------------------------------------------------------
            # Load runs
            # ------------------------------------------------------------
            run_data = []
            run_labels = []

            for run in subject_data[session][subject].keys():
                run_data.append(subject_data[session][subject][run][hemi])
                run_labels.append(sequence_label[session][subject][run])

            n_runs = len(run_data)

            scores[hemi][subject] = np.zeros(len(sls))
            permutation_scores[hemi][subject] = np.zeros((len(sls), n_permutations))
            pvalues[hemi][subject] = np.zeros(len(sls))

            # ------------------------------------------------------------
            # Leave-one-run-out cross-validation
            # ------------------------------------------------------------
            for test_run in range(n_runs):

                print(f"  Test run {test_run + 1}/{n_runs}")

                train_data = []
                train_labels = []

                for run_id, (data, labels) in enumerate(zip(run_data, run_labels)):
                    if run_id == test_run:
                        test_data = data
                        test_labels = labels
                    else:
                        train_data.append(data)
                        train_labels.append(labels)

                train_data = np.stack(train_data, axis=0)
                train_labels = np.concatenate(train_labels)

                # --------------------------------------------------------
                # Searchlight batches
                # --------------------------------------------------------
                for i in range(0, len(valid_indices), batch_size):

                    batch_indices = valid_indices[i:i + batch_size]
                    print(f"    Batch {i}–{i + len(batch_indices)}")

                    start = time.time()
                    batch_results = permutation_test_searchlight_within(
                        batch_indices,
                        sls,
                        train_data,
                        test_data,
                        train_labels,
                        test_labels,
                        decoder,
                        n_permutations
                    )

                    for idx, score, perm, pval in batch_results:
                        scores[hemi][subject][idx] += score
                        permutation_scores[hemi][subject][idx] += perm
                        pvalues[hemi][subject][idx] += pval

                    print(f"    Done in {time.time() - start:.2f}s")

                gc.collect()
                check_memory_usage()

            # ------------------------------------------------------------
            # Average across runs
            # ------------------------------------------------------------
            scores[hemi][subject] /= n_runs
            permutation_scores[hemi][subject] /= n_runs
            pvalues[hemi][subject] /= n_runs

            # ------------------------------------------------------------
            # Save
            # ------------------------------------------------------------
            os.makedirs(PERM_DIR, exist_ok=True)

            with open(os.path.join(PERM_DIR, f"scores--{hemi}--{subject}.pkl"), "wb") as f:
                pickle.dump(scores[hemi][subject], f)

            with open(os.path.join(PERM_DIR, f"perm_scores--{hemi}--{subject}.pkl"), "wb") as f:
                pickle.dump(permutation_scores[hemi][subject], f)

            with open(os.path.join(PERM_DIR, f"pvals--{hemi}--{subject}.pkl"), "wb") as f:
                pickle.dump(pvalues[hemi][subject], f)

            print(f"Saved results for subject {subject}")

print("\nAll within-subject processing completed successfully.")