OSCR

Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease.

Code ↔ Paper

3 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 3 matches
  1. [1] § Methods › Decoding algorithms of locomotor activities › Balancing of classes ↔ neurodecode/decoder/trainer.py, lines 1–62 · score 0.76 · Random Forest classifier, scikit learn, sklearn, training, errors
  2. [2] § Methods › Neural decoding framework ↔ neurodecode/decoder/trainer.py, lines 1–62 · score 0.66 · scikit learn, Random Forest, cross validation, training, decoder
  3. [3] § Methods › Decoding algorithms of locomotor activities › Balancing of classes ↔ neurodecode/decoder/trainer.py, lines 464–557 · score 0.51 · confusion matrices, cross validation, class, score

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 · 702 lines · 28 KB · gnu · 3 matches

  1. from __future__ import print_function, division
  2. """
  3. trainer.py
  4. Perform cross-validation and train a classifier.
  5. See run() to see the overall flow.
  6. When you add more classifiers, modify "CLASSIFIERS" variable to include your classifier.
  7. Kyuhwa Lee, 2018
  8. Swiss Federal Institute of Technology Lausanne (EPFL)
  9. This program is free software: you can redistribute it and/or modify
  10. it under the terms of the GNU General Public License as published by
  11. the Free Software Foundation, either version 3 of the License, or
  12. (at your option) any later version.
  13. This program is distributed in the hope that it will be useful,
  14. but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. GNU General Public License for more details.
  17. You should have received a copy of the GNU General Public License
  18. along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. """
  20. import os
  21. import sys
  22. import mne
  23. import mne.io
  24. import platform
  25. import numpy as np
  26. import multiprocessing as mp
  27. import sklearn.metrics as skmetrics
  28. import neurodecode.utils.q_common as qc
  29. import neurodecode.utils.pycnbi_utils as pu
  30. import neurodecode.decoder.features as features
  31. from builtins import input
  32. from sklearn.ensemble import RandomForestClassifier
  33. from sklearn.ensemble import GradientBoostingClassifier
  34. from xgboost import XGBClassifier
  35. from lightgbm import LGBMClassifier
  36. from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
  37. from neurodecode.decoder.rlda import rLDA
  38. from neurodecode import logger
  39. from neurodecode.triggers.trigger_def import trigger_def
  40. # supported classifiers: add this part as you add more classifiers
  41. CLASSIFIERS = {'RF':RandomForestClassifier, 'GB':GradientBoostingClassifier, 'XGB':XGBClassifier,
  42. 'LGB':LGBMClassifier, 'LDA':LDA, 'rLDA':rLDA}
  43. # scikit-learn old version compatibility
  44. try:
  45. from sklearn.model_selection import StratifiedShuffleSplit, LeaveOneOut
  46. SKLEARN_OLD = False
  47. except ImportError:
  48. from sklearn.cross_validation import StratifiedShuffleSplit, LeaveOneOut
  49. SKLEARN_OLD = True
  50. mne.set_log_level('ERROR')
  51. os.environ['OMP_NUM_THREADS'] = '1' # actually improves performance for multitaper
  52. def check_config(cfg):
  53. critical_vars = {
  54. 'COMMON': ['TRIGGER_FILE',
  55. 'TRIGGER_DEF',
  56. 'EPOCH',
  57. 'DATA_PATH',
  58. 'PICKED_CHANNELS',
  59. 'SP_FILTER',
  60. 'TP_FILTER',
  61. 'NOTCH_FILTER',
  62. 'FEATURES',
  63. 'CLASSIFIER',
  64. 'CV_PERFORM'],
  65. 'RF': ['n_estimators'],
  66. 'GB': ['n_estimators', 'learning_rate'],
  67. 'XGB': ['n_estimators', 'learning_rate'],
  68. 'LGB': ['n_estimators', 'learning_rate'],
  69. 'LDA': [],
  70. 'rLDA': ['reg_cov'],
  71. 'StratifiedShuffleSplit': ['test_ratio', 'folds', 'seed', 'export_result'],
  72. 'LeaveOneOut': ['export_result']
  73. }
  74. # optional variables with default values
  75. optional_vars = {
  76. 'MULTIPLIER': 1,
  77. 'EXPORT_GOOD_FEATURES': False,
  78. 'FEAT_TOPN': 10,
  79. 'EXPORT_CLS': False,
  80. 'REREFERENCE': None,
  81. 'N_JOBS': None,
  82. 'EXCLUDED_CHANNELS': None,
  83. 'LOAD_EVENTS': None,
  84. 'CV': {'IGNORE_THRES': None, 'DECISION_THRES': None, 'BALANCE_SAMPLES': False},
  85. }
  86. for v in critical_vars['COMMON']:
  87. if not hasattr(cfg, v):
  88. logger.error('%s not defined in config.' % v)
  89. raise KeyError
  90. for key in optional_vars:
  91. if not hasattr(cfg, key):
  92. setattr(cfg, key, optional_vars[key])
  93. logger.warning('Setting undefined parameter %s=%s' % (key, getattr(cfg, key)))
  94. if 'decim' not in cfg.FEATURES['PSD']:
  95. cfg.FEATURES['PSD']['decim'] = 1
  96. # classifier parameters check
  97. selected_classifier = cfg.CLASSIFIER['selected']
  98. if selected_classifier not in cfg.CLASSIFIER:
  99. logger.error('"%s" not defined in your config.' % selected_classifier)
  100. raise KeyError
  101. for v in critical_vars[selected_classifier]:
  102. if v not in cfg.CLASSIFIER[selected_classifier]:
  103. logger.error('parameter %s must be defined for %s classifier.' % (v, selected_classifier))
  104. raise KeyError
  105. cv_selected = cfg.CV_PERFORM['selected']
  106. if cfg.CV_PERFORM[cv_selected] is not None:
  107. if cv_selected not in cfg.CV_PERFORM:
  108. logger.error('"%s" not defined in config.' % cv_selected)
  109. raise KeyError
  110. for v in critical_vars[cv_selected]:
  111. if v not in cfg.CV_PERFORM[cv_selected]:
  112. logger.error('parameter %s must be defined for %s.' % (v, cv_selected))
  113. raise KeyError
  114. if cfg.N_JOBS is None:
  115. cfg.N_JOBS = mp.cpu_count()
  116. return cfg
  117. def balance_samples(X, Y, balance_type, verbose=False):
  118. if balance_type == 'OVER':
  119. """
  120. Oversample from classes that lack samples
  121. """
  122. label_set = np.unique(Y)
  123. max_set = []
  124. X_balanced = np.array(X)
  125. Y_balanced = np.array(Y)
  126. # find a class with maximum number of samples
  127. for c in label_set:
  128. yl = np.where(Y == c)[0]
  129. if len(max_set) == 0 or len(yl) > max_set[1]:
  130. max_set = [c, len(yl)]
  131. for c in label_set:
  132. if c == max_set[0]: continue
  133. yl = np.where(Y == c)[0]
  134. extra_samples = max_set[1] - len(yl)
  135. extra_idx = np.random.choice(yl, extra_samples)
  136. X_balanced = np.append(X_balanced, X[extra_idx], axis=0)
  137. Y_balanced = np.append(Y_balanced, Y[extra_idx], axis=0)
  138. elif balance_type == 'UNDER':
  139. """
  140. Undersample from classes that are excessive
  141. """
  142. label_set = np.unique(Y)
  143. min_set = []
  144. # find a class with minimum number of samples
  145. for c in label_set:
  146. yl = np.where(Y == c)[0]
  147. if len(min_set) == 0 or len(yl) < min_set[1]:
  148. min_set = [c, len(yl)]
  149. yl = np.where(Y == min_set[0])[0]
  150. X_balanced = np.array(X[yl])
  151. Y_balanced = np.array(Y[yl])
  152. for c in label_set:
  153. if c == min_set[0]: continue
  154. yl = np.where(Y == c)[0]
  155. reduced_idx = np.random.choice(yl, min_set[1])
  156. X_balanced = np.append(X_balanced, X[reduced_idx], axis=0)
  157. Y_balanced = np.append(Y_balanced, Y[reduced_idx], axis=0)
  158. elif balance_type is None or balance_type is False:
  159. return X, Y
  160. else:
  161. logger.error('Unknown balancing type %s' % balance_type)
  162. raise ValueError
  163. logger.info_green('\nNumber of samples after %ssampling' % balance_type.lower())
  164. for c in label_set:
  165. logger.info('%s: %d -> %d' % (c, len(np.where(Y == c)[0]), len(np.where(Y_balanced == c)[0])))
  166. return X_balanced, Y_balanced
  167. def crossval_epochs(cv, epochs_data, labels, cls, label_names=None, do_balance=None, n_jobs=None, ignore_thres=None, decision_thres=None):
  168. """
  169. Epoch-based cross-validation used by cross_validate().
  170. Params
  171. ======
  172. cv: scikit-learn cross-validation object
  173. epochs_data: np.array of shape [epochs x samples x features]
  174. labels: np.array of shape [epochs x samples]
  175. cls: classifier
  176. label_names: associated label names {0:'Left', 1:'Right', ...}
  177. do_balance: oversample or undersample to match the number of samples among classes
  178. """
  179. scores = []
  180. f1s = []
  181. cnum = 1
  182. cm_sum = 0
  183. label_set = np.unique(labels)
  184. num_labels = len(label_set)
  185. if label_names is None:
  186. label_names = {l:'%s' % l for l in label_set}
  187. if n_jobs is None:
  188. n_jobs = mp.cpu_count()
  189. if n_jobs > 1:
  190. logger.info('crossval_epochs(): Using %d cores' % n_jobs)
  191. pool = mp.Pool(n_jobs)
  192. results = []
  193. # for classifier itself, single core is usually faster
  194. cls.n_jobs = 1
  195. if SKLEARN_OLD:
  196. splits = cv
  197. else:
  198. splits = cv.split(epochs_data, labels[:, 0])
  199. for train, test in splits:
  200. X_train = np.concatenate(epochs_data[train])
  201. X_test = np.concatenate(epochs_data[test])
  202. Y_train = np.concatenate(labels[train])
  203. Y_test = np.concatenate(labels[test])
  204. if do_balance:
  205. X_train, Y_train = balance_samples(X_train, Y_train, do_balance)
  206. X_test, Y_test = balance_samples(X_test, Y_test, do_balance)
  207. if n_jobs > 1:
  208. results.append(pool.apply_async(fit_predict_thres,
  209. [cls, X_train, Y_train, X_test, Y_test, cnum, label_set, ignore_thres, decision_thres]))
  210. else:
  211. score, cm, f1 = fit_predict_thres(cls, X_train, Y_train, X_test, Y_test, cnum, label_set, ignore_thres, decision_thres)
  212. scores.append(score)
  213. f1s.append(f1)
  214. cm_sum += cm
  215. cnum += 1
  216. if n_jobs > 1:
  217. pool.close()
  218. pool.join()
  219. for r in results:
  220. score, cm, f1 = r.get()
  221. scores.append(score)
  222. f1s.append(f1)
  223. cm_sum += cm
  224. # confusion matrix
  225. cm_sum = cm_sum.astype('float')
  226. if cm_sum.shape[0] != cm_sum.shape[1]:
  227. # we have decision thresholding condition
  228. assert cm_sum.shape[0] < cm_sum.shape[1]
  229. cm_sum_all = cm_sum
  230. cm_sum = cm_sum[:, :cm_sum.shape[0]]
  231. underthres = np.array([r[-1] / sum(r) for r in cm_sum_all])
  232. else:
  233. underthres = None
  234. cm_rate = np.zeros(cm_sum.shape)
  235. for r_in, r_out in zip(cm_sum, cm_rate):
  236. rs = sum(r_in)
  237. if rs > 0:
  238. r_out[:] = r_in / rs
  239. else:
  240. assert min(r) == max(r) == 0
  241. if underthres is not None:
  242. cm_rate = np.concatenate((cm_rate, underthres[:, np.newaxis]), axis=1)
  243. cm_txt = 'Y: ground-truth, X: predicted\n'
  244. max_chars = 12
  245. tpl_str = '%%-%ds ' % max_chars
  246. tpl_float = '%%-%d.2f ' % max_chars
  247. for l in label_set:
  248. cm_txt += tpl_str % label_names[l][:max_chars]
  249. if underthres is not None:
  250. cm_txt += tpl_str % 'Ignored'
  251. cm_txt += '\n'
  252. for r in cm_rate:
  253. for c in r:
  254. cm_txt += tpl_float % c
  255. cm_txt += '\n'
  256. cm_txt += 'Average accuracy: %.2f\n' % np.mean(scores)
  257. cm_txt += 'Average F1 score: %.2f\n' % np.mean(f1s)
  258. return np.array(scores), cm_txt
  259. def balance_tpr(cfg, featdata):
  260. """
  261. Find the threshold of class index 0 that yields equal number of true positive samples of each class.
  262. Currently only available for binary classes.
  263. Params
  264. ======
  265. cfg: config module
  266. feetdata: feature data computed using compute_features()
  267. """
  268. n_jobs = cfg.N_JOBS
  269. if n_jobs is None:
  270. n_jobs = mp.cpu_count()
  271. if n_jobs > 1:
  272. logger.info('balance_tpr(): Using %d cores' % n_jobs)
  273. pool = mp.Pool(n_jobs)
  274. results = []
  275. # Init a classifier
  276. selected_classifier = cfg.CLASSIFIER[cfg.CLASSIFIER['selected']]
  277. if selected_classifier not in CLASSIFIERS:
  278. logger.error('Unsupported classifier %s' % selected_classifier)
  279. raise ValueError
  280. params = cfg.CLASSIFIER[selected_classifier]
  281. cls = CLASSIFIERS[selected_classifier](**params)
  282. # Setup features
  283. X_data = featdata['X_data']
  284. Y_data = featdata['Y_data']
  285. wlen = featdata['wlen']
  286. if cfg.CLASSIFIER['PSD']['wlen'] is None:
  287. cfg.CLASSIFIER['PSD']['wlen'] = wlen
  288. # Choose CV type
  289. ntrials, nsamples, fsize = X_data.shape
  290. selected_CV = cfg.CV_PERFORM[cfg.CV_PERFORM['selected']]
  291. if cselected_CV == 'LeaveOneOut':
  292. logger.info_green('\n%d-fold leave-one-out cross-validation' % ntrials)
  293. if SKLEARN_OLD:
  294. cv = LeaveOneOut(len(Y_data))
  295. else:
  296. cv = LeaveOneOut()
  297. elif selected_CV == 'StratifiedShuffleSplit':
  298. logger.info_green('\n%d-fold stratified cross-validation with test set ratio %.2f' % (cfg.CV_PERFORM[selected_CV]['folds'], cfg.CV_PERFORM[selected_CV]['test_ratio']))
  299. if SKLEARN_OLD:
  300. cv = StratifiedShuffleSplit(Y_data[:, 0], cfg.CV_PERFORM[selected_CV]['folds'], test_size=cfg.CV_PERFORM[selected_CV]['test_ratio'], random_state=cfg.CV_PERFORM[selected_CV]['random_seed'])
  301. else:
  302. cv = StratifiedShuffleSplit(n_splits=cfg.CV_PERFORM[selected_CV]['folds'], test_size=cfg.CV_PERFORM[selected_CV]['test_ratio'], random_state=cfg.CV_PERFORM[selected_CV]['random_seed'])
  303. else:
  304. logger.error('%s is not supported yet. Sorry.' % selected_CV)
  305. raise NotImplementedError
  306. logger.info('%d trials, %d samples per trial, %d feature dimension' % (ntrials, nsamples, fsize))
  307. # For classifier itself, single core is usually faster
  308. cls.n_jobs = 1
  309. Y_preds = []
  310. if SKLEARN_OLD:
  311. splits = cv
  312. else:
  313. splits = cv.split(X_data, Y_data[:, 0])
  314. for cnum, (train, test) in enumerate(splits):
  315. X_train = np.concatenate(X_data[train])
  316. X_test = np.concatenate(X_data[test])
  317. Y_train = np.concatenate(Y_data[train])
  318. Y_test = np.concatenate(Y_data[test])
  319. if n_jobs > 1:
  320. results.append(pool.apply_async(get_predict_proba, [cls, X_train, Y_train, X_test, Y_test, cnum+1]))
  321. else:
  322. Y_preds.append(get_predict_proba(cls, X_train, Y_train, X_test, Y_test, cnum+1))
  323. cnum += 1
  324. # Aggregate predictions
  325. if n_jobs > 1:
  326. pool.close()
  327. pool.join()
  328. for r in results:
  329. Y_preds.append(r.get())
  330. Y_preds = np.concatenate(Y_preds, axis=0)
  331. # Find threshold for class index 0
  332. Y_preds = sorted(Y_preds)
  333. mid_idx = int(len(Y_preds) / 2)
  334. if len(Y_preds) == 1:
  335. return 0.5 # should not reach here in normal conditions
  336. elif len(Y_preds) % 2 == 0:
  337. thres = Y_preds[mid_idx-1] + (Y_preds[mid_idx] - Y_preds[mid_idx-1]) / 2
  338. else:
  339. thres = Y_preds[mid_idx]
  340. return thres
  341. def cva_features(datadir):
  342. """
  343. (DEPRECATED FUNCTION)
  344. """
  345. for fin in qc.get_file_list(datadir, fullpath=True):
  346. if fin[-4:] != '.gdf': continue
  347. fout = fin + '.cva'
  348. if os.path.exists(fout):
  349. logger.info('Skipping', fout)
  350. continue
  351. logger.info("cva_features('%s')" % fin)
  352. qc.matlab("cva_features('%s')" % fin)
  353. def get_predict_proba(cls, X_train, Y_train, X_test, Y_test, cnum):
  354. """
  355. All likelihoods will be collected from every fold of a cross-validaiton. Based on these likelihoods,
  356. a threshold will be computed that will balance the true positive rate of each class.
  357. Available with binary classification scenario only.
  358. """
  359. timer = qc.Timer()
  360. cls.fit(X_train, Y_train)
  361. Y_pred = cls.predict_proba(X_test)
  362. logger.info('Cross-validation %d (%d tests) - %.1f sec' % (cnum, Y_pred.shape[0], timer.sec()))
  363. return Y_pred[:,0]
  364. def fit_predict_thres(cls, X_train, Y_train, X_test, Y_test, cnum, label_list, ignore_thres=None, decision_thres=None):
  365. """
  366. Any likelihood lower than a threshold is not counted as classification score
  367. Confusion matrix, accuracy and F1 score (macro average) are computed.
  368. Params
  369. ======
  370. ignore_thres:
  371. if not None or larger than 0, likelihood values lower than ignore_thres will be ignored
  372. while computing confusion matrix.
  373. """
  374. timer = qc.Timer()
  375. cls.fit(X_train, Y_train)
  376. assert ignore_thres is None or ignore_thres >= 0
  377. if ignore_thres is None or ignore_thres == 0:
  378. Y_pred = cls.predict(X_test)
  379. score = skmetrics.accuracy_score(Y_test, Y_pred)
  380. cm = skmetrics.confusion_matrix(Y_test, Y_pred, labels=label_list)
  381. f1 = skmetrics.f1_score(Y_test, Y_pred, average='macro')
  382. else:
  383. if decision_thres is not None:
  384. logger.error('decision threshold and ignore_thres cannot be set at the same time.')
  385. raise ValueError
  386. Y_pred = cls.predict_proba(X_test)
  387. Y_pred_labels = np.argmax(Y_pred, axis=1)
  388. Y_pred_maxes = np.array([x[i] for i, x in zip(Y_pred_labels, Y_pred)])
  389. Y_index_overthres = np.where(Y_pred_maxes >= ignore_thres)[0]
  390. Y_index_underthres = np.where(Y_pred_maxes < ignore_thres)[0]
  391. Y_pred_overthres = np.array([cls.classes_[x] for x in Y_pred_labels[Y_index_overthres]])
  392. Y_pred_underthres = np.array([cls.classes_[x] for x in Y_pred_labels[Y_index_underthres]])
  393. Y_pred_underthres_count = np.array([np.count_nonzero(Y_pred_underthres == c) for c in label_list])
  394. Y_test_overthres = Y_test[Y_index_overthres]
  395. score = skmetrics.accuracy_score(Y_test_overthres, Y_pred_overthres)
  396. cm = skmetrics.confusion_matrix(Y_test_overthres, Y_pred_overthres, labels=label_list)
  397. cm = np.concatenate((cm, Y_pred_underthres_count[:, np.newaxis]), axis=1)
  398. f1 = skmetrics.f1_score(Y_test_overthres, Y_pred_overthres, average='macro')
  399. logger.info('Cross-validation %d (%.3f) - %.1f sec' % (cnum, score, timer.sec()))
  400. return score, cm, f1
  401. def cross_validate(cfg, featdata, cv_file=None):
  402. """
  403. Perform cross validation
  404. """
  405. # Init a classifier
  406. selected_classifier = cfg.CLASSIFIER['selected']
  407. if selected_classifier not in CLASSIFIERS:
  408. logger.error('Unsupported classifier %s' % selected_classifier)
  409. raise ValueError
  410. params = cfg.CLASSIFIER[selected_classifier]
  411. cls = CLASSIFIERS[selected_classifier](**params)
  412. # Setup features
  413. X_data = featdata['X_data']
  414. Y_data = featdata['Y_data']
  415. wlen = featdata['wlen']
  416. # Choose CV type
  417. ntrials, nsamples, fsize = X_data.shape
  418. selected_cv = cfg.CV_PERFORM['selected']
  419. if selected_cv == 'LeaveOneOut':
  420. logger.info_green('%d-fold leave-one-out cross-validation' % ntrials)
  421. if SKLEARN_OLD:
  422. cv = LeaveOneOut(len(Y_data))
  423. else:
  424. cv = LeaveOneOut()
  425. elif selected_cv == 'StratifiedShuffleSplit':
  426. logger.info_green('%d-fold stratified cross-validation with test set ratio %.2f' % (cfg.CV_PERFORM[selected_cv]['folds'], cfg.CV_PERFORM[selected_cv]['test_ratio']))
  427. if SKLEARN_OLD:
  428. cv = StratifiedShuffleSplit(Y_data[:, 0], cfg.CV_PERFORM[selected_cv]['folds'], test_size=cfg.CV_PERFORM[selected_cv]['test_ratio'], random_state=cfg.CV_PERFORM[selected_cv]['seed'])
  429. else:
  430. cv = StratifiedShuffleSplit(n_splits=cfg.CV_PERFORM[selected_cv]['folds'], test_size=cfg.CV_PERFORM[selected_cv]['test_ratio'], random_state=cfg.CV_PERFORM[selected_cv]['seed'])
  431. else:
  432. logger.error('%s is not supported yet. Sorry.' % cfg.CV_PERFORM[cfg.CV_PERFORM['selected']])
  433. raise NotImplementedError
  434. logger.info('%d trials, %d samples per trial, %d feature dimension' % (ntrials, nsamples, fsize))
  435. # Do it!
  436. timer_cv = qc.Timer()
  437. scores, cm_txt = crossval_epochs(cv, X_data, Y_data, cls, cfg.tdef.by_value, cfg.CV['BALANCE_SAMPLES'], n_jobs=cfg.N_JOBS,
  438. ignore_thres=cfg.CV['IGNORE_THRES'], decision_thres=cfg.CV['DECISION_THRES'])
  439. t_cv = timer_cv.sec()
  440. # Export results
  441. txt = 'Cross validation took %d seconds.\n' % t_cv
  442. txt += '\n- Class information\n'
  443. txt += '%d epochs, %d samples per epoch, %d feature dimension (total %d samples)\n' %\
  444. (ntrials, nsamples, fsize, ntrials * nsamples)
  445. for ev in np.unique(Y_data):
  446. txt += '%s: %d trials\n' % (cfg.tdef.by_value[ev], len(np.where(Y_data[:, 0] == ev)[0]))
  447. if cfg.CV['BALANCE_SAMPLES']:
  448. txt += 'The number of samples was balanced using %ssampling.\n' % cfg.BALANCE_SAMPLES.lower()
  449. txt += '\n- Experiment condition\n'
  450. txt += 'Sampling frequency: %.3f Hz\n' % featdata['sfreq']
  451. txt += 'Spatial filter: %s\n' % cfg.SP_FILTER
  452. txt += 'Spectral filter: %s\n' % cfg.TP_FILTER[cfg.TP_FILTER['selected']]
  453. txt += 'Notch filter: %s\n' % cfg.NOTCH_FILTER[cfg.NOTCH_FILTER['selected']]
  454. #txt += 'Channels: ' + ','.join([str(featdata['ch_names'][p]) for p in featdata['picks']]) + '\n'
  455. txt += 'Channels: %s\n' % featdata['ch_names']
  456. txt += 'PSD range: %.1f - %.1f Hz\n' % (cfg.FEATURES['PSD']['fmin'], cfg.FEATURES['PSD']['fmax'])
  457. txt += 'Window step: %.2f msec\n' % (1000.0 * cfg.FEATURES['PSD']['wstep'] / featdata['sfreq'])
  458. if type(wlen) is list:
  459. for i, w in enumerate(wlen):
  460. txt += 'Window size: %.1f msec\n' % (w * 1000.0)
  461. txt += 'Epoch range: %s sec\n' % (cfg.EPOCH[i])
  462. else:
  463. txt += 'Window size: %.1f msec\n' % (cfg.FEATURES['PSD']['wlen'] * 1000.0)
  464. txt += 'Epoch range: %s sec\n' % (cfg.EPOCH)
  465. txt += 'Decimation factor: %d\n' % cfg.FEATURES['PSD']['decim']
  466. # Compute stats
  467. cv_mean, cv_std = np.mean(scores), np.std(scores)
  468. txt += '\n- Average CV accuracy over %d epochs (random seed=%s)\n' % (ntrials, cfg.CV_PERFORM[cfg.CV_PERFORM['selected']]['seed'])
  469. if cfg.CV_PERFORM[cfg.CV_PERFORM['selected']] in ['LeaveOneOut', 'StratifiedShuffleSplit']:
  470. txt += "mean %.3f, std: %.3f\n" % (cv_mean, cv_std)
  471. txt += 'Classifier: %s, ' % selected_classifier
  472. txt += ', '.join(['%s=%s' % (k, params[k]) for k in params]) + '\n'
  473. if cfg.CV['IGNORE_THRES'] is not None:
  474. txt += 'Decision threshold: %.2f\n' % cfg.CV['IGNORE_THRES']
  475. txt += '\n- Confusion Matrix\n' + cm_txt
  476. logger.info(txt)
  477. # Export to a file
  478. if 'export_result' in cfg.CV_PERFORM[selected_cv] and cfg.CV_PERFORM[selected_cv]['export_result'] is True:
  479. if cv_file is None:
  480. if cfg.EXPORT_CLS is True:
  481. qc.make_dirs('%s/classifier' % cfg.DATA_PATH)
  482. fout = open('%s/classifier/cv_result.txt' % cfg.DATA_PATH, 'w')
  483. else:
  484. fout = open('%s/cv_result.txt' % cfg.DATA_PATH, 'w')
  485. else:
  486. fout = open(cv_file, 'w')
  487. fout.write(txt)
  488. fout.close()
  489. def train_decoder(cfg, featdata, feat_file=None):
  490. """
  491. Train the final decoder using all data
  492. """
  493. # Init a classifier
  494. selected_classifier = cfg.CLASSIFIER['selected']
  495. if selected_classifier not in CLASSIFIERS:
  496. logger.error('Unsupported classifier %s' % selected_classifier)
  497. raise ValueError
  498. params = cfg.CLASSIFIER[selected_classifier]
  499. cls = CLASSIFIERS[selected_classifier](**params)
  500. # Setup features
  501. X_data = featdata['X_data']
  502. Y_data = featdata['Y_data']
  503. wlen = featdata['wlen']
  504. if cfg.FEATURES['PSD']['wlen'] is None:
  505. cfg.FEATURES['PSD']['wlen'] = wlen
  506. w_frames = featdata['w_frames']
  507. ch_names = featdata['ch_names']
  508. ch_names_raw = featdata['ch_names_raw']
  509. X_data_merged = np.concatenate(X_data)
  510. Y_data_merged = np.concatenate(Y_data)
  511. if cfg.CV['BALANCE_SAMPLES']:
  512. X_data_merged, Y_data_merged = balance_samples(X_data_merged, Y_data_merged, cfg.CV['BALANCE_SAMPLES'], verbose=True)
  513. # Start training the decoder
  514. logger.info_green('Training the decoder')
  515. timer = qc.Timer()
  516. cls.n_jobs = cfg.N_JOBS
  517. cls.fit(X_data_merged, Y_data_merged)
  518. logger.info('Trained %d samples x %d dimension in %.1f sec' %\
  519. (X_data_merged.shape[0], X_data_merged.shape[1], timer.sec()))
  520. cls.n_jobs = 1 # always set n_jobs=1 for testing
  521. # Export the decoder
  522. classes = {c:cfg.tdef.by_value[c] for c in np.unique(Y_data)}
  523. if cfg.FEATURES['selected'] == 'PSD':
  524. data = dict(cls=cls, ch_names=ch_names, ch_names_raw=ch_names_raw, psde=featdata['psde'], sfreq=featdata['sfreq'],
  525. picks=featdata['picks'], classes=classes, epochs=cfg.EPOCH, w_frames=w_frames,
  526. w_seconds=cfg.FEATURES['PSD']['wlen'], wstep=cfg.FEATURES['PSD']['wstep'], spatial=cfg.SP_FILTER,
  527. spatial_ch=featdata['picks'], spectral=cfg.TP_FILTER[cfg.TP_FILTER['selected']], spectral_ch=featdata['picks'],
  528. notch=cfg.NOTCH_FILTER[cfg.NOTCH_FILTER['selected']], notch_ch=featdata['picks'], multiplier=cfg.MULTIPLIER,
  529. ref_ch=cfg.REREFERENCE[cfg.REREFERENCE['selected']], decim=cfg.FEATURES['PSD']['decim'])
  530. clsfile = '%s/classifier/classifier-%s.pkl' % (cfg.DATA_PATH, platform.architecture()[0])
  531. qc.make_dirs('%s/classifier' % cfg.DATA_PATH)
  532. qc.save_obj(clsfile, data)
  533. logger.info('Decoder saved to %s' % clsfile)
  534. # Reverse-lookup frequency from FFT
  535. fq = 0
  536. if type(cfg.FEATURES['PSD']['wlen']) == list:
  537. fq_res = 1.0 / cfg.FEATURES['PSD']['wlen'][0]
  538. else:
  539. fq_res = 1.0 / cfg.FEATURES['PSD']['wlen']
  540. fqlist = []
  541. while fq <= cfg.FEATURES['PSD']['fmax']:
  542. if fq >= cfg.FEATURES['PSD']['fmin']:
  543. fqlist.append(fq)
  544. fq += fq_res
  545. # Show top distinctive features
  546. if cfg.FEATURES['selected'] == 'PSD':
  547. logger.info_green('Good features ordered by importance')
  548. if selected_classifier in ['RF', 'GB', 'XGB', 'LGB']:
  549. keys, values = qc.sort_by_value(list(cls.feature_importances_), rev=True)
  550. elif selected_classifier in ['LDA', 'rLDA']:
  551. keys, values = qc.sort_by_value(cls.w, rev=True)
  552. keys = np.array(keys)
  553. values = np.array(values)
  554. if cfg.EXPORT_GOOD_FEATURES:
  555. if feat_file is None:
  556. gfout = open('%s/classifier/good_features.txt' % cfg.DATA_PATH, 'w')
  557. else:
  558. gfout = open(feat_file, 'w')
  559. if type(wlen) is list:
  560. ch_names = []
  561. for w in range(len(wlen)):
  562. for c in featdata['picks']:
  563. ch_names.append('w%d-%s' % (w, ch_names_raw[c]))
  564. chlist, hzlist = features.feature2chz(keys, fqlist, ch_names=ch_names)
  565. valnorm = values.copy()
  566. valsum = np.sum(valnorm)
  567. if valsum > 0:
  568. valnorm = valnorm / valsum * 100.0
  569. # show top-N features
  570. for i, (ch, hz) in enumerate(zip(chlist, hzlist)):
  571. if i >= cfg.FEAT_TOPN:
  572. break
  573. txt = '%-3s %5.1f Hz normalized importance %-6s raw importance %-6s feature %-5d' %\
  574. (ch, hz, '%.2f%%' % valnorm[i], '%.2f' % values[i], keys[i])
  575. logger.info(txt)
  576. if cfg.EXPORT_GOOD_FEATURES:
  577. gfout.write('Importance(%) Channel Frequency Index\n')
  578. for i, (ch, hz) in enumerate(zip(chlist, hzlist)):
  579. gfout.write('%.3f\t%s\t%s\t%d\n' % (valnorm[i], ch, hz, keys[i]))
  580. gfout.close()
  581. # for batch scripts
  582. def batch_run(cfg_module):
  583. cfg = pu.load_config(cfg_module)
  584. cfg = check_config(cfg)
  585. run(cfg)
  586. def run(cfg, cv_file=None, feat_file=None, logger=logger):
  587. # add tdef object
  588. cfg.tdef = trigger_def(cfg.TRIGGER_FILE)
  589. # Extract features
  590. featdata = features.compute_features(cfg)
  591. # Find optimal threshold for TPR balancing
  592. #balance_tpr(cfg, featdata)
  593. # Perform cross validation
  594. if cfg.CV_PERFORM[cfg.CV_PERFORM['selected']] is not None:
  595. cross_validate(cfg, featdata, cv_file=cv_file)
  596. # Train a decoder
  597. if cfg.EXPORT_CLS is True:
  598. train_decoder(cfg, featdata, feat_file=feat_file)
  599. def main():
  600. """
  601. Invoked from console
  602. """
  603. # Load parameters
  604. if len(sys.argv) <= 1:
  605. print('Usage: %s config_module' % os.path.basename(__file__))
  606. return
  607. cfg_module = sys.argv[1]
  608. batch_run(cfg_module)
  609. logger.info('Finished.')
  610. if __name__ == '__main__':
  611. main()

