OSCR

Intracortical brain-computer interface for navigation in virtual reality in macaque monkeys.

Code ↔ Paper

10 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 10 matches
  1. [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. [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. [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. [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. [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. [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. [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. [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. [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. [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

  1. from typing import List, Tuple
  2. import numpy as np
  3. import pandas as pd
  4. from scipy.stats import linregress
  5. from src.load import load_files
  6. from src.utils import is_within_target_window, is_within_target_window_3D, make_df_structure, process_emg_data, process_eyemovement_data
  7. from src.metrics import spearman_correlation, linear_regression, polynomial_regression
  8. from src.load import load_emg_eyemovement_files
  9. from src.metrics import combined_p_value, compute_iqr
  10. from scipy.stats import ttest_ind, shapiro, mannwhitneyu
  11. from src.plots import plot_3D_endpoints_density_plots, plot_success_rates_offline_decoding
  12. import matplotlib.pyplot as plt
  13. import seaborn as sns
  14. import matplotlib as mpl
  15. from collections import defaultdict
  16. from scipy.stats import wilcoxon
  17. mpl.rcParams['svg.fonttype'] = 'none'
  18. def compute_offline_success_rate(trials, allPredictions, task, execution):
  19. success_count = 0
  20. failure_reasons = defaultdict(int)
  21. total_trials = len(trials)
  22. dt = 0.05
  23. for trial_index in range(total_trials):
  24. trial = trials[trial_index]
  25. predictions = allPredictions[trial_index]
  26. if execution == "offline":
  27. # Compute positions from predictions
  28. x_positions_pred = [0.0]
  29. y_positions_pred = [0.0]
  30. z_positions_pred = [0.0]
  31. for j in range(1, len(predictions)):
  32. if isinstance(predictions[j], int):
  33. continue # Skip if the prediction is an integer
  34. prev_x, prev_y, prev_z = x_positions_pred[-1], y_positions_pred[-1], z_positions_pred[-1]
  35. current_x_velocity = float(predictions[j]['x'])
  36. current_y_velocity = float(predictions[j]['y'])
  37. current_z_velocity = float(predictions[j]['z'])
  38. new_x_position = prev_x + current_x_velocity * dt
  39. new_y_position = prev_y + current_y_velocity * dt
  40. new_z_position = prev_z + current_z_velocity * dt
  41. x_positions_pred.append(new_x_position)
  42. y_positions_pred.append(new_y_position)
  43. z_positions_pred.append(new_z_position)
  44. else:
  45. x_positions_pred = trial.avatarTrajectory['x']
  46. y_positions_pred = trial.avatarTrajectory['y']
  47. z_positions_pred = trial.avatarTrajectory['z']
  48. # Check if computed trajectory is within target window
  49. target = [trial.targetPosition[0], trial.targetPosition[2]]
  50. if task == "Navigation_sphere_3D":
  51. target = [trial.targetPosition[0], trial.targetPosition[1], trial.targetPosition[2]]
  52. is_success = is_within_target_window_3D(
  53. list(zip(x_positions_pred,y_positions_pred, z_positions_pred)),
  54. target,
  55. task,
  56. )
  57. else:
  58. is_success, reason = is_within_target_window(
  59. list(zip(x_positions_pred, z_positions_pred)),
  60. target,
  61. task,
  62. )
  63. # === Count outcome ===
  64. if is_success:
  65. success_count += 1
  66. else:
  67. failure_reasons[reason] += 1
  68. success_rate = success_count / total_trials if total_trials > 0 else 0
  69. return success_rate, dict(failure_reasons)
  70. def compare_success_rate_between_areas_vs_online_simulation(all_success_rates, monkey, experiment):
  71. """
  72. Compare normalized success rates between cortical areas and online decoding simulations.
  73. Args:
  74. all_success_rates (dict): Dictionary of session-level success rates. Each entry should be:
  75. {
  76. 'session_id': {
  77. 'PMv': ..., 'PMd': ..., 'M1': ...,
  78. 'PMv+PMd': ..., 'M1+PMd': ..., 'M1+PMv': ..., 'All': ..., 'Online_simulation': ...
  79. },
  80. ...
  81. }
  82. Returns:
  83. dict: Contains median, IQR-normalized stats, session-wise deltas, Wilcoxon p-values, and raw values.
  84. """
  85. # Prepare containers
  86. region_labels = ['PMv', 'PMd', 'M1', 'PMv+PMd', 'M1+PMd', 'M1+PMv', 'All']
  87. norm_results = {region: [] for region in region_labels}
  88. raw_results = {region: [] for region in region_labels + ['Online']}
  89. # Pairwise deltas of interest
  90. session_deltas = {
  91. 'PMv-M1': [], 'PMv-PMd': [], 'PMd-M1': [],
  92. 'PMv+PMd-Online': [], 'M1+PMd-Online': [], 'M1+PMv-Online': [], 'All-Online': [],
  93. 'PMv-Online': [], 'PMd-Online': [], 'M1-Online': [],
  94. 'PMv+PMd-M1+PMd': [], 'PMv+PMd-M1+PMv': [], 'M1+PMd-M1+PMv': [],
  95. 'PMv+PMd-All': [], 'M1+PMd-All': [], 'M1+PMv-All': []
  96. }
  97. # Populate results
  98. for session, rates in all_success_rates.items():
  99. if all(k in rates for k in ['Online_simulation'] + region_labels):
  100. online = rates['Online_simulation']
  101. # Normalize
  102. for region in region_labels:
  103. norm_results[region].append(rates[region] / online)
  104. raw_results[region].append(rates[region])
  105. raw_results['Online'].append(online)
  106. # Deltas
  107. session_deltas['PMv-M1'].append(rates['PMv'] - rates['M1'])
  108. session_deltas['PMv-PMd'].append(rates['PMv'] - rates['PMd'])
  109. session_deltas['PMd-M1'].append(rates['PMd'] - rates['M1'])
  110. for key in ['PMv+PMd', 'M1+PMd', 'M1+PMv', 'All', 'PMv', 'PMd', 'M1']:
  111. session_deltas[f'{key}-Online'].append(rates[key] - online)
  112. session_deltas['PMv+PMd-M1+PMd'].append(rates['PMv+PMd'] - rates['M1+PMd'])
  113. session_deltas['PMv+PMd-M1+PMv'].append(rates['PMv+PMd'] - rates['M1+PMv'])
  114. session_deltas['M1+PMd-M1+PMv'].append(rates['M1+PMd'] - rates['M1+PMv'])
  115. session_deltas['PMv+PMd-All'].append(rates['PMv+PMd'] - rates['All'])
  116. session_deltas['M1+PMd-All'].append(rates['M1+PMd'] - rates['All'])
  117. session_deltas['M1+PMv-All'].append(rates['M1+PMv'] - rates['All'])
  118. # Summary stats (median ± IQR)
  119. norm_stats = {
  120. region: (np.median(vals), np.percentile(vals, 25), np.percentile(vals, 75))
  121. for region, vals in norm_results.items()
  122. }
  123. # Wilcoxon tests
  124. def pval(delta_key):
  125. return wilcoxon(session_deltas[delta_key], alternative='two-sided', method='approx').pvalue
  126. pvals = {
  127. 'PMv_vs_M1': pval('PMv-M1'),
  128. 'PMd_vs_M1': pval('PMd-M1'),
  129. 'PMd_vs_PMv': pval('PMv-PMd'),
  130. 'PMv+PMd_vs_M1+PMd': pval('PMv+PMd-M1+PMd'),
  131. 'PMv+PMd_vs_M1+PMv': pval('PMv+PMd-M1+PMv'),
  132. 'M1+PMd_vs_M1+PMv': pval('M1+PMd-M1+PMv'),
  133. 'PMv+PMd_vs_Online': pval('PMv+PMd-Online'),
  134. 'M1+PMd_vs_Online': pval('M1+PMd-Online'),
  135. 'M1+PMv_vs_Online': pval('M1+PMv-Online'),
  136. 'All_vs_Online': pval('All-Online'),
  137. 'PMv_vs_Online': pval('PMv-Online'),
  138. 'PMd_vs_Online': pval('PMd-Online'),
  139. 'M1_vs_Online': pval('M1-Online'),
  140. 'PMv+PMd_vs_All': pval('PMv+PMd-All'),
  141. 'M1+PMd_vs_All': pval('M1+PMd-All'),
  142. 'M1+PMv_vs_All': pval('M1+PMv-All')
  143. }
  144. plot_success_rates_offline_decoding(norm_results, pvals, monkey, experiment)
  145. return {
  146. "normalized_stats": norm_stats,
  147. "session_deltas": session_deltas,
  148. "p_values": pvals,
  149. "raw_success_rates": raw_results
  150. }
  151. def compute_significance_time_to_target(df_trials: pd.DataFrame, experiment_name: str) -> dict:
  152. """
  153. Perform Mann-Whitney U test to compare time-to-target across different conditions
  154. based on the specified experiment name.
  155. Args:
  156. df_trials (pd.DataFrame): DataFrame containing trial-level data with columns for condition, correctness,
  157. timeToTarget, and session identifiers.
  158. experiment_name (str): Name of the experiment. Must be one of: '3D Center-out', 'Respawn', or 'Obstacle'.
  159. Returns:
  160. dict: Dictionary containing test results, including z-score, U statistic, p-value, number of trials,
  161. and group-wise mean time-to-target.
  162. """
  163. df_trials = df_trials[df_trials['correct'] == True]
  164. if experiment_name == "3D Center-out":
  165. group1 = df_trials[df_trials['targetLevel'] == 'upper']['timeToTarget']
  166. group2 = df_trials[df_trials['targetLevel'] == 'lower']['timeToTarget']
  167. group_key = 'targetLevel'
  168. elif experiment_name == "Respawn":
  169. group1 = df_trials[df_trials['respawn'] == True]['timeToTarget']
  170. group2 = df_trials[df_trials['respawn'] == False]['timeToTarget']
  171. group_key = 'respawn'
  172. elif experiment_name == "Obstacle":
  173. group1 = df_trials[df_trials['obstacle'] == 'offPath']['timeToTarget']
  174. group2 = df_trials[df_trials['obstacle'] == 'onPath']['timeToTarget']
  175. group_key = 'obstacle'
  176. else:
  177. raise ValueError("Unsupported experiment name. Must be one of: '3D Center-out', 'Respawn', 'Obstacle'.")
  178. # Mann-Whitney U test
  179. result_mwu = mannwhitneyu(group1, group2, alternative='two-sided')
  180. n1, n2 = len(group1), len(group2)
  181. mu_U = n1 * n2 / 2
  182. sigma_U = np.sqrt(n1 * n2 * (n1 + n2 + 1) / 12)
  183. z_score = (result_mwu.statistic - mu_U) / sigma_U
  184. group_means = df_trials.groupby(group_key)['timeToTarget'].mean().to_dict()
  185. results = {
  186. "z_score": z_score,
  187. "U_statistic": result_mwu.statistic,
  188. "p_value": result_mwu.pvalue,
  189. "N_trials": n1 + n2,
  190. "group_means": group_means,
  191. }
  192. print(results)
  193. return results
  194. def compute_chance_level(trials, experiment, num_permutations=1000):
  195. """
  196. Estimate chance level by shuffling targets and checking success.
  197. Supports both 2D and 3D experiments.
  198. """
  199. def is_within_target_window_2D(states, target, window_size, min_consecutive=10):
  200. half_window = window_size / 2.0
  201. count = 0
  202. for x, z in states:
  203. if (target[0] - half_window <= x <= target[0] + half_window and
  204. target[1] - half_window <= z <= target[1] + half_window):
  205. count += 1
  206. if count >= min_consecutive:
  207. return True
  208. else:
  209. count = 0
  210. return False
  211. def is_within_target_window_3D(states, target, window_size=4.2, min_consecutive=10):
  212. half_window = window_size / 2.0
  213. half_height = 0.75 + 0.45
  214. count = 0
  215. for x, y, z in states:
  216. if (target[0] - half_window <= x <= target[0] + half_window and
  217. target[1] - half_height <= y <= target[1] + half_height and
  218. target[2] - half_window <= z <= target[2] + half_window):
  219. count += 1
  220. if count >= min_consecutive:
  221. return True
  222. else:
  223. count = 0
  224. return False
  225. def calculate_success(states, targs, is_3d, is_continuous_navigation):
  226. results = []
  227. for s, t in zip(states, targs):
  228. if is_3d:
  229. s = np.array(s) + np.array([0, 0.25, 0])
  230. results.append(is_within_target_window_3D(s, t))
  231. else:
  232. s = np.array(s)[:, [0, 2]] # x, z
  233. t_2d = [t[0], t[2]]
  234. if is_continuous_navigation:
  235. results.append(is_within_target_window_2D(s, t_2d, window_size=5.6))
  236. else:
  237. results.append(is_within_target_window_2D(s, t_2d, window_size=4.2))
  238. return np.mean(results)
  239. is_3d = "3D" in experiment
  240. is_continuous_navigation = "Continuous Navigation" in experiment
  241. conc_trials = [item for sublist in trials for item in sublist]
  242. states_data = [list(zip(trial.avatarTrajectory['x'],
  243. trial.avatarTrajectory['y'],
  244. trial.avatarTrajectory['z'])) for trial in conc_trials]
  245. targets = [trial.targetPosition for trial in conc_trials]
  246. observed = calculate_success(states_data, targets, is_3d, is_continuous_navigation)
  247. null_distribution = [
  248. calculate_success(states_data, np.random.permutation(targets), is_3d, is_continuous_navigation)
  249. for _ in range(num_permutations)
  250. ]
  251. return np.mean(null_distribution), observed, null_distribution
  252. def compute_session_accuracy(correct_trials, incorrect_trials, monkey, task=None):
  253. """
  254. Computes session-level success rates or parameter-split accuracy based on the task.
  255. Args:
  256. correct_trials (list of list): Correct trials per session
  257. incorrect_trials (list of list): Incorrect trials per session
  258. monkey (str): Monkey name (unused here but kept for compatibility)
  259. task (str): Optional task string. If includes 'Obstacle', 'Respawn', or '3D Center-out',
  260. computes split accuracy based on relevant parameter.
  261. Returns:
  262. If matched task: (accuracy_group1, accuracy_group2)
  263. Else: full-session accuracy list
  264. """
  265. def compute_parameter_accuracy(correct_trials, incorrect_trials, parameter):
  266. def compute_accuracy(correct, incorrect):
  267. return [len(c) / (len(c) + len(i)) * 100 if len(c) + len(i) > 0 else np.nan
  268. for c, i in zip(correct, incorrect)]
  269. def split_respawn_trials(trials):
  270. respawn = [t for t in trials if not np.all(np.isnan(t.targetJumpPosition))]
  271. norespawn = [t for t in trials if np.all(np.isnan(t.targetJumpPosition))]
  272. return respawn, norespawn
  273. def split_obstacle_trials(trials):
  274. 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)]
  275. obstacle_positions = [(-3.5, 0, 3), (-1.75, 0, 4.25), (0, 0, 4.6), (3.5, 0, 3), (1.75, 0, 4.25)]
  276. def obstacle_status(target, obstacle):
  277. t_idx = next((i for i, pos in enumerate(target_positions) if np.array_equal(pos, target)), -1)
  278. o_idx = next((i for i, pos in enumerate(obstacle_positions) if np.array_equal(pos, obstacle)), -1)
  279. return "onPath" if t_idx == o_idx and t_idx != -1 else "offPath"
  280. on_path = [t for t in trials if obstacle_status(t.targetPosition, t.obstaclePosition) == "onPath"]
  281. off_path = [t for t in trials if obstacle_status(t.targetPosition, t.obstaclePosition) == "offPath"]
  282. return on_path, off_path
  283. def split_3D_trials(trials):
  284. upper = [t for t in trials if t.targetPosition[1] == 2.35]
  285. lower = [t for t in trials if t.targetPosition[1] == 0.75]
  286. return upper, lower
  287. # Select splitter and condition names
  288. if task == "Respawn":
  289. splitter = split_respawn_trials
  290. name1, name2 = "Respawn", "No Respawn"
  291. elif task == "Obstacle":
  292. splitter = split_obstacle_trials
  293. name1, name2 = "Obstacle on Path", "Obstacle off Path"
  294. elif task == "3D Center-out":
  295. splitter = split_3D_trials
  296. name1, name2 = "Upper targets", "Lower targets"
  297. else:
  298. full_accuracy = compute_accuracy(correct_trials, incorrect_trials)
  299. return {"All": full_accuracy}
  300. # Apply splitter per session
  301. all_c1, all_c2, all_i1, all_i2 = [], [], [], []
  302. for correct, incorrect in zip(correct_trials, incorrect_trials):
  303. c1, c2 = splitter(correct)
  304. i1, i2 = splitter(incorrect)
  305. all_c1.append(c1)
  306. all_c2.append(c2)
  307. all_i1.append(i1)
  308. all_i2.append(i2)
  309. acc1 = compute_accuracy(all_c1, all_i1)
  310. acc2 = compute_accuracy(all_c2, all_i2)
  311. return {
  312. name1: acc1,
  313. name2: acc2
  314. }
  315. # Check task type
  316. if task in ["Obstacle", "Respawn", "3D Center-out"]:
  317. return compute_parameter_accuracy(correct_trials, incorrect_trials, task)
  318. else:
  319. return [len(c) / (len(c) + len(i)) * 100 if len(c) + len(i) > 0 else np.nan
  320. for c, i in zip(correct_trials, incorrect_trials)]
  321. def compute_target_accuracy(correct_trials, incorrect_trials, monkey, experiment_name):
  322. """
  323. Computes success rate per target and returns a flat list of dicts with Task, Target, Category, SuccessRate.
  324. Args:
  325. correct_trials (list of list): Correct trials per session.
  326. incorrect_trials (list of list): Incorrect trials per session.
  327. monkey (str): Monkey name.
  328. experiment_name (str): Task name.
  329. Returns:
  330. list of dict: [{'Task': ..., 'Target': ..., 'Category': ..., 'SuccessRate': ...}, ...]
  331. """
  332. def target_category(target):
  333. x, _, z = target
  334. key = (round(x, 1), round(z, 1))
  335. if key in {(-7.0, 6.0), (0.0, 9.2), (7.0, 6.0), (-6.0, 7.0), (6.0, 7.0)}:
  336. return "Pretrained"
  337. elif key in {(-3.5, 8.5), (3.5, 8.5), (-3.0, 8.7), (3.0, 8.7)}:
  338. return "New"
  339. else:
  340. return "Other"
  341. target_answers = defaultdict(list)
  342. for correct, incorrect in zip(correct_trials, incorrect_trials):
  343. for trial in correct:
  344. target = tuple(trial.targetPosition)
  345. target_answers[target].append(1)
  346. for trial in incorrect:
  347. target = tuple(trial.targetPosition)
  348. target_answers[target].append(0)
  349. summary = []
  350. for target, answers in target_answers.items():
  351. if answers:
  352. rate = sum(answers) / len(answers)
  353. summary.append({
  354. "Task": experiment_name,
  355. "Target": target,
  356. "Category": target_category(target),
  357. "SuccessRate": rate * 100
  358. })
  359. return summary
  360. def post_hoc_testing(all_trials, experiment, monkey):
  361. df_trials = make_df_structure(all_trials, experiment, monkey)
  362. if experiment == "3D Center-out":
  363. # Define the upper and lower target groups
  364. df_trials['targetLevel'] = df_trials['targetPosition'].apply(lambda x: 'upper' if x[1] > 1 else 'lower')
  365. # Compute time-to-target
  366. print(f"Significant difference in time-to-target({experiment} & {monkey}):")
  367. compute_significance_time_to_target(df_trials, experiment)
  368. elif experiment == "Respawn":
  369. print(f"Significant difference in time-to-target({experiment} & {monkey}):")
  370. compute_significance_time_to_target(df_trials, experiment)
  371. elif experiment == "Obstacle":
  372. print(f"Significant difference in time-to-target({experiment} & {monkey}):")
  373. compute_significance_time_to_target(df_trials, experiment)
  374. return None
  375. def significant_difference_distribution_endpoints(monkey, lower_trials, upper_trials):
  376. endpoints_upper_trials = np.array([trial.avatarTrajectory['y'][-10] for trial in upper_trials])
  377. endpoints_lower_trials = np.array([trial.avatarTrajectory['y'][-10] for trial in lower_trials])
  378. # Mann-Whitney U test
  379. u_stat, p_value_mannwhitney = mannwhitneyu(endpoints_upper_trials, endpoints_lower_trials)
  380. print(f"Mann-Whitney U test: U-statistic = {u_stat}, p-value = {p_value_mannwhitney}")
  381. # Plot density plots
  382. plot_3D_endpoints_density_plots(monkey, endpoints_upper_trials, endpoints_lower_trials)
  383. def summarize_emg_eyemovement_results(monkey, experiments, analysis, base_dir, coordinate = None):
  384. correlation_results = []
  385. linear_results = []
  386. nonlinear_results = []
  387. for experiment in experiments:
  388. data, velocityData, _ = load_emg_eyemovement_files(experiment, monkey, analysis, base_dir, coordinate)
  389. if len(data) == 0:
  390. continue
  391. if analysis == "eyemovement":
  392. eyemovementData = [[segment['eyeMovement'] for segment in trial] for trial in data]
  393. corr_df, reg_df = analyze_eyemovement(eyemovementData, velocityData, experiment)
  394. else:
  395. corr_df, reg_df = analyze_emg(data, velocityData, experiment)
  396. for velocity in corr_df['Velocity Component'].unique():
  397. corr_data = corr_df[(corr_df['Experiment'] == experiment) & (corr_df['Velocity Component'] == velocity)]
  398. regression_data = reg_df[(reg_df['Experiment'] == experiment) & (reg_df['Velocity Component'] == velocity)]
  399. correlation_results.append({
  400. 'Task': experiment,
  401. 'Velocity': velocity,
  402. 'Mean ± STD Spearman Correlation': f"{corr_data['Spearman Correlation'].mean():.7f} ± {corr_data['Spearman Correlation'].std():.7f}",
  403. 'Combined p (Correlation)': combined_p_value(corr_data['p-value'].tolist())
  404. })
  405. linear_results.append({
  406. 'Task': experiment,
  407. 'Velocity': velocity,
  408. 'Linear R²': f"{regression_data['Linear R²'].median():.7f} [{compute_iqr(regression_data['Linear R²']):.7f}]",
  409. 'Linear MSE': f"{regression_data['Linear MSE'].median():.7f} [{compute_iqr(regression_data['Linear MSE']):.7f}]",
  410. 'Combined p (Linear)': combined_p_value(regression_data['Linear p-value'].tolist())
  411. })
  412. nonlinear_results.append({
  413. 'Task': experiment,
  414. 'Velocity': velocity,
  415. 'Non-Linear R²': f"{regression_data['Polynomial R²'].median():.7f} [{compute_iqr(regression_data['Polynomial R²']):.7f}]",
  416. 'Non-Linear MSE': f"{regression_data['Polynomial MSE'].median():.7f} [{compute_iqr(regression_data['Polynomial MSE']):.7f}]",
  417. 'Combined p (Non-Linear)': combined_p_value(regression_data['Polynomial p-value'].tolist())
  418. })
  419. # Merge summaries
  420. df_corr = pd.DataFrame(correlation_results)
  421. df_lin = pd.DataFrame(linear_results)
  422. df_nonlin = pd.DataFrame(nonlinear_results)
  423. final_table = df_corr.merge(df_lin, on=['Task', 'Velocity']).merge(df_nonlin, on=['Task', 'Velocity'])
  424. return final_table
  425. def analyze_eyemovement(eyemovementData, velocityData, experiment_name):
  426. correlation_list = []
  427. linear_regression_list = []
  428. poly_regression_list = []
  429. # Process EMG and velocity data across all trials
  430. all_sessions_emg, all_sessions_velocity = process_eyemovement_data(eyemovementData, velocityData, sampling_rate = 20)
  431. for session_emg, session_velocity in zip(all_sessions_emg, all_sessions_velocity):
  432. # Perform the analyses
  433. spearman_results = spearman_correlation(session_emg, session_velocity, experiment_name)
  434. linear_results = linear_regression(session_emg, session_velocity, experiment_name)
  435. poly_results = polynomial_regression(session_emg, session_velocity, experiment_name=experiment_name)
  436. # Append the results from each session to the final lists
  437. correlation_list.extend(spearman_results)
  438. linear_regression_list.extend(linear_results)
  439. poly_regression_list.extend(poly_results)
  440. # Return DataFrames for the tables and mutual information results
  441. correlation_df = pd.DataFrame(correlation_list)
  442. regression_df = pd.DataFrame({
  443. 'Experiment': [res['Experiment'] for res in linear_regression_list],
  444. 'Velocity Component': [res['Velocity Component'] for res in linear_regression_list],
  445. 'Linear R²': [res['R² Score'] for res in linear_regression_list],
  446. 'Linear MSE': [res['MSE'] for res in linear_regression_list],
  447. 'Cross-validated Linear MSE': [res['Cross-Validated MSE'] for res in linear_regression_list],
  448. 'Linear p-value': [res['p-value'] for res in linear_regression_list],
  449. 'Polynomial R²': [res['R² Score (Polynomial)'] for res in poly_regression_list],
  450. 'Polynomial MSE': [res['MSE (Polynomial)'] for res in poly_regression_list],
  451. 'Polynomial p-value': [res['p-value (Polynomial)'] for res in poly_regression_list],
  452. 'Degree': [res['Degree'] for res in poly_regression_list]
  453. })
  454. return correlation_df, regression_df
  455. def analyze_emg(emgData, velocityData, experiment_name):
  456. correlation_list = []
  457. linear_regression_list = []
  458. poly_regression_list = []
  459. # Process EMG and velocity data across all trials
  460. all_sessions_emg, all_sessions_velocity = process_emg_data(emgData, velocityData, sampling_rate = 20)
  461. for session_emg, session_velocity in zip(all_sessions_emg, all_sessions_velocity):
  462. # Perform the analyses
  463. spearman_results = spearman_correlation(session_emg, session_velocity, experiment_name)
  464. linear_results = linear_regression(session_emg, session_velocity, experiment_name)
  465. poly_results = polynomial_regression(session_emg, session_velocity, experiment_name=experiment_name)
  466. # Append the results from each session to the final lists
  467. correlation_list.extend(spearman_results)
  468. linear_regression_list.extend(linear_results)
  469. poly_regression_list.extend(poly_results)
  470. # Return DataFrames for the tables and mutual information results
  471. correlation_df = pd.DataFrame(correlation_list)
  472. regression_df = pd.DataFrame({
  473. 'Experiment': [res['Experiment'] for res in linear_regression_list],
  474. 'Velocity Component': [res['Velocity Component'] for res in linear_regression_list],
  475. 'Linear R²': [res['R² Score'] for res in linear_regression_list],
  476. 'Linear MSE': [res['MSE'] for res in linear_regression_list],
  477. 'Cross-validated Linear MSE': [res['Cross-Validated MSE'] for res in linear_regression_list],
  478. 'Linear p-value': [res['p-value'] for res in linear_regression_list],
  479. 'Polynomial R²': [res['R² Score (Polynomial)'] for res in poly_regression_list],
  480. 'Polynomial MSE': [res['MSE (Polynomial)'] for res in poly_regression_list],
  481. 'Polynomial p-value': [res['p-value (Polynomial)'] for res in poly_regression_list],
  482. 'Degree': [res['Degree'] for res in poly_regression_list]
  483. })
  484. return correlation_df, regression_df
  485. def analyze_within_session_trends(monkeys, experiments, base_dir):
  486. """
  487. Analyze within-session performance trends (adaptation) using all trials.
  488. Returns a DataFrame with slope, p-value, R², number of trials, and trend classification for each session.
  489. """
  490. limited_monkeys_tasks = {"3D Center-out", "Respawn", "Obstacle"}
  491. all_data = []
  492. def is_success(answer):
  493. if answer == 1:
  494. return 1
  495. elif answer in [3, 5, 6]:
  496. return 0
  497. else:
  498. return None
  499. def classify_trend(slope, p_value):
  500. if p_value < 0.05:
  501. if slope > 0:
  502. return "Improvement"
  503. elif slope < 0:
  504. return "Decline"
  505. return "Constant"
  506. for experiment in experiments:
  507. session_data = []
  508. if experiment in limited_monkeys_tasks:
  509. task_monkeys = [m for m in monkeys if m != "Monkey 1"]
  510. else:
  511. task_monkeys = monkeys
  512. for monkey in task_monkeys:
  513. all_trials, correct, incorrect, all_training_trials, all_channels, *_ = load_files(experiment, monkey, base_dir)
  514. for session_idx, session in enumerate(all_trials):
  515. answers = [trial.answer for trial in session]
  516. binary_success = np.array([is_success(a) for a in answers if is_success(a) is not None])
  517. if len(binary_success) < 7:
  518. continue # skip short sessions
  519. # Moving average smoothing
  520. window_size = 7
  521. moving_avg = np.convolve(binary_success, np.ones(window_size)/window_size, mode='valid')
  522. x_vals = np.arange(len(moving_avg))
  523. # Linear regression
  524. slope, intercept, r_value, p_value, std_err = linregress(x_vals, moving_avg)
  525. trend = classify_trend(slope, p_value)
  526. session_data.append({
  527. "Monkey": monkey,
  528. "Task": experiment,
  529. "Session": session_idx + 1,
  530. "Trials": len(binary_success),
  531. "Slope": slope,
  532. "p_value": p_value,
  533. "R_squared": r_value**2,
  534. "Trend": trend
  535. })
  536. df_sessions = pd.DataFrame(session_data)
  537. all_data.append(df_sessions)
  538. df_all_sessions = pd.concat(all_data, ignore_index=True)
  539. return df_all_sessions
  540. def compute_parameter_accuracy(correct_trials: List[List], incorrect_trials: List[List], parameter: str) -> Tuple[List[float], List[float]]:
  541. """
  542. Computes per-session success rate split by task-specific parameters (e.g., Respawn vs No Respawn,
  543. Obstacle On-path vs Off-path, 3D Upper vs Lower targets).
  544. Args:
  545. correct_trials (List[List]): List of sessions containing correct trials.
  546. incorrect_trials (List[List]): List of sessions containing incorrect trials.
  547. parameter (str): Task type ('Respawn', 'Obstacle', or '3D').
  548. Returns:
  549. Tuple[List[float], List[float]]: Success rates for parameter 1 group and parameter 2 group.
  550. """
  551. def split_respawn(trials):
  552. respawn = [t for t in trials if not np.all(np.isnan(t.targetJumpPosition))]
  553. no_respawn = [t for t in trials if np.all(np.isnan(t.targetJumpPosition))]
  554. return respawn, no_respawn
  555. def split_obstacle(trials):
  556. def is_on_path(target, obstacle):
  557. 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)]
  558. obstacle_positions = [(-3.5, 0, 3), (-1.75, 0, 4.25), (0, 0, 4.6), (3.5, 0, 3), (1.75, 0, 4.25)]
  559. ti = next((i for i, p in enumerate(target_positions) if np.allclose(p, target)), -1)
  560. oi = next((i for i, p in enumerate(obstacle_positions) if np.allclose(p, obstacle)), -1)
  561. return ti == oi and ti != -1
  562. on_path = [t for t in trials if is_on_path(t.targetPosition, t.obstaclePosition)]
  563. off_path = [t for t in trials if not is_on_path(t.targetPosition, t.obstaclePosition)]
  564. return on_path, off_path
  565. def split_3D(trials):
  566. upper = [t for t in trials if t.targetPosition[1] == 2.35]
  567. lower = [t for t in trials if t.targetPosition[1] == 0.75]
  568. return upper, lower
  569. def compute_session_rates(group1, group2):
  570. rates1 = [len(g1) / (len(g1) + len(g2)) * 100 if (len(g1) + len(g2)) > 0 else 0
  571. for g1, g2 in zip(group1, group2)]
  572. return rates1
  573. group1_correct = []
  574. group2_correct = []
  575. group1_incorrect = []
  576. group2_incorrect = []
  577. if parameter == "Respawn":
  578. for c, i in zip(correct_trials, incorrect_trials):
  579. c1, c2 = split_respawn(c)
  580. i1, i2 = split_respawn(i)
  581. group1_correct.append(c1)
  582. group2_correct.append(c2)
  583. group1_incorrect.append(i1)
  584. group2_incorrect.append(i2)
  585. elif parameter == "Obstacle":
  586. for c, i in zip(correct_trials, incorrect_trials):
  587. c1, c2 = split_obstacle(c)
  588. i1, i2 = split_obstacle(i)
  589. group1_correct.append(c1)
  590. group2_correct.append(c2)
  591. group1_incorrect.append(i1)
  592. group2_incorrect.append(i2)
  593. elif parameter == "3D":
  594. for c, i in zip(correct_trials, incorrect_trials):
  595. c1, c2 = split_3D(c)
  596. i1, i2 = split_3D(i)
  597. group1_correct.append(c1)
  598. group2_correct.append(c2)
  599. group1_incorrect.append(i1)
  600. group2_incorrect.append(i2)
  601. else:
  602. raise ValueError("Unsupported parameter type. Use 'Respawn', 'Obstacle', or '3D'.")
  603. acc1 = compute_session_rates(group1_correct, group1_incorrect)
  604. acc2 = compute_session_rates(group2_correct, group2_incorrect)
  605. return acc1, acc2

analysis.py at commit 6a198ce, under MIT · at the source

Overview

Authors: Ophelie Saussus1, Sofie De Schrijver1,2, Jesus Garcia Ramirez3, Thomas Decramer3, Peter Janssen1
  1. Laboratory for Neuro- and Psychophysiology, Department of Neurosciences, KU Leuven and the Leuven Brain Institute, Leuven, Belgium
  2. Department of Electrical and Computer Engineering, University of Washington, Seattle, WA, USA
  3. Research Group Experimental Neurosurgery and Neuroanatomy, KU Leuven and the Leuven Brain Institute, Leuven, Belgium
Journal: Science advances, volume 12, issue 16, article eadw3876
Dates: received 30 January 2025; accepted 12 March 2026; published online 15 April 2026; in print April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1126/sciadv.adw3876 · PMID 41984955 · PMCID PMC13082338 · OpenAlex W4410258541
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: non-human primate (organism), systems (subfield)
Methods: Spectral & time-frequency, Preprocessing, Statistics, Machine learning, Connectivity, Smoothing, state filtering, decompositions, Physiology & signal measures
MeSH: Brain*, Brain-Computer Interfaces*, Motor Cortex*, Virtual Reality*, Animals, Macaca (* major topic)
Topic: EEG and Brain-Computer Interfaces (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: KU Leuven (C14/18/100, C14/22/134); Fonds Wetenschappelijk Onderzoek (G.097422N)
Citations: cited by 2 papers (Europe PMC); 43 references in the paper

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/avatar velocities. Second, we developed a realistic, immersive 3D virtual reality setup with dynamic camera tracking, allowing continuous navigation and obstacle avoidance that closely mimic real-world scenarios. Last, our BCI approach is very well suited for use by paralyzed patients, featuring a brief passive fixation without overt movements and closed-loop operation without retraining of the decoder during online decoding, relying on the user’s neural plasticity and the decoder’s robust generalization across tasks. Our BCI adapted to different environments, targets, and obstacles, illustrating its potential to substantially enhance the quality of life for paralyzed patients by enabling natural, reliable, and flexible control in complex settings.

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

License: MIT
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data, code, and materials availability:”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: pandas (9 files), NumPy (8 files), Matplotlib (4 files), SciPy (4 files), statsmodels (3 files), seaborn (2 files), scikit-learn (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers (HTTP 200)
  • 29 September 2026: the link answers (HTTP 200)
15 files

ophelie-bci/ibci_analysis

License: MIT
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: 6a198cec614d11e54fc7bc34a47c7270c3713da0, 2 March 2026
Languages: Python (13)
Size: 65 files, 13 scripts
Software Heritage: not archived
Found in: the Zenodo archive record
Holds: README, license file, CITATION.cff, environment (requirements.txt)
Not found: tests, continuous integration, documentation
Tools: pandas (9 files), NumPy (8 files), Matplotlib (4 files), SciPy (4 files), statsmodels (3 files), seaborn (2 files), scikit-learn (1 file)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
15 files

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

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/or the Supplementary Materials. All processed data supporting the findings of this study are available on the Dryad Digital Repository: https://doi.org/10.5061/dryad.2bvq83c34. Analysis scripts used to reproduce the figures and statistical analyses are publicly available on Zenodo: https://doi.org/10.5281/zenodo.15791186. Additional details about the virtual environment and neural decoding framework used during the experiments are provided in Materials and Methods.

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://doi.org/10.1126/sciadv.adw3876

BibTeX

@article{saussus2026intracortical,
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/sciadv.adw3876},
url = {https://doi.org/10.1126/sciadv.adw3876},
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/04/15
VL - 12
IS - 16
SP - eadw3876
SN - 2375-2548
PB - American Association for the Advancement of Science
DO - 10.1126/sciadv.adw3876
UR - https://doi.org/10.1126/sciadv.adw3876
LA - en
ER -

CSL-JSON

{
"id": "10.1126/sciadv.adw3876",
"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": "Sci Adv",
"volume": "12",
"issue": "16",
"page": "eadw3876",
"DOI": "10.1126/sciadv.adw3876",
"PMID": "41984955",
"PMCID": "PMC13082338",
"ISSN": "2375-2548",
"publisher": "American Association for the Advancement of Science",
"URL": "https://doi.org/10.1126/sciadv.adw3876",
"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 communications
In 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 medicine
In 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 advances
In 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 communications
In 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 reports
In 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: Nature
In 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: Neuroinformatics
In common: seaborn, scikit-learn, pandas, 3 other tools, 2 references
[8] doi:10.1126/sciadv.aef0343 [code]
Learning induces activation-mechanism-dependent neural plasticity in an intracortical microstimulation task.
Journal: Science advances
In 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 communications
In 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 communications
In 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.

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.