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


# # Decoding model
# 

# ### Between-subject classification

# In[ ]:


"""
Permutation-based searchlight decoding on hyperaligned/anatomically aligned data

This script loads hyperaligned/anatomically aligned training and test data, aggregates runs,
and performs manual permutation testing using a Linear SVM decoder
within searchlights on the cortical surface.

Outputs:
- Decoding accuracy per vertex
- Permutation distributions
- P-values (nonparametric)

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

# Silence convergence warnings
warnings.filterwarnings("ignore", category=ConvergenceWarning)

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


PERM_DIR = os.path.join(BASE_DIR, "permutations_HA") # or "permutations_AA" for anatomical alignment
os.makedirs(OUT_DIR, exist_ok=True)

test_data_file = os.path.join(PERM_DIR, "test_data.pkl")
train_data_file = os.path.join(PERM_DIR, "train_data.pkl")
test_label_file = os.path.join(PERM_DIR, "test_labels.pkl")
train_label_file = os.path.join(PERM_DIR, "train_labels.pkl")

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

with open(test_data_file, "rb") as f:
    test_data_dict = pickle.load(f)

with open(train_data_file, "rb") as f:
    train_data_dict = pickle.load(f)

with open(test_label_file, "rb") as f:
    test_label_dict = pickle.load(f)

with open(train_label_file, "rb") as f:
    train_label_dict = pickle.load(f)

subjects = list(test_data_dict["l"].keys())
print("Loaded subjects:", subjects)

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

radius = 10
batch_size = 60
n_permutations = 100
n_jobs = -1

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

# Memory guard (important for cluster runs)
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 (manual, searchlight-wise)
# ============================================================================

def permutation_test_searchlight(
    batch_indices,
    sls,
    train_data,
    test_data,
    train_labels,
    test_labels,
    decoder,
    n_permutations
):
    """
    Perform permutation testing for a batch of searchlights.
    """

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

    # concatenate runs for training
    train_data = np.concatenate(train_data, axis=0)

    for idx in batch_indices:
        sl = sls[idx]

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

        # True score
        clf = clone(decoder)
        clf.fit(X_train, train_labels)
        score = clf.score(X_test, test_labels)

        # Permutations
        perm_scores = []
        for _ in range(n_permutations):
            y_perm = rng.permutation(train_labels)
            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
# ============================================================================

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

for hemi in ["l", "r"]:

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

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

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

    for subject in tqdm(subjects, desc=f"{hemi} hemisphere"):

        print(f"Subject {subject}")

        # Output files (resume-safe)
        score_file = os.path.join(PERM_DIR, f"score--{hemi}--sub{subject}.pkl")
        perm_file = os.path.join(PERM_DIR, f"perm_scores--{hemi}--sub{subject}.pkl")
        pval_file = os.path.join(PERM_DIR, f"pvals_perm--{hemi}--sub{subject}.pkl")

        if all(os.path.exists(f) for f in [score_file, perm_file, pval_file]):
            print("  Skipping (already processed)")
            continue

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

        train_data = train_data_dict[hemi][subject]
        test_data = test_data_dict[hemi][subject]
        train_labels = train_label_dict[hemi][subject]
        test_labels = test_label_dict[hemi][subject]

        # ------------------------------------------------------------
        # Process in batches (memory-safe)
        # ------------------------------------------------------------
        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(
                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")

        check_memory_usage()
        gc.collect()

        # ------------------------------------------------------------
        # Save subject-level outputs
        # ------------------------------------------------------------
        with open(score_file, "wb") as f:
            pickle.dump(scores[hemi][subject], f)

        with open(perm_file, "wb") as f:
            pickle.dump(permutation_scores[hemi][subject], f)

        with open(pval_file, "wb") as f:
            pickle.dump(pvalues[hemi][subject], f)

print("\nAll processing completed successfully.")