trainer.py at commit 8071d09, under gnu · at the source

Overview

Authors: Stefano Scafa1,2,3, Valeria de Seta1,2, Ruijia Wang2,4, Paula Sánchez López2,4, Andrea Sánchez López1,2, Camille Varescon1,2, Icare Sakr2,4, Nadia Bérard5, Lea Bole-Feysot1,2, Céline Deschenaux1,2, Ian Enderli4, Yohann Thenaisie1,2, Morgane Burri2, Frédéric Merlos2, Vanessa Fleury6,7, Benoit Wicki8, Ettore Accolla9,10, Andria Tziakouri10, Cécile Hübsch10, Mayte Castro Jiménez10
and 7 other authorsJulien F Bally10, Alessandro Puiatti3, Kyuhwa Lee11, Henri Lorach1,2, Antoine Collomb-Clerc4, Jocelyne Bloch1,2,4,5, Eduardo M Moraud1,2,4
  1. Department of Clinical Neurosciences, Lausanne University Hospital (CHUV) and University of Lausanne (UNIL), Lausanne, Switzerland
  2. NeuroRestore, Lausanne University Hospital (CHUV) and Ecole Polytechnique Fédérale de Lausanne (EPFL), Lausanne, Switzerland
  3. Institute of Digital Technologies for Personalized Healthcare (MeDiTech), University of Southern Switzerland (SUPSI), Viganello, Switzerland
  4. Neuro-X Institute, École Polytechnique Fédérale de Lausanne (EPFL), Geneva, Switzerland
  5. Department of Neurosurgery, Lausanne University Hospital (CHUV) and University of Lausanne (UNIL), Lausanne, Switzerland
  6. Department of Neurology, Hôpitaux Universitaires de Genève (HUG), Geneva, Switzerland
  7. Faculty of Medicine, University of Geneva, Geneva, Switzerland
  8. Department of Neurology, Hôpital du Valais, Sion, Switzerland
  9. Department of Neurology, Hôpital Fribourgeois, Fribourg, Switzerland
  10. Department of Neurology, Lausanne University Hospital (CHUV) and University of Lausanne (UNIL), Lausanne, Switzerland
  11. Wyss Center for Bio and Neuroengineering, Geneva, Switzerland
