Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs.
The 8 matches
- [1] § Materials and Methods › Predictive models ↔ LSVR_model.py, lines 37–114 · score 0.92 · temporal weight vector, Gram matrix, dual variables, Longitudinal Support Vector, temporal trends, insensitive
- [2] § Materials and Methods › Predictive models ↔ LSVR_model.py, lines 37–114 · score 0.81 · temporal trend vector, dual coefficients, dual variables, support vectors, QP, linear
- [3] § Materials and Methods › Predictive models ↔ LSVC_model.py, lines 95–176 · score 0.74 · temporal trend vector, dual coefficients, support vectors, QP, linear, iterative
- [4] § Materials and Methods › Predictive models ↔ LSVC_model.py, lines 27–93 · score 0.73 · Gram matrix, Longitudinal Support Vector, temporal trends, QP, dual, model
- [5] § Results › Prediction results › BCI performance prediction ↔ LSVR_model.py, lines 567–648 · score 0.70 · Square Error, LSVR model, continuous predicted, BCI score, RMSE, shuffled
- [6] § Materials and Methods › Predictive models ↔ LSVC_model.py, lines 27–93 · score 0.63 · Longitudinal Support Vector, temporal trend, LSVC, Classifier, model, matrix
- [7] § Materials and Methods › Predictive models ↔ LSVC_model.py, lines 95–176 · score 0.56 · dual coefficients, LSVC, linear, iterative, temporal, matrix
- [8] § Results › Prediction results ↔ LSVC_model.py, lines 528–573 · score 0.53 · Support Vector Classifier, classification model, BCI score, LSVC, longitudinal, predictive
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Python · 754 lines · 34 KB · no license · 5 matches
- # %%
- """
- ==============================================================
- Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs
- Longitudinal Support Vector Classification (LSVC) model
- ================================================================
- """
- # Authors: Camilla Mannino <[email hidden]>
- # License: BSD (3-clause)
- from qpsolvers import solve_qp
- import pickle
- from sklearn.metrics import confusion_matrix
- import matplotlib.pyplot as plt
- from matplotlib.lines import Line2D
- from sklearn.model_selection import LeaveOneOut
- from sklearn.svm import SVC
- from sklearn.linear_model import LogisticRegression
- from sklearn.ensemble import RandomForestClassifier
- from sklearn.metrics import balanced_accuracy_score
- import numpy as np
- import random
- #%% Functions
- def longitudinal_svc(X_list, y_list, C=1.0):
- """
- Trains a Longitudinal Support Vector Classifier (LSVC) model.
- Parameters:
- X_list : list of np.ndarray
- List of T x p feature matrices per subject.
- y_list : list of np.ndarray
- List of T-dimensional binary target vectors (0 or 1) per subject.
- C : float
- Regularization parameter.
- Returns:
- alpha : np.ndarray
- Dual coefficients.
- beta : np.ndarray
- Temporal trend vector of shape (T,).
- """
- N = len(X_list)
- T = X_list[0].shape[0]
- beta = np.ones(T)
- # Compute Gram matrix G where G[i,j] = beta^T X_i X_j^T beta
- G = np.zeros((N, N))
- for i in range(N):
- Xi = X_list[i]
- for j in range(N):
- Xj = X_list[j]
- G[i, j] = beta @ Xi @ Xj.T @ beta
- # Convert targets to +1/-1
- y_bin = np.array([2 * int(np.mean(y_list[i])) - 1 for i in range(N)]) # mean in case T > 1
- # Create QP matrices
- P = np.outer(y_bin, y_bin) * G
- P = (P + P.T) / 2 + 1e-6 * np.eye(N)
- q = -np.ones(N)
- # Constraints: 0 <= alpha_i <= C
- G_qp = np.vstack([np.eye(N), -np.eye(N)])
- h_qp = np.hstack([C * np.ones(N), np.zeros(N)])
- # Equality constraint: sum alpha_i * y_i = 0
- A_qp = y_bin.reshape(1, -1)
- b_qp = np.array([0.0])
- # Solve QP
- alpha = solve_qp(P, q, G_qp, h_qp, A_qp, b_qp, solver='quadprog')
- if alpha is None:
- raise ValueError("QP solver failed to find a solution!")
- # Compute beta from duals
- numerator = np.zeros(T)
- denominator = np.zeros((T, T))
- for i in range(N):
- yi = y_bin[i]
- Xi = X_list[i]
- coeff_i = alpha[i] * yi
- numerator += coeff_i * np.mean(y_list[i]) # again averaging if T > 1
- for j in range(N):
- yj = y_bin[j]
- Xj = X_list[j]
- coeff_j = alpha[j] * yj
- denominator += coeff_i * coeff_j * (Xi @ Xj.T)
- beta = np.linalg.solve(denominator + 1e-6 * np.eye(T), numerator)
- return alpha, beta, y_bin
- def longitudinal_svc_iterative(X_list, y_list, C=1.0, max_iter=200, tol=1e-4):
- """
- Iterative Longitudinal Support Vector Classifier (LSVC).
- Parameters:
- X_list : list of np.ndarray
- Each entry is a T x p matrix for a subject.
- y_list : list of int
- Class labels (0 or 1) for each subject.
- C : float
- Regularization parameter.
- max_iter : int
- Maximum number of iterations.
- tol : float
- Convergence tolerance on beta.
- Returns:
- alpha : np.ndarray
- Dual coefficients.
- beta : np.ndarray
- Temporal trend vector of shape (T,).
- """
- N = len(X_list)
- T = X_list[0].shape[0]
- # Binary labels {-1, +1}
- # Ensure one label per subject
- # If your y_list is a list of arrays of shape (T,) per subject:
- if isinstance(y_list[0], (list, np.ndarray)) and len(y_list[0]) > 1:
- y_bin = np.array([2 * int(round(np.mean(y))) - 1 for y in y_list])
- else:
- y_bin = np.array([2 * int(y) - 1 for y in y_list]) # Already per-subject
- # Initialize beta
- beta = np.ones(T)
- beta /= np.linalg.norm(beta)
- for it in range(max_iter):
- # --- Step 1: Solve QP for alpha given beta ---
- G = np.zeros((N, N))
- for i in range(N):
- Xi = X_list[i]
- for j in range(N):
- Xj = X_list[j]
- G[i, j] = beta @ Xi @ Xj.T @ beta
- P = np.outer(y_bin, y_bin) * G
- P = (P + P.T) / 2 + 1e-6 * np.eye(N) # Ensure positive semi-definite
- q = -np.ones(N)
- # Constraints
- G_qp = np.vstack([np.eye(N), -np.eye(N)])
- h_qp = np.hstack([C * np.ones(N), np.zeros(N)])
- A_qp = y_bin.reshape(1, -1)
- b_qp = np.array([0.0])
- alpha = solve_qp(P, q, G_qp, h_qp, A_qp, b_qp, solver='quadprog')
- if alpha is None:
- raise ValueError("QP solver failed to converge.")
- # --- Step 2: Update beta given alpha ---
- numerator = np.zeros(T)
- denominator = np.zeros((T, T))
- for i in range(N):
- ai_yi = alpha[i] * y_bin[i]
- Xi = X_list[i]
- for j in range(N):
- aj_yj = alpha[j] * y_bin[j]
- Xj = X_list[j]
- denominator += ai_yi * aj_yj * (Xi @ Xj.T)
- numerator += ai_yi * (Xi @ np.ones(Xi.shape[1])) # Linear aggregation
- # Regularize to ensure invertibility
- beta_new = np.linalg.solve(denominator + 1e-6 * np.eye(T), numerator)
- beta_new /= np.linalg.norm(beta_new)
- # Check convergence
- if np.linalg.norm(beta_new - beta) < tol:
- break
- beta = beta_new
- return alpha, beta, y_bin
- def predict_lsvc(X_new_list, support_vectors, alpha, beta, y_bin):
- preds = []
- N = len(support_vectors)
- for X_new in X_new_list:
- score = 0.0
- for i in range(N):
- Xi = support_vectors[i]
- score += alpha[i] * y_bin[i] * (beta @ X_new @ Xi.T @ beta)
- preds.append(1 if score >= 0 else 0)
- return np.array(preds)
- def vertical_dot_plot(y_true, y_pred, title, save_path=None):
- n_subjects = len(y_true)
- fig, ax = plt.subplots(figsize=(10, 4))
- for i, (yt, yp) in enumerate(zip(y_true, y_pred), start=1):
- y_pos = 1 if yt == 1 else 0.5
- color = 'green' if yt == yp else 'red'
- # Draw the vertical line
- ax.plot([i, i], [0, y_pos], color='black', linewidth=1)
- # Draw the circle
- ax.scatter(i, y_pos, color=color, s=200, zorder=3, edgecolors='black')
- # Reference horizontal dashed line at 0.57
- ax.axhline(0.57, color='teal', linewidth=2, linestyle='--')
- # Axis settings
- ax.set_xticks(range(1, n_subjects + 1))
- ax.set_xticklabels(range(1, n_subjects + 1))
- ax.set_xlabel("Subjects", fontsize=12, fontweight='bold')
- ax.set_ylabel("Performance", fontsize=12, fontweight='bold')
- ax.set_xlim(0.5, n_subjects + 0.5)
- ax.set_ylim(0, 1.5)
- ax.set_yticks([])
- ax.spines['top'].set_visible(False)
- ax.spines['right'].set_visible(False)
- # Custom axis arrows
- ax.plot([0, 0], [0, 1.5], color='teal', linewidth=2) # Y-axis line
- ax.plot([0.5, n_subjects + 0.5], [0, 0], color='teal', linewidth=2) # X-axis line
- # Add legend
- legend_elements = [
- Line2D([0], [0], marker='o', color='w', label='Correctly Classified',
- markerfacecolor='green', markersize=10, markeredgecolor='black'),
- Line2D([0], [0], marker='o', color='w', label='Miss Classified',
- markerfacecolor='red', markersize=10, markeredgecolor='black'),
- Line2D([0], [0], color='teal', linestyle='--', linewidth=2, label='Chance Level: 57%')
- ]
- ax.legend(handles=legend_elements, loc='upper right')
- ax.set_title(title, fontsize=14, fontweight='bold')
- plt.tight_layout()
- if save_path:
- plt.savefig(save_path, format='jpg', dpi=300)
- plt.show()
- #%%
- # ==== Step 1: Input Data ====
- # Avalanches Length features
- with open("Mean_Avalanches_MI_task_feedback_broad_1st_session.pkl", "rb") as f:
- subject_avalanche_analysis_MI_concat_1st = pickle.load(f)
- with open("Mean_Avalanches_Rest_task_feedback_broad_1st_session.pkl", "rb") as f:
- subject_avalanche_analysis_Rest_concat_1st = pickle.load(f)
- with open("Mean_Avalanches_MI_task_feedback_broad_2nd_session.pkl", "rb") as f:
- subject_avalanche_analysis_MI_concat_2nd = pickle.load(f)
- with open("Mean_Avalanches_Rest_task_feedback_broad_2nd_session.pkl", "rb") as f:
- subject_avalanche_analysis_Rest_concat_2nd = pickle.load(f)
- with open("Mean_Avalanches_MI_task_feedback_broad_3th_session.pkl", "rb") as f:
- subject_avalanche_analysis_MI_concat_3th = pickle.load(f)
- with open("Mean_Avalanches_Rest_task_feedback_broad_3th_session.pkl", "rb") as f:
- subject_avalanche_analysis_Rest_concat_3th = pickle.load(f)
- with open("Mean_Avalanches_MI_task_feedback_broad_4th_session.pkl", "rb") as f:
- subject_avalanche_analysis_MI_concat_4th = pickle.load(f)
- with open("Mean_Avalanches_Rest_task_feedback_broad_4th_session.pkl", "rb") as f:
- subject_avalanche_analysis_Rest_concat_4th = pickle.load(f)
- #%% Activations Features
- mean_activation_1 = np.load('/Users/camilla.mannino/Library/CloudStorage/OneDrive-ICM/paper_preATM/activations_1st&2nd_tot_mean_broad_weighted.pkl', allow_pickle=True)
- mean_activation_2 = np.load('/Users/camilla.mannino/Library/CloudStorage/OneDrive-ICM/paper_preATM/activations_3rd&4th_tot_mean_broad_weighted.pkl', allow_pickle=True)
- mean_activation_MI_1st = mean_activation_1['activations_MI_1st_tot_mean_broad_weigh']
- mean_activation_Rest_1st = mean_activation_1['activations_Rest_1st_tot_mean_broad_weigh']
- mean_activation_MI_2nd = mean_activation_1['activations_MI_2nd_tot_mean_broad_weigh']
- mean_activation_Rest_2nd = mean_activation_1['activations_Rest_2nd_tot_mean_broad_weigh']
- mean_activation_MI_3rd = mean_activation_2['activations_MI_3rd_tot_mean_broad_weigh']
- mean_activation_Rest_3rd = mean_activation_2['activations_Rest_3rd_tot_mean_broad_weigh']
- mean_activation_MI_4th = mean_activation_2['activations_MI_4th_tot_mean_broad_weigh']
- mean_activation_Rest_4th = mean_activation_2['activations_Rest_4th_tot_mean_broad_weigh']
- #%% Load Data over a set of selected ROIs
- # Avalanches Length Features
- #with open("Mean_Avalanches_MI_task_feedback_broad_1st_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_MI_concat_1st = pickle.load(f)
- #with open("Mean_Avalanches_Rest_task_feedback_broad_1st_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_Rest_concat_1st = pickle.load(f)
- #with open("Mean_Avalanches_MI_task_feedback_broad_2nd_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_MI_concat_2nd = pickle.load(f)
- #with open("Mean_Avalanches_Rest_task_feedback_broad_2nd_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_Rest_concat_2nd = pickle.load(f)
- #with open("Mean_Avalanches_MI_task_feedback_broad_3rd_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_MI_concat_3th = pickle.load(f)
- #with open("Mean_Avalanches_Rest_task_feedback_broad_3rd_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_Rest_concat_3th = pickle.load(f)
- #with open("Mean_Avalanches_MI_task_feedback_broad_4th_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_MI_concat_4th = pickle.load(f)
- #with open("Mean_Avalanches_Rest_task_feedback_broad_4th_session_selected_ROIs_preATM_normalized.pkl", "rb") as f:
- # subject_avalanche_analysis_Rest_concat_4th = pickle.load(f)
- # Activations Features
- #mean_activation = np.load('/Users/camilla.mannino/Library/CloudStorage/OneDrive-ICM/paper_preATM/activations_tot_mean_broad_selected_ROIs_preATM.pkl', allow_pickle=True)
- #mean_activation_MI_1st = mean_activation['activations_MI_1st_tot_mean_broad']
- #mean_activation_Rest_1st = mean_activation['activations_Rest_1st_tot_mean_broad']
- #mean_activation_MI_2nd = mean_activation['activations_MI_2nd_tot_mean_broad']
- #mean_activation_Rest_2nd = mean_activation['activations_Rest_2nd_tot_mean_broad']
- #mean_activation_MI_3rd = mean_activation['activations_MI_3rd_tot_mean_broad']
- #mean_activation_Rest_3rd = mean_activation['activations_Rest_3rd_tot_mean_broad']
- #mean_activation_MI_4th = mean_activation['activations_MI_4th_tot_mean_broad']
- #mean_activation_Rest_4th = mean_activation['activations_Rest_4th_tot_mean_broad']
- #%% Mean across trials for each subject inside each parameters' combination excluding trials without avalanches
- subjects = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
- # Helper function to compute mean considering only values > 0
- def mean_gt_zero(values):
- values_gt_zero = [v for v in values if v > 0]
- return np.mean(values_gt_zero) if values_gt_zero else 0
- # ---------- First Interval ----------
- max_cardinal_order_MI_1st = max(len(subject_avalanche_analysis_MI_concat_1st[subject]) for subject in subjects)
- mean_length_array_MI_1st = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_MI_1st)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_MI_concat_1st[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_MI_concat_1st[subject][key]["mean_lengths_per_trial"])
- mean_length_array_MI_1st[cardinal_position][subject_index] = mean_val
- max_cardinal_order_Rest_1st = max(len(subject_avalanche_analysis_Rest_concat_1st[subject]) for subject in subjects)
- mean_length_array_Rest_1st = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_Rest_1st)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_Rest_concat_1st[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_Rest_concat_1st[subject][key]["mean_lengths_per_trial"])
- mean_length_array_Rest_1st[cardinal_position][subject_index] = mean_val
- # ---------- Second Interval ----------
- max_cardinal_order_MI_2nd = max(len(subject_avalanche_analysis_MI_concat_2nd[subject]) for subject in subjects)
- mean_length_array_MI_2nd = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_MI_2nd)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_MI_concat_2nd[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_MI_concat_2nd[subject][key]["mean_lengths_per_trial"])
- mean_length_array_MI_2nd[cardinal_position][subject_index] = mean_val
- max_cardinal_order_Rest_2nd = max(len(subject_avalanche_analysis_Rest_concat_2nd[subject]) for subject in subjects)
- mean_length_array_Rest_2nd = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_Rest_2nd)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_Rest_concat_2nd[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_Rest_concat_2nd[subject][key]["mean_lengths_per_trial"])
- mean_length_array_Rest_2nd[cardinal_position][subject_index] = mean_val
- # ---------- Third Interval ----------
- max_cardinal_order_MI_3rd = max(len(subject_avalanche_analysis_MI_concat_3th[subject]) for subject in subjects)
- mean_length_array_MI_3rd = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_MI_3rd)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_MI_concat_3th[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_MI_concat_3th[subject][key]["mean_lengths_per_trial"])
- mean_length_array_MI_3rd[cardinal_position][subject_index] = mean_val
- max_cardinal_order_Rest_3rd = max(len(subject_avalanche_analysis_Rest_concat_3th[subject]) for subject in subjects)
- mean_length_array_Rest_3rd = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_Rest_3rd)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_Rest_concat_3th[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_Rest_concat_3th[subject][key]["mean_lengths_per_trial"])
- mean_length_array_Rest_3rd[cardinal_position][subject_index] = mean_val
- # ---------- Fourth Interval ----------
- max_cardinal_order_MI_4th = max(len(subject_avalanche_analysis_MI_concat_4th[subject]) for subject in subjects)
- mean_length_array_MI_4th = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_MI_4th)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_MI_concat_4th[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_MI_concat_4th[subject][key]["mean_lengths_per_trial"])
- mean_length_array_MI_4th[cardinal_position][subject_index] = mean_val
- max_cardinal_order_Rest_4th = max(len(subject_avalanche_analysis_Rest_concat_4th[subject]) for subject in subjects)
- mean_length_array_Rest_4th = [np.zeros(len(subjects)) for _ in range(max_cardinal_order_Rest_4th)]
- for subject_index, subject in enumerate(subjects):
- subject_keys = list(subject_avalanche_analysis_Rest_concat_4th[subject].keys())
- for cardinal_position, key in enumerate(subject_keys):
- mean_val = mean_gt_zero(
- subject_avalanche_analysis_Rest_concat_4th[subject][key]["mean_lengths_per_trial"])
- mean_length_array_Rest_4th[cardinal_position][subject_index] = mean_val
- #%% Difference avalanches' length computation for all sessions: Avalanches length
- mean_length_diff = {}
- for session_index, mean_length_array_Rest, mean_length_array_MI in zip(
- range(1, 5),
- [mean_length_array_Rest_1st, mean_length_array_Rest_2nd, mean_length_array_Rest_3rd, mean_length_array_Rest_4th],
- [mean_length_array_MI_1st, mean_length_array_MI_2nd, mean_length_array_MI_3rd, mean_length_array_MI_4th],
- ):
- mean_length_diff[session_index] = {
- cardinal_position: mean_length_array_Rest[cardinal_position] - mean_length_array_MI[cardinal_position]
- for cardinal_position in range(len(mean_length_array_Rest))
- }
- #%% Difference activations computation for all sessions: Activations
- mean_activation_diff = {}
- for session_index, mean_activation_array_Rest, mean_activation_array_MI in zip(range(1, 5),
- [mean_activation_Rest_1st, mean_activation_Rest_2nd, mean_activation_Rest_3rd, mean_activation_Rest_4th],
- [mean_activation_MI_1st, mean_activation_MI_2nd, mean_activation_MI_3rd, mean_activation_MI_4th],):
- mean_activation_diff[session_index] = {
- cardinal_position: {
- key: mean_activation_array_Rest[cardinal_position][key] - mean_activation_array_MI[cardinal_position][key]
- for key in mean_activation_array_Rest[cardinal_position]
- }
- for cardinal_position in range(len(mean_activation_array_Rest))
- }
- #%%
- n_cardinal_position = 10
- BCI_scores = {
- 1: np.array([73.8, 45, 66.1, 50, 51.7, 61.2, 54.4, 59.4, 52.8, 42.2, 53.9, 47.22, 53.3, 54.17, 60, 76.1, 48.33, 56, 53.89, 45]),
- 2: np.array([90.2, 35.6, 61.7, 50, 51.7, 50.6, 58.9, 66.11, 50, 45.6, 45, 52.22, 65.6, 50, 68.9, 70.6, 61.67, 63.89, 53.89, 57.22]),
- 3: np.array([86.6, 38.9, 72.8, 62.8, 61.7, 79.3, 51.7, 56.67, 68.9, 58.9, 55.6, 47.78, 57.8, 60, 76.1, 53.9, 66, 72, 52.78, 58.33]),
- 4: np.array([89.3, 38.1, 86.1, 73.1, 63.9, 88.9, 56.7, 70.0, 60.0, 66.1, 63.3, 53.9, 77.2, 69.2, 80.0, 61.1, 71.0, 73.0, 68.3, 56.7])
- }
- # 57.8 is the chance level - we set it as threshold
- binary_BCI_scores = {
- key: (values > 57).astype(int)
- for key, values in BCI_scores.items()
- }
- #%%
- # Parameters
- n_subjects = 20
- n_sessions = 4
- results = {}
- for cardinal_position in range(10):
- X_data = {session: [] for session in range(1, n_sessions + 1)}
- Y_data = binary_BCI_scores
- X_temporal_list = []
- for subj in range(n_subjects):
- features_per_subject = []
- for session in range(1, n_sessions + 1):
- length_feat = mean_length_diff[session][cardinal_position][subj]
- activations_feat = mean_activation_diff[session][cardinal_position][subj]
- features_matrix = np.array([length_feat, activations_feat])
- X_data[session].append(features_matrix)
- features_per_subject.append(features_matrix)
- y_true_session4 = Y_data[4]
- X_list, y_list = [], []
- for subj in range(n_subjects):
- X_subj = np.vstack([X_data[1][subj], X_data[2][subj], X_data[3][subj]])
- y_subj = np.array([Y_data[1][subj], Y_data[2][subj], Y_data[3][subj]])
- X_list.append(X_subj)
- y_list.append(y_subj)
- best_acc = 0
- best_acc_perc = 0
- best_acc_preds = []
- # Define grid of hyperparameters
- C_grid = [0.01, 0.1, 1, 10]
- tol_grid = [1e-10, 1e-9, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1]
- max_iter_grid = [50,100, 200]
- grid_results = {}
- # Store results for each value of C
- results_normal = {}
- for C in C_grid:
- y_preds_normal = []
- # Normal Model (SVC)
- normal_model = SVC(C=C, kernel='linear')
- # Perform Leave-One-Out Cross Validation (LOO)
- loo = LeaveOneOut()
- for train_idx, test_idx in loo.split(X_list):
- X_train = np.vstack([X_list[i] for i in train_idx])
- y_train = np.concatenate([y_list[i] for i in train_idx])
- X_test = np.vstack([X_list[i] for i in test_idx])
- # Train and predict with the normal model
- normal_model.fit(X_train, y_train)
- y_pred_normal = normal_model.predict(X_test)
- y_preds_normal.append(y_pred_normal[0])
- # Calculate accuracy for the normal model
- acc_normal = balanced_accuracy_score(y_true_session4, y_preds_normal)
- # Confusion matrix for the normal model
- cm_normal = confusion_matrix(y_true_session4, y_preds_normal)
- # Plot for normal SVC
- # vertical_dot_plot(y_true_session4, y_preds_normal, "SVC, Accuracy: 41%", save_path="normal_svc_results_new_2.jpg")
- # Store results for the current value of C
- results_normal[C] = {'Accuracy Normal': acc_normal, 'Confusion Matrix': cm_normal}
- # Print the results for the current value of C
- print(f"Results for Normal Model with C={C}:, Cardinal_position:{cardinal_position}")
- print(f"Accuracy Normal, Cardinal_position: {cardinal_position}: {acc_normal:.2f}")
- print(f"Confusion Matrix Normal, Cardinal_position {cardinal_position}:\n{cm_normal}")
- # Optional: Print the best model (highest accuracy) after the loop
- best_C = max(results_normal, key=lambda k: results_normal[k]['Accuracy Normal'])
- best_result = results_normal[best_C]
- print(f"Best C, Cardinal_position {cardinal_position}: {best_C}")
- print(f"Best Accuracy Normal, Cardinal_position {cardinal_position}: {best_result['Accuracy Normal']:.2f}")
- print(f"Best Confusion Matrix, Cardinal_position {cardinal_position}:\n{best_result['Confusion Matrix']}")
- #%% Logitudinal Support Vector Classifier Model
- # Hyperparameter grid search
- for C in C_grid:
- for tol in tol_grid:
- for max_iter in max_iter_grid:
- y_preds_lsvc = []
- # Perform Leave-One-Out Cross Validation (LOO)
- loo = LeaveOneOut()
- for train_idx, test_idx in loo.split(X_list):
- X_train = [X_list[i] for i in train_idx]
- y_train = [y_list[i] for i in train_idx]
- X_test = [X_list[i] for i in test_idx]
- try:
- alpha, beta, y_bin_train = longitudinal_svc_iterative(X_train, y_train, C=C, tol=tol,
- max_iter=max_iter)
- y_pred_lsvc = predict_lsvc(X_test, X_train, alpha, beta, y_bin_train)[0]
- except Exception as e:
- print(f"Skipped config C={C}, tol={tol}, max_iter={max_iter} due to error: {e}")
- y_pred_lsvc = 0 # Fallback value
- y_preds_lsvc.append(y_pred_lsvc)
- # Calculate accuracy and store results
- acc_l = balanced_accuracy_score(y_true_session4, y_preds_lsvc)
- grid_results[(C, tol, max_iter)] = {'Accuracy Long': acc_l, 'Predictions Long': y_preds_lsvc}
- # Find best hyperparameter configuration
- best_config = max(grid_results, key=lambda k: grid_results[k]['Accuracy Long'])
- best_result = grid_results[best_config]
- # Get predicted BCI scores
- y_true = np.array(Y_data[4]) # Actual BCI scores for session 4
- y_pred = np.array(best_result['Predictions Long']) # Predicted BCI scores for session 4
- # Compute confusion matrix
- cm = confusion_matrix(y_true, y_pred)
- #vertical_dot_plot(y_true_session4, y_pred, "LSVC, Accuracy: 88%", save_path="lsvc_results_new_2.jpg")
- print(f"Confusion Matrix for Cardinal Position {cardinal_position}:")
- print(cm)
- # Print best hyperparameter configuration and results
- print(f"Best configuration, Cardinal_position:{cardinal_position}: C={best_config[0]}, tol={best_config[1]}, max_iter={best_config[2]}")
- print(f"Best Accuracy, Cardinal_position:{cardinal_position}: {best_result['Accuracy Long']:.2f}")
- print(f"Predictions, Cardinal_position:{cardinal_position}: {best_result['Predictions Long']}")
- #%% Longitudinal Support Vector Classifier random session
- # Data structure to hold the features per session
- for cardinal_position in range(10):
- X_data = {session: [] for session in range(1, n_sessions + 1)} # Initialize X_data
- # Step 1: Extract features for each subject and session
- for subj in range(n_subjects):
- for session in range(1, n_sessions + 1):
- # Placeholder for actual feature extraction (replace with your real data extraction)
- length_feat = mean_length_diff[session][cardinal_position][subj]
- activations_feat = mean_activation_diff[session][cardinal_position][subj]
- features_matrix = np.array([length_feat, activations_feat])
- X_data[session].append(features_matrix)
- # Step 2: Shuffle the sessions for each subject independently (shuffling only X_data, not Y_data)
- shuffled_X_data = {session: [] for session in range(1, n_sessions + 1)}
- for subj in range(n_subjects):
- sessions_order = np.random.permutation(n_sessions) # Shuffle the sessions for each subject
- for i, session_idx in enumerate(sessions_order):
- session = session_idx + 1 # Session number starts from 1
- shuffled_X_data[session].append(X_data[session][subj])
- # Step 3: Create the final X_data and y_data
- X_list, y_list = [], []
- for subj in range(n_subjects):
- # Stack the shuffled session data for each subject
- X_subj = np.vstack([shuffled_X_data[session][subj] for session in range(1, n_sessions + 1)])
- # Keep the original labels (y_subj) for each subject intact (no shuffling of Y_data)
- y_subj = np.array([Y_data[1][subj], Y_data[2][subj], Y_data[3][subj]])
- X_list.append(X_subj)
- y_list.append(y_subj)
- # Convert to numpy arrays for easy handling during model training
- X_data_final = np.vstack(X_list)
- y_data_final = np.array(y_list)
- print("Shuffling Session complete. X_data_final and y_data_final are ready for further analysis.")
- # Step 4: Hyperparameter grid search initialization
- grid_results = {}
- # Step 5: Perform Leave-One-Out Cross Validation (LOO) and grid search for hyperparameters
- y_true_session4 = Y_data[4] # Labels for session 4
- best_acc = 0
- best_acc_perc = 0
- best_acc_preds = []
- for C in C_grid:
- for tol in tol_grid:
- for max_iter in max_iter_grid:
- y_preds = []
- # Leave-One-Out Cross Validation
- loo = LeaveOneOut()
- for train_idx, test_idx in loo.split(X_list):
- X_train = [X_list[i] for i in train_idx]
- y_train = [y_list[i] for i in train_idx]
- X_test = [X_list[i] for i in test_idx]
- try:
- # Replace this with your actual model training and prediction functions
- alpha, beta, y_bin_train = longitudinal_svc_iterative(X_train, y_train, C=C, tol=tol, max_iter=max_iter)
- y_pred_lsvc = predict_lsvc(X_test, X_train, alpha, beta, y_bin_train)[0]
- except Exception as e:
- print(f"Skipped config C={C}, tol={tol}, max_iter={max_iter} due to error: {e}")
- y_pred_lsvc = 0 # Fallback value
- y_preds.append(y_pred_lsvc)
- # Evaluate and store results
- acc = balanced_accuracy_score(y_true_session4, y_preds)
- grid_results[(C, tol, max_iter)] = {'Accuracy': acc, 'Predictions': y_preds}
- # Step 6: Find best hyperparameter configuration
- best_config = max(grid_results, key=lambda k: grid_results[k]['Accuracy'])
- best_result = grid_results[best_config]
- # Step 7: Get predicted and actual BCI scores
- y_true = np.array(Y_data[4]) # Actual BCI scores for session 4
- y_pred = np.array(best_result['Predictions']) # Predicted BCI scores for session 4
- # Step 8: Compute confusion matrix
- cm = confusion_matrix(y_true, y_pred)
- print(f"Confusion Matrix:")
- print(cm)
- # vertical_dot_plot(y_true_session4, y_pred, "LSVC, Random Sessions, Accuracy: 47%", save_path="lsvc_results_random_session_new2.jpg")
- # Step 9: Print best hyperparameter configuration and results
- print(f"Best configuration, Cardinal_position:{cardinal_position}: C={best_config[0]}, tol={best_config[1]}, max_iter={best_config[2]}")
- print(f"Best Accuracy, Cardinal_position:{cardinal_position}: {best_result['Accuracy']:.2f}")
- print(f"Predictions, Cardinal_position:{cardinal_position}: {best_result['Predictions']}")
- #%% Longitudinal Support Vector Classifier with Random Session and Random Subjects
- # Set seeds for reproducibility
- #seed_value = 24
- #np.random.seed(seed_value)
- #random.seed(seed_value)
- #for cardinal_position in range(10):
- # Extract features per subject and session
- # for subj in range(n_subjects):
- # for session in range(1, n_sessions + 1):
- # length_feat = mean_length_diff[session][cardinal_position][subj]
- # activations_feat = mean_activation_diff[session][cardinal_position][subj]
- # features_matrix = np.array([length_feat, activations_feat])
- # X_data[session].append(features_matrix)
- # Shuffle the sessions for each subject independently
- # shuffled_X_data = {session: [] for session in range(1, n_sessions + 1)}
- # for subj in range(n_subjects):
- # sessions_order = np.random.permutation(n_sessions)
- # for i, session_idx in enumerate(sessions_order):
- # session = session_idx + 1
- # shuffled_X_data[session].append(X_data[session][subj])
- # Shuffle subjects themselves
- # subject_order = np.random.permutation(n_subjects)
- # X_list, y_list = [], []
- # for subj_idx in subject_order:
- # X_subj = np.vstack([shuffled_X_data[session][subj_idx] for session in range(1, n_sessions + 1)])
- # y_subj = np.array([Y_data[session][subj_idx] for session in range(1, n_sessions + 1)])
- # X_list.append(X_subj)
- # y_list.append(y_subj)
- # X_data_final = np.vstack(X_list)
- # y_data_final = np.hstack(y_list)
- # print("Shuffling Subj + Sessions complete. X_data_final and y_data_final are ready for further analysis.")
- # Hyperparameter grid search initialization
- # grid_results = {}
- # y_true_session4 = Y_data[4]
- # best_acc = 0
- # best_acc_perc = 0
- # best_acc_preds = []
- # for C in C_grid:
- # for tol in tol_grid:
- # for max_iter in max_iter_grid:
- # y_preds = []
- # loo = LeaveOneOut()
- # for train_idx, test_idx in loo.split(X_list):
- # X_train = [X_list[i] for i in train_idx]
- # y_train = [y_list[i] for i in train_idx]
- # X_test = [X_list[i] for i in test_idx]
- # try:
- # alpha, beta, y_bin_train = longitudinal_svc_iterative(X_train, y_train, C=C, tol=tol,
- # max_iter=max_iter)
- # y_pred_lsvc = predict_lsvc(X_test, X_train, alpha, beta, y_bin_train)[0]
- # except Exception as e:
- # print(f"Skipped config C={C}, tol={tol}, max_iter={max_iter} due to error: {e}")
- # y_pred_lsvc = 0
- # y_preds.append(y_pred_lsvc)
- # acc = balanced_accuracy_score(y_true_session4, y_preds)
- # grid_results[(C, tol, max_iter)] = {'Accuracy': acc, 'Predictions': y_preds}
- # best_config = max(grid_results, key=lambda k: grid_results[k]['Accuracy'])
- # best_result = grid_results[best_config]
- # y_true = np.array(Y_data[4])
- # y_pred = np.array(best_result['Predictions'])
- # cm = confusion_matrix(y_true, y_pred)
- # print(f"Confusion Matrix for Cardinal Position {cardinal_position}:")
- # print(cm)
- # vertical_dot_plot(y_true_session4, y_pred, "LSVC, Random Sessions & Random Subjects, Accuracy: 62%",
- # save_path="lsvc_results_random_subject&sessions_new2.jpg")
- # print(
- # f"Best configuration, Cardinal_position:{cardinal_position}: C={best_config[0]}, tol={best_config[1]}, max_iter={best_config[2]}")
- # print(f"Best Accuracy, Cardinal_position:{cardinal_position}: {best_result['Accuracy']:.2f}")
- # print(f"Predictions, Cardinal_position:{cardinal_position}: {best_result['Predictions']}")
LSVC_model.py at commit 45a88f8, no license · at the source
Overview
- Sorbonne Université, Paris Brain Institute-ICM, CNRS, Inria, Inserm, AP-HP, Hôpital de la Pitié Salpêtrière, Paris, France
- Institut de Neurosciences des Systèmes, Aix-Marseille Université, Marseille, France
- Department of Biomedical Sciences, University of Sassari, Sassari, Italy
Abstract
Motor imagery-based Brain-Computer Interfaces (BCIs) restore control in persons with motor impairments, but up to 30% of users struggle, a phenomenon known as “BCI inefficiency”. This study tackles a key limitation of current protocol: the use of fixed-length sessions training paradigms that ignore individual learning variability. We propose a novel approach based on neuronal avalanches, spatiotemporal cascades of brain activities, as biomarkers to characterize and predict user-specific learning. From electroencephalography data across four sessions in 20 subjects, we characterized avalanches by their length and their spatiotemporal size. These features showed significant training and task effects and were found to correlate to BCI performance across sessions. We further assessed their ability to predict BCI success through longitudinal models, achieving up to 91% accuracy, improved by spatial filtering on selected brain regions. These findings demonstrate the utility of neuronal avalanche dynamics as robust biomarkers for BCI training, supporting the development of personalized protocols aimed at mitigating BCI illiteracy.
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 8 matches between paragraphs and lines of code.
CamiMannino/Neuronal-avalanches-as-a-predictive-biomarker-for-guiding-tailored-BCI-training-programs
45a88f8b7911f116d0359b5433c72acd2bd05636, 11 August 2025Availability: 1 check, the latest on 28 September 2026: the link answers
- 28 September 2026: the link answers
10 files
- Activations_across_sessi
ons.py , Python, 2,147 lines - Activations_across_sessi
ons_selected_ROIs.py , Python, 1,594 lines - Hit_Miss_Activations.py, Python, 1,082 lines
- Hit_Miss_Avalanches_leng
th.py , Python, 1,051 lines - LSVC_model.py, Python, 754 lines, 5 matches
- LSVR_model.py, Python, 745 lines, 3 matches
- Mean_Avalanches_Length_a
cross_sessions.py , Python, 2,247 lines - Mean_Avalanches_Length_a
cross_sessions_slected_R , Python, 2,089 linesOIs.py - Occurence_ROIs_preATM_we
ighted_hit_miss.py , Python, 1,129 lines - README.md, Text, 6 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 9 scripts, each with its path and the digest of its content;
- 8 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data and Code Availability
The study dataset has been fully collected and curated. A dedicated data-descriptor paper is currently in preparation; upon its acceptance, the de-identified dataset and full documentation will be deposited in an open repository with a citable DOI and made publicly available. Until release, controlled access may be granted on reasonable request to the corresponding author.
Code used to import data, analyse data, and generate manuscript figures are available on GitHub: https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 28 September 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 4 authors, 9 keywords, 49 references.
Cite
This paper
Mannino, C., Sorrentino, P., Chavez, M., & Corsi, M.-C. (2026). Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1259. https://
BibTeX
@article{mannino2026neur
author = {Mannino, Camilla and Sorrentino, Pierpaolo and Chavez, Mario and Corsi, Marie-Constance},
title = {{Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = may,
volume = {4},
pages = {IMAG.a.1259},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/
url = {https://
pmid = {42232076},
pmcid = {PMC13224313}
}
RIS
TY - JOUR
AU - Mannino, Camilla
AU - Sorrentino, Pierpaolo
AU - Chavez, Mario
AU - Corsi, Marie-Constance
TI - Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/
VL - 4
SP - IMAG.a.1259
SN - 2837-6056
PB - MIT Press
DO - 10.1162/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1162/
"type": "article-journal",
"title": "Neuronal avalanches as a predictive biomarker for guiding tailored BCI training programs",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Mannino",
"given": "Camilla"
},
{
"family": "Sorrentino",
"given": "Pierpaolo"
},
{
"family": "Chavez",
"given": "Mario"
},
{
"family": "Corsi",
"given": "Marie-Constance"
}
],
"container-title-short":
"volume": "4",
"page": "IMAG.a.1259",
"DOI": "10.1162/
"PMID": "42232076",
"PMCID": "PMC13224313",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
29
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1162/imag.a.1348 [code]
- The role of high-amplitude bursts of high-gamma activity in naturalistic speech and music listening.Journal: Imaging neuroscience (Cambridge, Mass.)In common: MNE-Python, seaborn, pandas, 3 other tools, 5 references
- [2] doi:10.1162/imag.a.1193
- Linking local and large-scale salient events with oscillatory and broadband arrhythmic activities in the resting human brain.Journal: Imaging neuroscience (Cambridge, Mass.)In common: 5 references, author Pierpaolo Sorrentino
- [3] doi:10.7554/elife.100605 [code]
- Age-related changes in ‘cortical’ 1/
f dynamics are linked to cardiac activity Journal: n/aIn common: Pingouin, MNE-Python, statsmodels, 6 other tools, 1 reference - [4] doi:10.7554/elife.108673 [code]
- Adaptive behavior is guided by integrated representations of controlled and non-controlled information.Journal: eLifeIn common: Pingouin, MNE-Python, statsmodels, 6 other tools, EEG
- [5] doi:10.1162/imag.a.1269 [code]
- From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Pingouin, MNE-Python, statsmodels, 6 other tools, EEG
- [6] doi:10.1093/cercor/bhag077 [code]
- The longitudinal development of intrinsic timescales in infancy and their relation to alpha brain rhythm.Journal: Cerebral cortex (New York, N.Y. : 1991)In common: Pingouin, MNE-Python, statsmodels, 6 other tools, EEG
- [7] doi:10.1038/s41597-026-07350-9 [code]
- An open multi-center MEG-EEG dataset for studying conscious visual perception.Journal: Scientific dataIn common: Pingouin, MNE-Python, statsmodels, 6 other tools, EEG
- [8] doi:10.1038/s41531-026-01380-1 [code]
- Identifying maximal beta power from directional subthalamic local field potentials in Parkinson's disease.Journal: NPJ Parkinson's diseaseIn common: Pingouin, MNE-Python, statsmodels, 6 other tools, clinical / translational
- [9] doi:10.1162/imag.a.1245 [code]
- Towards precision EEG connectomics: Evaluating the benefits of dense sampling.Journal: Imaging neuroscience (Cambridge, Mass.)In common: Pingouin, MNE-Python, statsmodels, 5 other tools, EEG, 1 reference
- [10] doi:10.1038/s41467-026-75662-w [code]
- Distinct Roles of Deep and Superficial Cortical Layers in Tone Prediction, Comparison, and Adaptation in Human Auditory Cortices.Journal: Nature communicationsIn common: Pingouin, MNE-Python, statsmodels, 6 other tools
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 9 scripts, and 8 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:5ed955c13fe3cbe8…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
