Intracortical brain-computer interface for navigation in virtual reality in macaque monkeys.
The 10 matches
- [1] § MATERIALS AND METHODS › Statistical analysis › Offline decoding analysis per cortical region ↔ src/analysis.py, lines 76–171 · score 0.78 · cortical areas, Online Simulation, offline decoding, Online Decoding, Wilcoxon, pairwise
- [2] § MATERIALS AND METHODS › Statistical analysis › Within-session adaptation analysis ↔ src/analysis.py, lines 555–621 · score 0.67 · linear regression, monkey task, slope, classified, decline, smoothed
- [3] § RESULTS › Offline decoding reveals regional contributions and the importance of closed-loop dynamics ↔ src/analysis.py, lines 76–171 · score 0.64 · Online Simulation, Offline decoding, Online Decoding, median, Wilcoxon, IQR
- [4] § MATERIALS AND METHODS › Statistical analysis › Influence of sEMG on decoded velocity ↔ src/analysis.py, lines 487–519 · score 0.64 · Spearman correlation, velocity components, polynomial, MSE, R2, cross
- [5] § RESULTS › Influence of surface electromyography and eye movements on decoded velocities ↔ src/metrics.py, lines 39–59 · score 0.63 · square error, velocity components, MSE, R2, Cross, models
- [6] § RESULTS › Offline decoding reveals regional contributions and the importance of closed-loop dynamics ↔ scripts/run_offline_brain_area_analysis.py, lines 26–62 · score 0.63 · brain area, Online Simulation, Online Decoding, offline, matched, M1
- [7] § MATERIALS AND METHODS › Statistical analysis › Offline decoding analysis per cortical region ↔ scripts/run_offline_brain_area_analysis.py, lines 1–18 · score 0.62 · cortical areas, offline decoding, Online Simulation, Monkey
- [8] § MATERIALS AND METHODS › Statistical analysis › Offline decoding analysis per cortical region ↔ scripts/run_offline_brain_area_analysis.py, lines 1–18 · score 0.56 · cortical area, brain area, offline, online, decoding, monkeys
- [9] § MATERIALS AND METHODS › Statistical analysis › Reaction time ↔ src/metrics.py, lines 122–150 · score 0.56 · target jump, tangents, reaction, deviation, target position, threshold
- [10] § MATERIALS AND METHODS › Statistical analysis › Influence of eye movements on decoded velocity ↔ src/analysis.py, lines 437–485 · score 0.56 · eye movement, velocity components, Spearman correlation, segmented, monkey
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 · 698 lines · 31 KB · MIT · 5 matches
- from typing import List, Tuple
- import numpy as np
- import pandas as pd
- from scipy.stats import linregress
- from src.load import load_files
- from src.utils import is_within_target_window, is_within_target_window_3D, make_df_structure, process_emg_data, process_eyemovement_data
- from src.metrics import spearman_correlation, linear_regression, polynomial_regression
- from src.load import load_emg_eyemovement_files
- from src.metrics import combined_p_value, compute_iqr
- from scipy.stats import ttest_ind, shapiro, mannwhitneyu
- from src.plots import plot_3D_endpoints_density_plots, plot_success_rates_offline_decoding
- import matplotlib.pyplot as plt
- import seaborn as sns
- import matplotlib as mpl
- from collections import defaultdict
- from scipy.stats import wilcoxon
- mpl.rcParams['svg.fonttype'] = 'none'
- def compute_offline_success_rate(trials, allPredictions, task, execution):
- success_count = 0
- failure_reasons = defaultdict(int)
- total_trials = len(trials)
- dt = 0.05
- for trial_index in range(total_trials):
- trial = trials[trial_index]
- predictions = allPredictions[trial_index]
- if execution == "offline":
- # Compute positions from predictions
- x_positions_pred = [0.0]
- y_positions_pred = [0.0]
- z_positions_pred = [0.0]
- for j in range(1, len(predictions)):
- if isinstance(predictions[j], int):
- continue # Skip if the prediction is an integer
- prev_x, prev_y, prev_z = x_positions_pred[-1], y_positions_pred[-1], z_positions_pred[-1]
- current_x_velocity = float(predictions[j]['x'])
- current_y_velocity = float(predictions[j]['y'])
- current_z_velocity = float(predictions[j]['z'])
- new_x_position = prev_x + current_x_velocity * dt
- new_y_position = prev_y + current_y_velocity * dt
- new_z_position = prev_z + current_z_velocity * dt
- x_positions_pred.append(new_x_position)
- y_positions_pred.append(new_y_position)
- z_positions_pred.append(new_z_position)
- else:
- x_positions_pred = trial.avatarTrajectory['x']
- y_positions_pred = trial.avatarTrajectory['y']
- z_positions_pred = trial.avatarTrajectory['z']
- # Check if computed trajectory is within target window
- target = [trial.targetPosition[0], trial.targetPosition[2]]
- if task == "Navigation_sphere_3D":
- target = [trial.targetPosition[0], trial.targetPosition[1], trial.targetPosition[2]]
- is_success = is_within_target_window_3D(
- list(zip(x_positions_pred,y_positions_pred, z_positions_pred)),
- target,
- task,
- )
- else:
- is_success, reason = is_within_target_window(
- list(zip(x_positions_pred, z_positions_pred)),
- target,
- task,
- )
- # === Count outcome ===
- if is_success:
- success_count += 1
- else:
- failure_reasons[reason] += 1
- success_rate = success_count / total_trials if total_trials > 0 else 0
- return success_rate, dict(failure_reasons)
- def compare_success_rate_between_areas_vs_online_simulation(all_success_rates, monkey, experiment):
- """
- Compare normalized success rates between cortical areas and online decoding simulations.
- Args:
- all_success_rates (dict): Dictionary of session-level success rates. Each entry should be:
- {
- 'session_id': {
- 'PMv': ..., 'PMd': ..., 'M1': ...,
- 'PMv+PMd': ..., 'M1+PMd': ..., 'M1+PMv': ..., 'All': ..., 'Online_simulation': ...
- },
- ...
- }
- Returns:
- dict: Contains median, IQR-normalized stats, session-wise deltas, Wilcoxon p-values, and raw values.
- """
- # Prepare containers
- region_labels = ['PMv', 'PMd', 'M1', 'PMv+PMd', 'M1+PMd', 'M1+PMv', 'All']
- norm_results = {region: [] for region in region_labels}
- raw_results = {region: [] for region in region_labels + ['Online']}
- # Pairwise deltas of interest
- session_deltas = {
- 'PMv-M1': [], 'PMv-PMd': [], 'PMd-M1': [],
- 'PMv+PMd-Online': [], 'M1+PMd-Online': [], 'M1+PMv-Online': [], 'All-Online': [],
- 'PMv-Online': [], 'PMd-Online': [], 'M1-Online': [],
- 'PMv+PMd-M1+PMd': [], 'PMv+PMd-M1+PMv': [], 'M1+PMd-M1+PMv': [],
- 'PMv+PMd-All': [], 'M1+PMd-All': [], 'M1+PMv-All': []
- }
- # Populate results
- for session, rates in all_success_rates.items():
- if all(k in rates for k in ['Online_simulation'] + region_labels):
- online = rates['Online_simulation']
- # Normalize
- for region in region_labels:
- norm_results[region].append(rates[region] / online)
- raw_results[region].append(rates[region])
- raw_results['Online'].append(online)
- # Deltas
- session_deltas['PMv-M1'].append(rates['PMv'] - rates['M1'])
- session_deltas['PMv-PMd'].append(rates['PMv'] - rates['PMd'])
- session_deltas['PMd-M1'].append(rates['PMd'] - rates['M1'])
- for key in ['PMv+PMd', 'M1+PMd', 'M1+PMv', 'All', 'PMv', 'PMd', 'M1']:
- session_deltas[f'{key}-Online'].append(rates[key] - online)
- session_deltas['PMv+PMd-M1+PMd'].append(rates['PMv+PMd'] - rates['M1+PMd'])
- session_deltas['PMv+PMd-M1+PMv'].append(rates['PMv+PMd'] - rates['M1+PMv'])
- session_deltas['M1+PMd-M1+PMv'].append(rates['M1+PMd'] - rates['M1+PMv'])
- session_deltas['PMv+PMd-All'].append(rates['PMv+PMd'] - rates['All'])
- session_deltas['M1+PMd-All'].append(rates['M1+PMd'] - rates['All'])
- session_deltas['M1+PMv-All'].append(rates['M1+PMv'] - rates['All'])
- # Summary stats (median ± IQR)
- norm_stats = {
- region: (np.median(vals), np.percentile(vals, 25), np.percentile(vals, 75))
- for region, vals in norm_results.items()
- }
- # Wilcoxon tests
- def pval(delta_key):
- return wilcoxon(session_deltas[delta_key], alternative='two-sided', method='approx').pvalue
- pvals = {
- 'PMv_vs_M1': pval('PMv-M1'),
- 'PMd_vs_M1': pval('PMd-M1'),
- 'PMd_vs_PMv': pval('PMv-PMd'),
- 'PMv+PMd_vs_M1+PMd': pval('PMv+PMd-M1+PMd'),
- 'PMv+PMd_vs_M1+PMv': pval('PMv+PMd-M1+PMv'),
- 'M1+PMd_vs_M1+PMv': pval('M1+PMd-M1+PMv'),
- 'PMv+PMd_vs_Online': pval('PMv+PMd-Online'),
- 'M1+PMd_vs_Online': pval('M1+PMd-Online'),
- 'M1+PMv_vs_Online': pval('M1+PMv-Online'),
- 'All_vs_Online': pval('All-Online'),
- 'PMv_vs_Online': pval('PMv-Online'),
- 'PMd_vs_Online': pval('PMd-Online'),
- 'M1_vs_Online': pval('M1-Online'),
- 'PMv+PMd_vs_All': pval('PMv+PMd-All'),
- 'M1+PMd_vs_All': pval('M1+PMd-All'),
- 'M1+PMv_vs_All': pval('M1+PMv-All')
- }
- plot_success_rates_offline_decoding(norm_results, pvals, monkey, experiment)
- return {
- "normalized_stats": norm_stats,
- "session_deltas": session_deltas,
- "p_values": pvals,
- "raw_success_rates": raw_results
- }
- def compute_significance_time_to_target(df_trials: pd.DataFrame, experiment_name: str) -> dict:
- """
- Perform Mann-Whitney U test to compare time-to-target across different conditions
- based on the specified experiment name.
- Args:
- df_trials (pd.DataFrame): DataFrame containing trial-level data with columns for condition, correctness,
- timeToTarget, and session identifiers.
- experiment_name (str): Name of the experiment. Must be one of: '3D Center-out', 'Respawn', or 'Obstacle'.
- Returns:
- dict: Dictionary containing test results, including z-score, U statistic, p-value, number of trials,
- and group-wise mean time-to-target.
- """
- df_trials = df_trials[df_trials['correct'] == True]
- if experiment_name == "3D Center-out":
- group1 = df_trials[df_trials['targetLevel'] == 'upper']['timeToTarget']
- group2 = df_trials[df_trials['targetLevel'] == 'lower']['timeToTarget']
- group_key = 'targetLevel'
- elif experiment_name == "Respawn":
- group1 = df_trials[df_trials['respawn'] == True]['timeToTarget']
- group2 = df_trials[df_trials['respawn'] == False]['timeToTarget']
- group_key = 'respawn'
- elif experiment_name == "Obstacle":
- group1 = df_trials[df_trials['obstacle'] == 'offPath']['timeToTarget']
- group2 = df_trials[df_trials['obstacle'] == 'onPath']['timeToTarget']
- group_key = 'obstacle'
- else:
- raise ValueError("Unsupported experiment name. Must be one of: '3D Center-out', 'Respawn', 'Obstacle'.")
- # Mann-Whitney U test
- result_mwu = mannwhitneyu(group1, group2, alternative='two-sided')
- n1, n2 = len(group1), len(group2)
- mu_U = n1 * n2 / 2
- sigma_U = np.sqrt(n1 * n2 * (n1 + n2 + 1) / 12)
- z_score = (result_mwu.statistic - mu_U) / sigma_U
- group_means = df_trials.groupby(group_key)['timeToTarget'].mean().to_dict()
- results = {
- "z_score": z_score,
- "U_statistic": result_mwu.statistic,
- "p_value": result_mwu.pvalue,
- "N_trials": n1 + n2,
- "group_means": group_means,
- }
- print(results)
- return results
- def compute_chance_level(trials, experiment, num_permutations=1000):
- """
- Estimate chance level by shuffling targets and checking success.
- Supports both 2D and 3D experiments.
- """
- def is_within_target_window_2D(states, target, window_size, min_consecutive=10):
- half_window = window_size / 2.0
- count = 0
- for x, z in states:
- if (target[0] - half_window <= x <= target[0] + half_window and
- target[1] - half_window <= z <= target[1] + half_window):
- count += 1
- if count >= min_consecutive:
- return True
- else:
- count = 0
- return False
- def is_within_target_window_3D(states, target, window_size=4.2, min_consecutive=10):
- half_window = window_size / 2.0
- half_height = 0.75 + 0.45
- count = 0
- for x, y, z in states:
- if (target[0] - half_window <= x <= target[0] + half_window and
- target[1] - half_height <= y <= target[1] + half_height and
- target[2] - half_window <= z <= target[2] + half_window):
- count += 1
- if count >= min_consecutive:
- return True
- else:
- count = 0
- return False
- def calculate_success(states, targs, is_3d, is_continuous_navigation):
- results = []
- for s, t in zip(states, targs):
- if is_3d:
- s = np.array(s) + np.array([0, 0.25, 0])
- results.append(is_within_target_window_3D(s, t))
- else:
- s = np.array(s)[:, [0, 2]] # x, z
- t_2d = [t[0], t[2]]
- if is_continuous_navigation:
- results.append(is_within_target_window_2D(s, t_2d, window_size=5.6))
- else:
- results.append(is_within_target_window_2D(s, t_2d, window_size=4.2))
- return np.mean(results)
- is_3d = "3D" in experiment
- is_continuous_navigation = "Continuous Navigation" in experiment
- conc_trials = [item for sublist in trials for item in sublist]
- states_data = [list(zip(trial.avatarTrajectory['x'],
- trial.avatarTrajectory['y'],
- trial.avatarTrajectory['z'])) for trial in conc_trials]
- targets = [trial.targetPosition for trial in conc_trials]
- observed = calculate_success(states_data, targets, is_3d, is_continuous_navigation)
- null_distribution = [
- calculate_success(states_data, np.random.permutation(targets), is_3d, is_continuous_navigation)
- for _ in range(num_permutations)
- ]
- return np.mean(null_distribution), observed, null_distribution
- def compute_session_accuracy(correct_trials, incorrect_trials, monkey, task=None):
- """
- Computes session-level success rates or parameter-split accuracy based on the task.
- Args:
- correct_trials (list of list): Correct trials per session
- incorrect_trials (list of list): Incorrect trials per session
- monkey (str): Monkey name (unused here but kept for compatibility)
- task (str): Optional task string. If includes 'Obstacle', 'Respawn', or '3D Center-out',
- computes split accuracy based on relevant parameter.
- Returns:
- If matched task: (accuracy_group1, accuracy_group2)
- Else: full-session accuracy list
- """
- def compute_parameter_accuracy(correct_trials, incorrect_trials, parameter):
- def compute_accuracy(correct, incorrect):
- return [len(c) / (len(c) + len(i)) * 100 if len(c) + len(i) > 0 else np.nan
- for c, i in zip(correct, incorrect)]
- def split_respawn_trials(trials):
- respawn = [t for t in trials if not np.all(np.isnan(t.targetJumpPosition))]
- norespawn = [t for t in trials if np.all(np.isnan(t.targetJumpPosition))]
- return respawn, norespawn
- def split_obstacle_trials(trials):
- target_positions = [(-7.0, 0.75, 6.0), (-3.5, 0.75, 8.5), (0.0, 0.75, 9.2), (3.5, 0.75, 8.5), (7.0, 0.75, 6.0)]
- obstacle_positions = [(-3.5, 0, 3), (-1.75, 0, 4.25), (0, 0, 4.6), (3.5, 0, 3), (1.75, 0, 4.25)]
- def obstacle_status(target, obstacle):
- t_idx = next((i for i, pos in enumerate(target_positions) if np.array_equal(pos, target)), -1)
- o_idx = next((i for i, pos in enumerate(obstacle_positions) if np.array_equal(pos, obstacle)), -1)
- return "onPath" if t_idx == o_idx and t_idx != -1 else "offPath"
- on_path = [t for t in trials if obstacle_status(t.targetPosition, t.obstaclePosition) == "onPath"]
- off_path = [t for t in trials if obstacle_status(t.targetPosition, t.obstaclePosition) == "offPath"]
- return on_path, off_path
- def split_3D_trials(trials):
- upper = [t for t in trials if t.targetPosition[1] == 2.35]
- lower = [t for t in trials if t.targetPosition[1] == 0.75]
- return upper, lower
- # Select splitter and condition names
- if task == "Respawn":
- splitter = split_respawn_trials
- name1, name2 = "Respawn", "No Respawn"
- elif task == "Obstacle":
- splitter = split_obstacle_trials
- name1, name2 = "Obstacle on Path", "Obstacle off Path"
- elif task == "3D Center-out":
- splitter = split_3D_trials
- name1, name2 = "Upper targets", "Lower targets"
- else:
- full_accuracy = compute_accuracy(correct_trials, incorrect_trials)
- return {"All": full_accuracy}
- # Apply splitter per session
- all_c1, all_c2, all_i1, all_i2 = [], [], [], []
- for correct, incorrect in zip(correct_trials, incorrect_trials):
- c1, c2 = splitter(correct)
- i1, i2 = splitter(incorrect)
- all_c1.append(c1)
- all_c2.append(c2)
- all_i1.append(i1)
- all_i2.append(i2)
- acc1 = compute_accuracy(all_c1, all_i1)
- acc2 = compute_accuracy(all_c2, all_i2)
- return {
- name1: acc1,
- name2: acc2
- }
- # Check task type
- if task in ["Obstacle", "Respawn", "3D Center-out"]:
- return compute_parameter_accuracy(correct_trials, incorrect_trials, task)
- else:
- return [len(c) / (len(c) + len(i)) * 100 if len(c) + len(i) > 0 else np.nan
- for c, i in zip(correct_trials, incorrect_trials)]
- def compute_target_accuracy(correct_trials, incorrect_trials, monkey, experiment_name):
- """
- Computes success rate per target and returns a flat list of dicts with Task, Target, Category, SuccessRate.
- Args:
- correct_trials (list of list): Correct trials per session.
- incorrect_trials (list of list): Incorrect trials per session.
- monkey (str): Monkey name.
- experiment_name (str): Task name.
- Returns:
- list of dict: [{'Task': ..., 'Target': ..., 'Category': ..., 'SuccessRate': ...}, ...]
- """
- def target_category(target):
- x, _, z = target
- key = (round(x, 1), round(z, 1))
- if key in {(-7.0, 6.0), (0.0, 9.2), (7.0, 6.0), (-6.0, 7.0), (6.0, 7.0)}:
- return "Pretrained"
- elif key in {(-3.5, 8.5), (3.5, 8.5), (-3.0, 8.7), (3.0, 8.7)}:
- return "New"
- else:
- return "Other"
- target_answers = defaultdict(list)
- for correct, incorrect in zip(correct_trials, incorrect_trials):
- for trial in correct:
- target = tuple(trial.targetPosition)
- target_answers[target].append(1)
- for trial in incorrect:
- target = tuple(trial.targetPosition)
- target_answers[target].append(0)
- summary = []
- for target, answers in target_answers.items():
- if answers:
- rate = sum(answers) / len(answers)
- summary.append({
- "Task": experiment_name,
- "Target": target,
- "Category": target_category(target),
- "SuccessRate": rate * 100
- })
- return summary
- def post_hoc_testing(all_trials, experiment, monkey):
- df_trials = make_df_structure(all_trials, experiment, monkey)
- if experiment == "3D Center-out":
- # Define the upper and lower target groups
- df_trials['targetLevel'] = df_trials['targetPosition'].apply(lambda x: 'upper' if x[1] > 1 else 'lower')
- # Compute time-to-target
- print(f"Significant difference in time-to-target({experiment} & {monkey}):")
- compute_significance_time_to_target(df_trials, experiment)
- elif experiment == "Respawn":
- print(f"Significant difference in time-to-target({experiment} & {monkey}):")
- compute_significance_time_to_target(df_trials, experiment)
- elif experiment == "Obstacle":
- print(f"Significant difference in time-to-target({experiment} & {monkey}):")
- compute_significance_time_to_target(df_trials, experiment)
- return None
- def significant_difference_distribution_endpoints(monkey, lower_trials, upper_trials):
- endpoints_upper_trials = np.array([trial.avatarTrajectory['y'][-10] for trial in upper_trials])
- endpoints_lower_trials = np.array([trial.avatarTrajectory['y'][-10] for trial in lower_trials])
- # Mann-Whitney U test
- u_stat, p_value_mannwhitney = mannwhitneyu(endpoints_upper_trials, endpoints_lower_trials)
- print(f"Mann-Whitney U test: U-statistic = {u_stat}, p-value = {p_value_mannwhitney}")
- # Plot density plots
- plot_3D_endpoints_density_plots(monkey, endpoints_upper_trials, endpoints_lower_trials)
- def summarize_emg_eyemovement_results(monkey, experiments, analysis, base_dir, coordinate = None):
- correlation_results = []
- linear_results = []
- nonlinear_results = []
- for experiment in experiments:
- data, velocityData, _ = load_emg_eyemovement_files(experiment, monkey, analysis, base_dir, coordinate)
- if len(data) == 0:
- continue
- if analysis == "eyemovement":
- eyemovementData = [[segment['eyeMovement'] for segment in trial] for trial in data]
- corr_df, reg_df = analyze_eyemovement(eyemovementData, velocityData, experiment)
- else:
- corr_df, reg_df = analyze_emg(data, velocityData, experiment)
- for velocity in corr_df['Velocity Component'].unique():
- corr_data = corr_df[(corr_df['Experiment'] == experiment) & (corr_df['Velocity Component'] == velocity)]
- regression_data = reg_df[(reg_df['Experiment'] == experiment) & (reg_df['Velocity Component'] == velocity)]
- correlation_results.append({
- 'Task': experiment,
- 'Velocity': velocity,
- 'Mean ± STD Spearman Correlation': f"{corr_data['Spearman Correlation'].mean():.7f} ± {corr_data['Spearman Correlation'].std():.7f}",
- 'Combined p (Correlation)': combined_p_value(corr_data['p-value'].tolist())
- })
- linear_results.append({
- 'Task': experiment,
- 'Velocity': velocity,
- 'Linear R²': f"{regression_data['Linear R²'].median():.7f} [{compute_iqr(regression_data['Linear R²']):.7f}]",
- 'Linear MSE': f"{regression_data['Linear MSE'].median():.7f} [{compute_iqr(regression_data['Linear MSE']):.7f}]",
- 'Combined p (Linear)': combined_p_value(regression_data['Linear p-value'].tolist())
- })
- nonlinear_results.append({
- 'Task': experiment,
- 'Velocity': velocity,
- 'Non-Linear R²': f"{regression_data['Polynomial R²'].median():.7f} [{compute_iqr(regression_data['Polynomial R²']):.7f}]",
- 'Non-Linear MSE': f"{regression_data['Polynomial MSE'].median():.7f} [{compute_iqr(regression_data['Polynomial MSE']):.7f}]",
- 'Combined p (Non-Linear)': combined_p_value(regression_data['Polynomial p-value'].tolist())
- })
- # Merge summaries
- df_corr = pd.DataFrame(correlation_results)
- df_lin = pd.DataFrame(linear_results)
- df_nonlin = pd.DataFrame(nonlinear_results)
- final_table = df_corr.merge(df_lin, on=['Task', 'Velocity']).merge(df_nonlin, on=['Task', 'Velocity'])
- return final_table
- def analyze_eyemovement(eyemovementData, velocityData, experiment_name):
- correlation_list = []
- linear_regression_list = []
- poly_regression_list = []
- # Process EMG and velocity data across all trials
- all_sessions_emg, all_sessions_velocity = process_eyemovement_data(eyemovementData, velocityData, sampling_rate = 20)
- for session_emg, session_velocity in zip(all_sessions_emg, all_sessions_velocity):
- # Perform the analyses
- spearman_results = spearman_correlation(session_emg, session_velocity, experiment_name)
- linear_results = linear_regression(session_emg, session_velocity, experiment_name)
- poly_results = polynomial_regression(session_emg, session_velocity, experiment_name=experiment_name)
- # Append the results from each session to the final lists
- correlation_list.extend(spearman_results)
- linear_regression_list.extend(linear_results)
- poly_regression_list.extend(poly_results)
- # Return DataFrames for the tables and mutual information results
- correlation_df = pd.DataFrame(correlation_list)
- regression_df = pd.DataFrame({
- 'Experiment': [res['Experiment'] for res in linear_regression_list],
- 'Velocity Component': [res['Velocity Component'] for res in linear_regression_list],
- 'Linear R²': [res['R² Score'] for res in linear_regression_list],
- 'Linear MSE': [res['MSE'] for res in linear_regression_list],
- 'Cross-validated Linear MSE': [res['Cross-Validated MSE'] for res in linear_regression_list],
- 'Linear p-value': [res['p-value'] for res in linear_regression_list],
- 'Polynomial R²': [res['R² Score (Polynomial)'] for res in poly_regression_list],
- 'Polynomial MSE': [res['MSE (Polynomial)'] for res in poly_regression_list],
- 'Polynomial p-value': [res['p-value (Polynomial)'] for res in poly_regression_list],
- 'Degree': [res['Degree'] for res in poly_regression_list]
- })
- return correlation_df, regression_df
- def analyze_emg(emgData, velocityData, experiment_name):
- correlation_list = []
- linear_regression_list = []
- poly_regression_list = []
- # Process EMG and velocity data across all trials
- all_sessions_emg, all_sessions_velocity = process_emg_data(emgData, velocityData, sampling_rate = 20)
- for session_emg, session_velocity in zip(all_sessions_emg, all_sessions_velocity):
- # Perform the analyses
- spearman_results = spearman_correlation(session_emg, session_velocity, experiment_name)
- linear_results = linear_regression(session_emg, session_velocity, experiment_name)
- poly_results = polynomial_regression(session_emg, session_velocity, experiment_name=experiment_name)
- # Append the results from each session to the final lists
- correlation_list.extend(spearman_results)
- linear_regression_list.extend(linear_results)
- poly_regression_list.extend(poly_results)
- # Return DataFrames for the tables and mutual information results
- correlation_df = pd.DataFrame(correlation_list)
- regression_df = pd.DataFrame({
- 'Experiment': [res['Experiment'] for res in linear_regression_list],
- 'Velocity Component': [res['Velocity Component'] for res in linear_regression_list],
- 'Linear R²': [res['R² Score'] for res in linear_regression_list],
- 'Linear MSE': [res['MSE'] for res in linear_regression_list],
- 'Cross-validated Linear MSE': [res['Cross-Validated MSE'] for res in linear_regression_list],
- 'Linear p-value': [res['p-value'] for res in linear_regression_list],
- 'Polynomial R²': [res['R² Score (Polynomial)'] for res in poly_regression_list],
- 'Polynomial MSE': [res['MSE (Polynomial)'] for res in poly_regression_list],
- 'Polynomial p-value': [res['p-value (Polynomial)'] for res in poly_regression_list],
- 'Degree': [res['Degree'] for res in poly_regression_list]
- })
- return correlation_df, regression_df
- def analyze_within_session_trends(monkeys, experiments, base_dir):
- """
- Analyze within-session performance trends (adaptation) using all trials.
- Returns a DataFrame with slope, p-value, R², number of trials, and trend classification for each session.
- """
- limited_monkeys_tasks = {"3D Center-out", "Respawn", "Obstacle"}
- all_data = []
- def is_success(answer):
- if answer == 1:
- return 1
- elif answer in [3, 5, 6]:
- return 0
- else:
- return None
- def classify_trend(slope, p_value):
- if p_value < 0.05:
- if slope > 0:
- return "Improvement"
- elif slope < 0:
- return "Decline"
- return "Constant"
- for experiment in experiments:
- session_data = []
- if experiment in limited_monkeys_tasks:
- task_monkeys = [m for m in monkeys if m != "Monkey 1"]
- else:
- task_monkeys = monkeys
- for monkey in task_monkeys:
- all_trials, correct, incorrect, all_training_trials, all_channels, *_ = load_files(experiment, monkey, base_dir)
- for session_idx, session in enumerate(all_trials):
- answers = [trial.answer for trial in session]
- binary_success = np.array([is_success(a) for a in answers if is_success(a) is not None])
- if len(binary_success) < 7:
- continue # skip short sessions
- # Moving average smoothing
- window_size = 7
- moving_avg = np.convolve(binary_success, np.ones(window_size)/window_size, mode='valid')
- x_vals = np.arange(len(moving_avg))
- # Linear regression
- slope, intercept, r_value, p_value, std_err = linregress(x_vals, moving_avg)
- trend = classify_trend(slope, p_value)
- session_data.append({
- "Monkey": monkey,
- "Task": experiment,
- "Session": session_idx + 1,
- "Trials": len(binary_success),
- "Slope": slope,
- "p_value": p_value,
- "R_squared": r_value**2,
- "Trend": trend
- })
- df_sessions = pd.DataFrame(session_data)
- all_data.append(df_sessions)
- df_all_sessions = pd.concat(all_data, ignore_index=True)
- return df_all_sessions
- def compute_parameter_accuracy(correct_trials: List[List], incorrect_trials: List[List], parameter: str) -> Tuple[List[float], List[float]]:
- """
- Computes per-session success rate split by task-specific parameters (e.g., Respawn vs No Respawn,
- Obstacle On-path vs Off-path, 3D Upper vs Lower targets).
- Args:
- correct_trials (List[List]): List of sessions containing correct trials.
- incorrect_trials (List[List]): List of sessions containing incorrect trials.
- parameter (str): Task type ('Respawn', 'Obstacle', or '3D').
- Returns:
- Tuple[List[float], List[float]]: Success rates for parameter 1 group and parameter 2 group.
- """
- def split_respawn(trials):
- respawn = [t for t in trials if not np.all(np.isnan(t.targetJumpPosition))]
- no_respawn = [t for t in trials if np.all(np.isnan(t.targetJumpPosition))]
- return respawn, no_respawn
- def split_obstacle(trials):
- def is_on_path(target, obstacle):
- target_positions = [(-7.0, 0.75, 6.0), (-3.5, 0.75, 8.5), (0.0, 0.75, 9.2), (3.5, 0.75, 8.5), (7.0, 0.75, 6.0)]
- obstacle_positions = [(-3.5, 0, 3), (-1.75, 0, 4.25), (0, 0, 4.6), (3.5, 0, 3), (1.75, 0, 4.25)]
- ti = next((i for i, p in enumerate(target_positions) if np.allclose(p, target)), -1)
- oi = next((i for i, p in enumerate(obstacle_positions) if np.allclose(p, obstacle)), -1)
- return ti == oi and ti != -1
- on_path = [t for t in trials if is_on_path(t.targetPosition, t.obstaclePosition)]
- off_path = [t for t in trials if not is_on_path(t.targetPosition, t.obstaclePosition)]
- return on_path, off_path
- def split_3D(trials):
- upper = [t for t in trials if t.targetPosition[1] == 2.35]
- lower = [t for t in trials if t.targetPosition[1] == 0.75]
- return upper, lower
- def compute_session_rates(group1, group2):
- rates1 = [len(g1) / (len(g1) + len(g2)) * 100 if (len(g1) + len(g2)) > 0 else 0
- for g1, g2 in zip(group1, group2)]
- return rates1
- group1_correct = []
- group2_correct = []
- group1_incorrect = []
- group2_incorrect = []
- if parameter == "Respawn":
- for c, i in zip(correct_trials, incorrect_trials):
- c1, c2 = split_respawn(c)
- i1, i2 = split_respawn(i)
- group1_correct.append(c1)
- group2_correct.append(c2)
- group1_incorrect.append(i1)
- group2_incorrect.append(i2)
- elif parameter == "Obstacle":
- for c, i in zip(correct_trials, incorrect_trials):
- c1, c2 = split_obstacle(c)
- i1, i2 = split_obstacle(i)
- group1_correct.append(c1)
- group2_correct.append(c2)
- group1_incorrect.append(i1)
- group2_incorrect.append(i2)
- elif parameter == "3D":
- for c, i in zip(correct_trials, incorrect_trials):
- c1, c2 = split_3D(c)
- i1, i2 = split_3D(i)
- group1_correct.append(c1)
- group2_correct.append(c2)
- group1_incorrect.append(i1)
- group2_incorrect.append(i2)
- else:
- raise ValueError("Unsupported parameter type. Use 'Respawn', 'Obstacle', or '3D'.")
- acc1 = compute_session_rates(group1_correct, group1_incorrect)
- acc2 = compute_session_rates(group2_correct, group2_incorrect)
- return acc1, acc2
analysis.py at commit 6a198ce, under MIT · at the source
Overview
- Laboratory for Neuro- and Psychophysiology, Department of Neurosciences, KU Leuven and the Leuven Brain Institute, Leuven, Belgium
- Department of Electrical and Computer Engineering, University of Washington, Seattle, WA, USA
- Research Group Experimental Neurosurgery and Neuroanatomy, KU Leuven and the Leuven Brain Institute, Leuven, Belgium
Abstract
We present an innovative intracortical brain-computer interface (BCI) to bridge the gap between laboratory settings and real-world applications. This BCI approach introduces three key advancements. First, we used neural signals from three macaque brain regions—primary motor, dorsal, and ventral premotor cortex—enabling precise and flexible decoding of real-time three-dimensional (3D) sphere/
Reproduced under the paper's license (CC BY), from the paper cited above.
Repositories
Its files are read in the Code ↔ Paper reader above, with 10 matches between paragraphs and lines of code.
Zenodo 15791186
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
- 29 September 2026: the link answers (HTTP 200)
15 files
- run_all.py — Python, 31 lines
- scripts/
run_analyse_trajectories — Python, 113 lines.py - scripts/
run_emg_and_eyemovement. — Python, 52 linespy - scripts/
run_offline_brain_area_a — Python, 85 linesnalysis.py - scripts/
run_success_rate.py — Python, 114 lines - scripts/
run_time_to_target.py — Python, 94 lines - scripts/
run_within_session_perfo — Python, 49 linesrmance_trend.py - src/
analysis.py — Python, 698 lines - src/
load.py — Python, 227 lines - src/
metrics.py — Python, 150 lines - src/
plots.py — Python, 672 lines - src/
tables.py — Python, 63 lines - src/
utils.py — Python, 582 lines - LICENSE — License, 43 lines
- README.md — Text, 122 lines
ophelie-bci/ibci_analysis
6a198cec614d11e54fc7bc34a47c7270c3713da0, 2 March 2026Availability: 1 check, the latest on 29 September 2026: the link answers
- 29 September 2026: the link answers
15 files
- run_all.py — Python, 31 lines
- scripts/
run_analyse_trajectories — Python, 113 lines.py - scripts/
run_emg_and_eyemovement. — Python, 52 linespy - scripts/
run_offline_brain_area_a — Python, 85 lines, 3 matchesnalysis.py - scripts/
run_success_rate.py — Python, 114 lines - scripts/
run_time_to_target.py — Python, 94 lines - scripts/
run_within_session_perfo — Python, 49 linesrmance_trend.py - src/
analysis.py — Python, 698 lines, 5 matches - src/
load.py — Python, 227 lines - src/
metrics.py — Python, 150 lines, 2 matches - src/
plots.py — Python, 672 lines - src/
tables.py — Python, 63 lines - src/
utils.py — Python, 582 lines - LICENSE — License, 43 lines
- README.md — Text, 122 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:
- 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 26 scripts, each with its path and the digest of its content;
- 10 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
Datasets cited
- doi:10.5061/
dryad.2bvq83c34 — at Dryad; found in DataCite
Data, code, and materials availability
All data and code needed to evaluate and reproduce the results in the paper are present in the paper and/
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, 29 September 2026: the first record
Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 6 MeSH terms, 2 funders, 41 references.
Cite
This paper
Saussus, O., De Schrijver, S., Ramirez, J. G., Decramer, T., & Janssen, P. (2026). Intracortical brain-computer interface for navigation in virtual reality in macaque monkeys. Science advances, 12(16), eadw3876. https://
BibTeX
@article{saussus2026intr
author = {Saussus, Ophelie and De Schrijver, Sofie and Ramirez, Jesus Garcia and Decramer, Thomas and Janssen, Peter},
title = {{Intracortical brain-computer interface for navigation in virtual reality in macaque monkeys}},
journal = {Science advances},
year = {2026},
month = apr,
volume = {12},
number = {16},
pages = {eadw3876},
publisher = {American Association for the Advancement of Science},
issn = {2375-2548},
doi = {10.1126/
url = {https://
pmid = {41984955},
pmcid = {PMC13082338}
}
RIS
TY - JOUR
AU - Saussus, Ophelie
AU - De Schrijver, Sofie
AU - Ramirez, Jesus Garcia
AU - Decramer, Thomas
AU - Janssen, Peter
TI - Intracortical brain-computer interface for navigation in virtual reality in macaque monkeys
T2 - Science advances
J2 - Sci Adv
PY - 2026
DA - 2026/
VL - 12
IS - 16
SP - eadw3876
SN - 2375-2548
PB - American Association for the Advancement of Science
DO - 10.1126/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1126/
"type": "article-journal",
"title": "Intracortical brain-computer interface for navigation in virtual reality in macaque monkeys",
"container-title": "Science advances",
"author": [
{
"family": "Saussus",
"given": "Ophelie"
},
{
"family": "De Schrijver",
"given": "Sofie"
},
{
"family": "Ramirez",
"given": "Jesus Garcia"
},
{
"family": "Decramer",
"given": "Thomas"
},
{
"family": "Janssen",
"given": "Peter"
}
],
"container-title-short":
"volume": "12",
"issue": "16",
"page": "eadw3876",
"DOI": "10.1126/
"PMID": "41984955",
"PMCID": "PMC13082338",
"ISSN": "2375-2548",
"publisher": "American Association for the Advancement of Science",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
15
]
]
}
}
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.1038/s41467-026-76109-y [code]
- Assistive algorithms influence neural representations in motor brain-computer interfaces.Journal: Nature communicationsIn common: seaborn, scikit-learn, pandas, 3 other tools, non-human primate, systems, 8 references
- [2] doi:10.1038/s41591-026-04414-6 [code]
- Long-term independent use of an intracortical brain-computer interface for speech and cursor control.Journal: Nature medicineIn common: pandas, SciPy, Matplotlib, 1 other tool, 7 references
- [3] doi:10.1126/sciadv.aed9309 [code]
- Dynamic population coding of kinematic structure across executed and observed actions in primate premotor cortex.Journal: Science advancesIn common: non-human primate, systems, 5 references
- [4] doi:10.1038/s41467-026-75455-1 [code]
- Shared latent representations of speech production for cross-patient speech decoding.Journal: Nature communicationsIn common: statsmodels, seaborn, scikit-learn, 4 other tools, 2 references
- [5] doi:10.1016/j.celrep.2026.117420 [code]
- Neural population dynamics of direct electrical stimulation of neocortex.Journal: Cell reportsIn common: statsmodels, seaborn, scikit-learn, 4 other tools, systems, 1 reference
- [6] doi:10.1038/s41586-026-10691-5 [code]
- Mapping the neuronal building blocks of human language with language models.Journal: NatureIn common: statsmodels, scikit-learn, pandas, 3 other tools, 2 references
- [7] doi:10.1007/s12021-026-09807-z [code]
- Optimal Size of Electrocorticography Grids for Classification of Hand Movements.Journal: NeuroinformaticsIn common: seaborn, scikit-learn, pandas, 3 other tools, 2 references
- [8] doi:10.1126/sciadv.aef0343 [code]
- Learning induces activation-mechanism-dep
endent neural plasticity in an intracortical microstimulation task. Journal: Science advancesIn common: statsmodels, seaborn, scikit-learn, 4 other tools, 1 reference - [9] doi:10.1038/s41467-026-76104-3 [code]
- Sensorimotor remapping drives task specialization in prefrontal cortex.Journal: Nature communicationsIn common: statsmodels, seaborn, scikit-learn, 4 other tools, non-human primate, systems
- [10] doi:10.1038/s41467-026-74347-8 [code]
- Compositionality of social gaze in the prefrontal-amygdala circuits.Journal: Nature communicationsIn common: statsmodels, seaborn, scikit-learn, 4 other tools, non-human primate, systems
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: 2 repositories of the authors' code, each at its verified commit and with its license, 26 scripts, and 10 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:fb313d8dd0437dfe…
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.