Journal: Nature medicine, volume 32, issue 8, pages 2815-2830
Dates: received 31 July 2025; accepted 29 April 2026; published online 15 June 2026; in print 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1038/s41591-026-04432-4 · PMID 42297979 · PMCID PMC13472876 · OpenAlex W7164830639
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), Parkinson's (population), clinical / translational (subfield)
Methods: Spectral & time-frequency, Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, Connectivity, fMRI & imaging, Evoked potentials, Physiology & signal measures
Keywords: Predictive markers, Parkinson's disease, Therapeutics
MeSH: Deep Brain Stimulation*, Gait*, Parkinson Disease*, Activities of Daily Living, Humans, Subthalamic Nucleus (* major topic)
Topic: Neurological disorders and treatments (Neurology, Medicine), according to OpenAlex
Funding: Swiss National Science Foundation (TMSGI3_218471, 218471)
Citations: cited by 4 papers (Europe PMC); 83 references in the paper

Abstract

Parkinson’s disease leads to a spectrum of locomotor deficits that vary in severity with the nature of daily activities and the fluctuating physiology of patients. Many of these deficits remain inadequately addressed by existing deep brain stimulation therapies that rely on activity-agnostic parameters optimized for cardinal motor symptoms. By contrast, therapies embedding activity-specific parameters have the potential to better address the entire range of symptoms. Here we expose physiological principles that enable real-time decoding of ongoing locomotor activities across motor fluctuations from the neural dynamics of the subthalamic nucleus. This decoding steered activity-dependent adaptations of deep brain stimulation therapies that improved locomotor deficits while preserving efficacy for cardinal motor symptoms across activities of daily living. Our activity-dependent framework provides a blueprint for next-generation neuromodulation therapies that continuously select parameters optimized to the behavioral context and fluctuating physiology of each patient. ClinicalTrials.gov registration NCT06791902.

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 3 matches between paragraphs and lines of code.

dbdq/neurodecode

License: gnu
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 8071d0975910c23b8339b2660e822c68e9cd6f1f, 24 July 2026
Languages: Python (75), Java (2)
Size: 141 files, 77 scripts
Software Heritage: archived
Found in: the text, “Neural decoding framework”
Holds: README, environment (setup.py), documentation
Not found: license file, CITATION.cff, tests, continuous integration
Tools: NumPy (33 files), MNE-Python (18 files), SciPy (14 files), OpenCV (8 files), Matplotlib (4 files), scikit-learn (4 files), h5py (2 files), LightGBM (2 files), XGBoost (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
78 files

Zenodo 19371521

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
At the source:

Code availability

The custom code used in this study, including the implementation of the decoding framework, is available via Zenodo at 10.5281/zenodo.19371521 (ref. 83).

Reproduced under the paper's license (CC BY), from the paper cited above.

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;
  • 77 scripts, each with its path and the digest of its content;
  • 3 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 Availability Statement

All data associated with this study are present in the paper, as well as in Extended Data Figs. 1–9, Extended Data Table 1 or Supplementary Information. Anonymized datasets and scripts that support the findings of this study are available via Zenodo at 10.5281/zenodo.19371521 (ref. 83).

The custom code used in this study, including the implementation of the decoding framework, is available via Zenodo at 10.5281/zenodo.19371521 (ref. 83).

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 2, 28 September 2026

  • Publisher: n/a → Nature Portfolio

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 27 authors, 3 keywords, 6 MeSH terms, 1 funder, 78 references.

Cite

This paper

Scafa, S., de Seta, V., Wang, R., Sánchez López, P., Sánchez López, A., Varescon, C., Sakr, I., Bérard, N., Bole-Feysot, L., Deschenaux, C., Enderli, I., Thenaisie, Y., Burri, M., Merlos, F., Fleury, V., Wicki, B., Accolla, E., Tziakouri, A., Hübsch, C., . . . Moraud, E. M. (2026). Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease. Nature medicine, 32(8), 2815-2830. https://doi.org/10.1038/s41591-026-04432-4

BibTeX

@article{scafa2026activity,
author = {Scafa, Stefano and de Seta, Valeria and Wang, Ruijia and Sánchez López, Paula and Sánchez López, Andrea and Varescon, Camille and Sakr, Icare and Bérard, Nadia and Bole-Feysot, Lea and Deschenaux, Céline and Enderli, Ian and Thenaisie, Yohann and Burri, Morgane and Merlos, Frédéric and Fleury, Vanessa and Wicki, Benoit and Accolla, Ettore and Tziakouri, Andria and Hübsch, Cécile and Castro Jiménez, Mayte and Bally, Julien F and Puiatti, Alessandro and Lee, Kyuhwa and Lorach, Henri and Collomb-Clerc, Antoine and Bloch, Jocelyne and Moraud, Eduardo M},
title = {{Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease}},
journal = {Nature medicine},
year = {2026},
month = jun,
volume = {32},
number = {8},
pages = {2815--2830},
publisher = {Nature Portfolio},
issn = {1078-8956},
doi = {10.1038/s41591-026-04432-4},
url = {https://doi.org/10.1038/s41591-026-04432-4},
pmid = {42297979},
pmcid = {PMC13472876}
}

RIS

TY - JOUR
AU - Scafa, Stefano
AU - de Seta, Valeria
AU - Wang, Ruijia
AU - Sánchez López, Paula
AU - Sánchez López, Andrea
AU - Varescon, Camille
AU - Sakr, Icare
AU - Bérard, Nadia
AU - Bole-Feysot, Lea
AU - Deschenaux, Céline
AU - Enderli, Ian
AU - Thenaisie, Yohann
AU - Burri, Morgane
AU - Merlos, Frédéric
AU - Fleury, Vanessa
AU - Wicki, Benoit
AU - Accolla, Ettore
AU - Tziakouri, Andria
AU - Hübsch, Cécile
AU - Castro Jiménez, Mayte
AU - Bally, Julien F
AU - Puiatti, Alessandro
AU - Lee, Kyuhwa
AU - Lorach, Henri
AU - Collomb-Clerc, Antoine
AU - Bloch, Jocelyne
AU - Moraud, Eduardo M
TI - Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease
T2 - Nature medicine
J2 - Nat Med
PY - 2026
DA - 2026/06/15
VL - 32
IS - 8
SP - 2815
EP - 2830
SN - 1078-8956
PB - Nature Portfolio
DO - 10.1038/s41591-026-04432-4
UR - https://doi.org/10.1038/s41591-026-04432-4
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41591-026-04432-4",
"type": "article-journal",
"title": "Activity-dependent adaptive deep brain stimulation improves gait in Parkinson's disease",
"container-title": "Nature medicine",
"author": [
{
"family": "Scafa",
"given": "Stefano"
},
{
"family": "de Seta",
"given": "Valeria"
},
{
"family": "Wang",
"given": "Ruijia"
},
{
"family": "Sánchez López",
"given": "Paula"
},
{
"family": "Sánchez López",
"given": "Andrea"
},
{
"family": "Varescon",
"given": "Camille"
},
{
"family": "Sakr",
"given": "Icare"
},
{
"family": "Bérard",
"given": "Nadia"
},
{
"family": "Bole-Feysot",
"given": "Lea"
},
{
"family": "Deschenaux",
"given": "Céline"
},
{
"family": "Enderli",
"given": "Ian"
},
{
"family": "Thenaisie",
"given": "Yohann"
},
{
"family": "Burri",
"given": "Morgane"
},
{
"family": "Merlos",
"given": "Frédéric"
},
{
"family": "Fleury",
"given": "Vanessa"
},
{
"family": "Wicki",
"given": "Benoit"
},
{
"family": "Accolla",
"given": "Ettore"
},
{
"family": "Tziakouri",
"given": "Andria"
},
{
"family": "Hübsch",
"given": "Cécile"
},
{
"family": "Castro Jiménez",
"given": "Mayte"
},
{
"family": "Bally",
"given": "Julien F"
},
{
"family": "Puiatti",
"given": "Alessandro"
},
{
"family": "Lee",
"given": "Kyuhwa"
},
{
"family": "Lorach",
"given": "Henri"
},
{
"family": "Collomb-Clerc",
"given": "Antoine"
},
{
"family": "Bloch",
"given": "Jocelyne"
},
{
"family": "Moraud",
"given": "Eduardo M"
}
],
"container-title-short": "Nat Med",
"volume": "32",
"issue": "8",
"page": "2815-2830",
"DOI": "10.1038/s41591-026-04432-4",
"PMID": "42297979",
"PMCID": "PMC13472876",
"ISSN": "1078-8956",
"publisher": "Nature Portfolio",
"URL": "https://doi.org/10.1038/s41591-026-04432-4",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
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/s41591-026-04434-2 [code]
Adaptive deep brain stimulation for dynamic gait control in Parkinson's disease: a randomized feasibility trial.
Journal: Nature medicine
In common: scikit-learn, SciPy, Matplotlib, 1 other tool, Parkinson's, clinical / translational, 10 references
[2] doi:10.1038/s44172-026-00688-3
N2G calibrator: a cross-subject adversarial learning framework for neural signal-driven gait tracking in Parkinson's disease.
Journal: Communications engineering
In common: Parkinson's, 11 references
[3] doi:10.1016/j.ebiom.2026.106293 [code]
Dynamic neural states underpin motor symptom severity in Parkinson's disease: a longitudinal analysis of chronic cortico-subthalamic nucleus recordings.
Journal: EBioMedicine
In common: SciPy, Matplotlib, NumPy, Parkinson's, 7 references
[4] doi:10.1038/s41531-026-01421-9 [code]
Cortico-pallidal beta dynamics underlie impaired turning in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: Parkinson's, 8 references
[5] doi:10.1038/s41531-026-01531-4 [code]
Inconsistent subthalamic local field potential beta activity amid in- and antiphasic neuronal bursts.
Journal: NPJ Parkinson's disease
In common: MNE-Python, h5py, scikit-learn, 3 other tools, Parkinson's, clinical / translational, 4 references
[6] doi:10.1038/s41531-026-01372-1 [code]
Varying patterns of association between cortical large-scale networks and subthalamic nucleus activity in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: MNE-Python, OpenCV, h5py, 4 other tools, Parkinson's, 3 references
[7] doi:10.1038/s41531-026-01439-z [code]
DBSsync: combining intracranial and multimodal data to investigate new biomarkers in Parkinson's disease.
Journal: NPJ Parkinson's disease
In common: MNE-Python, SciPy, Matplotlib, 1 other tool, Parkinson's, clinical / translational, 4 references
[8] doi:10.1002/ana.78206 [code]
Multimodal Image Guidance in Subthalamic Deep Brain Stimulation for Parkinson's Disease.
Journal: Annals of neurology
In common: h5py, SciPy, Matplotlib, 1 other tool, Parkinson's, clinical / translational, 5 references
[9] doi:10.64898/2026.08.12.26350419 [code]
Volitional deep brain stimulation following brain-computer interface training for Parkinson’s disease
Journal: medRxiv (preprint)
In common: SciPy, Matplotlib, NumPy, Parkinson's, clinical / translational, 5 references
[10] doi:10.21203/rs.3.rs-9901682/v1
An Implantable Digital Bridge Enables Arm And Hand Movements After Tetraplegia
Journal: Research Square (preprint)
In common: 2 authors

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.