OSCR

Evaluation of PainWaive: a consumer-grade EEG headset for remote EEG monitoring and neurofeedback research in chronic pain.

Code ↔ Paper

6 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 6 matches
  1. [1] § Methods › EEG data post-processing pipeline ↔ experimental_data_analysis_final.m, lines 249 to 331 · score 0.84 · ft_freqanalysis, epoch overlap, FieldTrip, tapsmofrq, keeptrials, mtmfft · Report a mismatch
  2. [2] § Methods › EEG data post-processing pipeline ↔ clinical_data_analysis_final.m, lines 299 to 405 · score 0.83 · ft_freqanalysis, FieldTrip, analysed, keeptrials, mtmfft, yes · Report a mismatch
  3. [3] § Results › Relative test-retest reliability (clinical sample) ↔ clinical_data_analysis_final.m, lines 58 to 93 · score 0.79 · neuropathic eye pain, reward band, chronic low, 10–15 Hz, overlapping window, beta power · Report a mismatch
  4. [4] § Results › Relative test-retest reliability (clinical sample) ↔ clinical_data_analysis_final.m, lines 58 to 93 · score 0.75 · reward band power, Fixed window PAF, 10–15 Hz, 8–12 Hz, high beta, overlapping · Report a mismatch
  5. [5] § Methods › EEG data pre-processing pipeline ↔ experimental_data_analysis_final.m, lines 137 to 183 · score 0.65 · bad channel, EEGLAB, analogous, PainWaive, pop, LiveAmp · Report a mismatch
  6. [6] § Methods › Data collection procedures—clinical sample ↔ clinical_data_analysis_final.m, lines 1 to 34 · score 0.63 · 10–15 Hz, 20–30 Hz, feedback, 4–7 Hz, raw, windows · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

MATLAB · 808 lines · 25 KB · no license · 4 matches

  1. %% clinical_data_analysis_final.m
  2. % Clinical PainWaive EEG reliability analysis from saved preprocessed recordings.
  3. %
  4. % This script starts from the saved *_eeg_preprocessed.mat files generated by
  5. % the clinical preprocessing pipeline. It does not re-import raw CSV or JSON
  6. % files.
  7. %
  8. % Outputs:
  9. % 1. Long-form short-window EEG feature table
  10. % 2. Pooled descriptive ICCs across all sessions
  11. % 3. Participant-level ICCs for the standard 5-s analysis
  12. % 4. Cohort-stratified ICCs for the standard 5-s analysis
  13. % 5. Separate 2-s 80%-overlap analysis for feedback-timescale estimates
  14. %
  15. % Window definitions:
  16. % - Non-overlapping windows: 1, 2, 3, 4, and 5 s
  17. % - PAF is computed only for windows >= 2 s
  18. % - Additional 2-s windows with 80% overlap are computed for all metrics
  19. %
  20. % Metrics:
  21. % alpha_power = 8-12 Hz
  22. % beta_power = 13-30 Hz
  23. % theta_power = 4-7 Hz
  24. % highbeta_power = 20-30 Hz
  25. % band1015_power = 10-15 Hz
  26. % paf_fixed = centre-of-gravity PAF in 8-12 Hz
  27. %
  28. % Spectral features are computed from C1/C2 when both labels are available.
  29. clearvars
  30. close all
  31. clc
  32. set(0, 'DefaultFigureVisible', 'off');
  33. %% ------------------------------------------------------------------------
  34. % Paths and toolboxes
  35. %% ------------------------------------------------------------------------
  36. addpath('C:\Users\nchowdhury\eeglab2023.0') %Replace with EEGLAB Path
  37. eeglab; close all;
  38. addpath('C:\Users\nchowdhury\fieldtrip-20250106');
  39. addpath('C:\Users\nchowdhury\fieldtrip-20250106\external\eeglab')
  40. addpath('C:\Users\nchowdhury\eeglab2023.0\functions\sigprocfunc')
  41. addpath(genpath('C:\Users\nchowdhury\mcolonFolder'))
  42. ft_defaults;
  43. root_dir = "C:\Users\nchowdhury\Clinical_Data"; %Replace with Data directory
  44. out_dir = fullfile(root_dir, 'clinical_data_analysis_outputs');
  45. if ~exist(out_dir, 'dir')
  46. mkdir(out_dir);
  47. end
  48. %% ------------------------------------------------------------------------
  49. % Analysis settings
  50. %% ------------------------------------------------------------------------
  51. nonoverlap_windows_sec = 1:5;
  52. overlap_window_sec = 2;
  53. overlap_fraction = 0.80;
  54. artifact_threshold_uv = 150;
  55. bands.alpha = [8 12];
  56. bands.beta = [13 30];
  57. bands.theta = [4 7];
  58. bands.highbeta = [20 30];
  59. bands.band1015 = [10 15];
  60. bands.paf = [8 12];
  61. metric_names = {'alpha_power', 'beta_power', 'theta_power', ...
  62. 'highbeta_power', 'band1015_power', 'paf_fixed'};
  63. metric_labels = containers.Map( ...
  64. {'alpha_power','beta_power','theta_power','highbeta_power','band1015_power','paf_fixed'}, ...
  65. {'Alpha power 8-12 Hz', ...
  66. 'Beta power 13-30 Hz', ...
  67. 'Theta power 4-7 Hz', ...
  68. 'High beta power 20-30 Hz', ...
  69. '10-15 Hz reward-band power', ...
  70. 'Fixed-window PAF 8-12 Hz'} );
  71. % Edit this mapping so it matches the participant folders in root_dir.
  72. % Extra rows are ignored. Participants without a listed cohort are labelled
  73. % as "unassigned" in the cohort outputs.
  74. cohort_map = table( ...
  75. ["1"; "2"; "3"; "4"; "BBADE"; "CCSYD"; "EJSYD"; "MDMEL"], ...
  76. ["neuropathic_eye_pain"; "neuropathic_eye_pain"; "neuropathic_eye_pain"; "neuropathic_eye_pain"; ...
  77. "chronic_low_back_pain"; "chronic_low_back_pain"; "chronic_low_back_pain"; "chronic_low_back_pain"], ...
  78. 'VariableNames', {'participant_id','cohort'} );
  79. %% ------------------------------------------------------------------------
  80. % Discover participant folders
  81. %% ------------------------------------------------------------------------
  82. participant_dirs = dir(root_dir);
  83. participant_dirs = participant_dirs([participant_dirs.isdir]);
  84. folder_names = string({participant_dirs.name});
  85. participant_ids_to_include = string(cohort_map.participant_id);
  86. keep = ismember(folder_names, participant_ids_to_include);
  87. participant_dirs = participant_dirs(keep);
  88. if isempty(participant_dirs)
  89. error('No participant folders matching cohort_map were found in %s', root_dir);
  90. end
  91. fprintf('\nParticipant folders retained:\n');
  92. disp(string({participant_dirs.name})');
  93. %% ------------------------------------------------------------------------
  94. % Extract spectral features from saved preprocessed recordings
  95. %% ------------------------------------------------------------------------
  96. rows = repmat(make_empty_output_row(), 0, 1);
  97. row_count = 0;
  98. for p = 1:numel(participant_dirs)
  99. participant_id = string(participant_dirs(p).name);
  100. participant_path = fullfile(participant_dirs(p).folder, participant_dirs(p).name);
  101. session_dirs = dir(participant_path);
  102. session_dirs = session_dirs([session_dirs.isdir]);
  103. session_names = string({session_dirs.name});
  104. keep_sessions = ~startsWith(session_names, ".");
  105. session_dirs = session_dirs(keep_sessions);
  106. fprintf('\nParticipant %s\n', participant_id);
  107. for s = 1:numel(session_dirs)
  108. session_id = string(session_dirs(s).name);
  109. session_path = fullfile(session_dirs(s).folder, session_dirs(s).name);
  110. rec_files = dir(fullfile(session_path, '*_eeg_preprocessed.mat'));
  111. rec_files = sort_files_by_name(rec_files);
  112. if isempty(rec_files)
  113. fprintf(' %s: no saved preprocessed recordings found\n', session_id);
  114. continue
  115. end
  116. fprintf(' Session %s\n', session_id);
  117. for r = 1:numel(rec_files)
  118. recording_name = string(rec_files(r).name);
  119. mat_path = fullfile(rec_files(r).folder, rec_files(r).name);
  120. fprintf(' Recording %d: %s\n', r, recording_name);
  121. S = load(mat_path, 'EEG');
  122. if ~isfield(S, 'EEG') || isempty(S.EEG)
  123. fprintf(' Missing EEG structure. Skipping.\n');
  124. continue
  125. end
  126. EEG = S.EEG;
  127. try
  128. data_ft = eeglab2fieldtrip(EEG, 'preprocessing');
  129. data_ft.label = {EEG.chanlocs.labels};
  130. catch ME
  131. fprintf(' Could not convert to FieldTrip format: %s\n', ME.message);
  132. continue
  133. end
  134. % Non-overlapping analyses, 1 to 5 s.
  135. for w = 1:numel(nonoverlap_windows_sec)
  136. window_sec = nonoverlap_windows_sec(w);
  137. overlap_prop = 0;
  138. analysis_id = "nonoverlap";
  139. allow_paf = window_sec >= 2;
  140. feature_row = analyse_one_window( ...
  141. data_ft, window_sec, overlap_prop, artifact_threshold_uv, bands, allow_paf);
  142. if ~feature_row.success
  143. fprintf(' %s %ds failed: %s\n', analysis_id, window_sec, feature_row.message);
  144. continue
  145. end
  146. row_count = row_count + 1;
  147. rows(row_count) = make_output_row( ...
  148. participant_id, session_id, s, r, recording_name, ...
  149. analysis_id, window_sec, overlap_prop, feature_row);
  150. end
  151. % Additional 2-s 80%-overlap analysis.
  152. window_sec = overlap_window_sec;
  153. overlap_prop = overlap_fraction;
  154. analysis_id = "overlap_2s_80pct";
  155. allow_paf = true;
  156. feature_row = analyse_one_window( ...
  157. data_ft, window_sec, overlap_prop, artifact_threshold_uv, bands, allow_paf);
  158. if feature_row.success
  159. row_count = row_count + 1;
  160. rows(row_count) = make_output_row( ...
  161. participant_id, session_id, s, r, recording_name, ...
  162. analysis_id, window_sec, overlap_prop, feature_row);
  163. else
  164. fprintf(' %s failed: %s\n', analysis_id, feature_row.message);
  165. end
  166. end
  167. end
  168. end
  169. if isempty(rows)
  170. error('No clinical feature rows were created.');
  171. end
  172. LongTableClinical = struct2table(rows);
  173. LongTableClinical = add_cohort_labels(LongTableClinical, cohort_map);
  174. writetable(LongTableClinical, fullfile(out_dir, 'clinical_long_features.csv'));
  175. save(fullfile(out_dir, 'clinical_long_features.mat'), ...
  176. 'LongTableClinical', 'bands', 'nonoverlap_windows_sec', ...
  177. 'overlap_window_sec', 'overlap_fraction', 'artifact_threshold_uv');
  178. %% ------------------------------------------------------------------------
  179. % Descriptive pooled ICC summaries
  180. %% ------------------------------------------------------------------------
  181. PooledICCs = compute_pooled_icc_table(LongTableClinical, metric_names, metric_labels);
  182. disp(' ');
  183. disp('Pooled descriptive ICCs');
  184. disp(PooledICCs);
  185. writetable(PooledICCs, fullfile(out_dir, 'clinical_pooled_iccs.csv'));
  186. save(fullfile(out_dir, 'clinical_pooled_iccs.mat'), 'PooledICCs');
  187. PooledICCs_5s = PooledICCs( ...
  188. PooledICCs.analysis_id == "nonoverlap" & PooledICCs.window_sec == 5, :);
  189. PooledICCs_2sOverlap = PooledICCs( ...
  190. PooledICCs.analysis_id == "overlap_2s_80pct", :);
  191. writetable(PooledICCs_5s, fullfile(out_dir, 'clinical_pooled_iccs_5s_nonoverlap.csv'));
  192. writetable(PooledICCs_2sOverlap, fullfile(out_dir, 'clinical_pooled_iccs_2s_80pct_overlap.csv'));
  193. %% ------------------------------------------------------------------------
  194. % Participant-level and cohort-level summaries for the standard 5-s analysis
  195. %% ------------------------------------------------------------------------
  196. T5 = LongTableClinical( ...
  197. LongTableClinical.analysis_id == "nonoverlap" & ...
  198. LongTableClinical.window_sec == 5, :);
  199. ParticipantICCs_5s = compute_participant_icc_table(T5, metric_names, metric_labels);
  200. CohortICCs_5s = compute_cohort_icc_table(T5, metric_names, metric_labels);
  201. disp(' ');
  202. disp('Participant-level ICCs, 5-s non-overlapping analysis');
  203. disp(ParticipantICCs_5s);
  204. disp(' ');
  205. disp('Cohort-stratified ICCs, 5-s non-overlapping analysis');
  206. disp(CohortICCs_5s);
  207. writetable(ParticipantICCs_5s, fullfile(out_dir, 'clinical_participant_iccs_5s_nonoverlap.csv'));
  208. writetable(CohortICCs_5s, fullfile(out_dir, 'clinical_cohort_iccs_5s_nonoverlap.csv'));
  209. save(fullfile(out_dir, 'clinical_5s_icc_tables.mat'), ...
  210. 'ParticipantICCs_5s', 'CohortICCs_5s', 'PooledICCs_5s');
  211. %% ------------------------------------------------------------------------
  212. % Participant-level and cohort-level summaries for 2-s overlapping analysis
  213. %% ------------------------------------------------------------------------
  214. T2overlap = LongTableClinical(LongTableClinical.analysis_id == "overlap_2s_80pct", :);
  215. ParticipantICCs_2sOverlap = compute_participant_icc_table(T2overlap, metric_names, metric_labels);
  216. CohortICCs_2sOverlap = compute_cohort_icc_table(T2overlap, metric_names, metric_labels);
  217. writetable(ParticipantICCs_2sOverlap, fullfile(out_dir, 'clinical_participant_iccs_2s_80pct_overlap.csv'));
  218. writetable(CohortICCs_2sOverlap, fullfile(out_dir, 'clinical_cohort_iccs_2s_80pct_overlap.csv'));
  219. save(fullfile(out_dir, 'clinical_2s_80pct_overlap_icc_tables.mat'), ...
  220. 'ParticipantICCs_2sOverlap', 'CohortICCs_2sOverlap', 'PooledICCs_2sOverlap');
  221. fprintf('\nAnalysis complete. Outputs saved to:\n%s\n', out_dir);
  222. %% =========================================================================
  223. % Local functions
  224. %% =========================================================================
  225. function out = analyse_one_window(data_ft, window_sec, overlap_prop, artifact_threshold_uv, bands, allow_paf)
  226. out = empty_feature_row();
  227. out.window_sec = window_sec;
  228. out.overlap_prop = overlap_prop;
  229. out.update_interval_sec = window_sec * (1 - overlap_prop);
  230. out.nominal_freq_resolution_hz = 1 / window_sec;
  231. try
  232. cfg = [];
  233. cfg.length = window_sec;
  234. cfg.overlap = overlap_prop;
  235. data_epo = ft_redefinetrial(cfg, data_ft);
  236. catch ME
  237. out.message = "trial definition failed: " + string(ME.message);
  238. return
  239. end
  240. try
  241. cfg = [];
  242. cfg.artfctdef.threshold.channel = 'all';
  243. cfg.artfctdef.threshold.min = -artifact_threshold_uv;
  244. cfg.artfctdef.threshold.max = artifact_threshold_uv;
  245. cfg.continuous = 'no';
  246. [cfg, artifact] = ft_artifact_threshold(cfg, data_epo);
  247. cfg.artfctdef.reject = 'complete';
  248. cfg.artfctdef.threshold.artifact = artifact;
  249. data_clean = ft_rejectartifact(cfg, data_epo);
  250. catch ME
  251. out.message = "artifact rejection failed: " + string(ME.message);
  252. return
  253. end
  254. if ~isfield(data_clean, 'trial') || isempty(data_clean.trial)
  255. out.message = "no surviving trials";
  256. return
  257. end
  258. foi = make_foi(window_sec, 2, 45);
  259. try
  260. cfg = [];
  261. cfg.method = 'mtmfft';
  262. cfg.taper = 'hanning';
  263. cfg.foi = foi;
  264. cfg.keeptrials = 'yes';
  265. cfg.pad = window_sec;
  266. freq_data = ft_freqanalysis(cfg, data_clean);
  267. catch ME
  268. out.message = "spectral analysis failed: " + string(ME.message);
  269. return
  270. end
  271. freqs = freq_data.freq(:)';
  272. pow_allch = freq_data.powspctrm;
  273. % Use the PainWaive central channels for all clinical features. If C1/C2
  274. % labels are unavailable, fall back to all available EEG channels and record
  275. % this in the output table.
  276. labels = string(freq_data.label);
  277. c12_idx = find(ismember(upper(labels), ["C1", "C2"]));
  278. if numel(c12_idx) == 2
  279. pow_selected = pow_allch(:, c12_idx, :);
  280. out.channels_used = "C1;C2";
  281. else
  282. pow_selected = pow_allch;
  283. out.channels_used = strjoin(labels, ";");
  284. end
  285. mean_power_trials = squeeze(mean(pow_selected, 2, 'omitnan'));
  286. if isvector(mean_power_trials)
  287. mean_power_trials = mean_power_trials(:)';
  288. end
  289. mean_power = squeeze(mean(mean_power_trials, 1, 'omitnan'));
  290. mean_power = mean_power(:)';
  291. if numel(mean_power) > 1 && any(isfinite(mean_power))
  292. mean_power_z = zscore(mean_power);
  293. else
  294. mean_power_z = NaN(size(mean_power));
  295. end
  296. out.alpha_power = band_sum(mean_power_z, freqs, bands.alpha);
  297. out.beta_power = band_sum(mean_power_z, freqs, bands.beta);
  298. out.theta_power = band_sum(mean_power_z, freqs, bands.theta);
  299. out.highbeta_power = band_sum(mean_power_z, freqs, bands.highbeta);
  300. out.band1015_power = band_sum(mean_power_z, freqs, bands.band1015);
  301. if allow_paf
  302. out.paf_fixed = compute_paf_cog(mean_power, freqs, bands.paf);
  303. else
  304. out.paf_fixed = NaN;
  305. end
  306. out.n_epochs_kept = numel(data_clean.trial);
  307. out.n_freq_bins = numel(freqs);
  308. out.n_paf_bins = sum(freqs >= bands.paf(1) & freqs <= bands.paf(2));
  309. out.success = true;
  310. out.message = "ok";
  311. end
  312. function out = empty_feature_row()
  313. out = struct();
  314. out.success = false;
  315. out.message = "";
  316. out.window_sec = NaN;
  317. out.overlap_prop = NaN;
  318. out.update_interval_sec = NaN;
  319. out.nominal_freq_resolution_hz = NaN;
  320. out.n_epochs_kept = NaN;
  321. out.n_freq_bins = NaN;
  322. out.n_paf_bins = NaN;
  323. out.channels_used = "";
  324. out.alpha_power = NaN;
  325. out.beta_power = NaN;
  326. out.theta_power = NaN;
  327. out.highbeta_power = NaN;
  328. out.band1015_power = NaN;
  329. out.paf_fixed = NaN;
  330. end
  331. function row = make_output_row(participant_id, session_id, session_index, recording_index, recording_name, ...
  332. analysis_id, window_sec, overlap_prop, feature_row)
  333. row = struct();
  334. row.participant_id = string(participant_id);
  335. row.group_id = string(participant_id);
  336. row.session_id = string(session_id);
  337. row.session_index = session_index;
  338. row.recording_index = recording_index;
  339. row.recording_name = string(recording_name);
  340. row.analysis_id = string(analysis_id);
  341. row.window_sec = window_sec;
  342. row.overlap_prop = overlap_prop;
  343. row.update_interval_sec = feature_row.update_interval_sec;
  344. row.nominal_freq_resolution_hz = feature_row.nominal_freq_resolution_hz;
  345. row.n_epochs_kept = feature_row.n_epochs_kept;
  346. row.n_freq_bins = feature_row.n_freq_bins;
  347. row.n_paf_bins = feature_row.n_paf_bins;
  348. row.channels_used = feature_row.channels_used;
  349. row.alpha_power = feature_row.alpha_power;
  350. row.beta_power = feature_row.beta_power;
  351. row.theta_power = feature_row.theta_power;
  352. row.highbeta_power = feature_row.highbeta_power;
  353. row.band1015_power = feature_row.band1015_power;
  354. row.paf_fixed = feature_row.paf_fixed;
  355. end
  356. function row = make_empty_output_row()
  357. row = struct();
  358. row.participant_id = "";
  359. row.group_id = "";
  360. row.session_id = "";
  361. row.session_index = NaN;
  362. row.recording_index = NaN;
  363. row.recording_name = "";
  364. row.analysis_id = "";
  365. row.window_sec = NaN;
  366. row.overlap_prop = NaN;
  367. row.update_interval_sec = NaN;
  368. row.nominal_freq_resolution_hz = NaN;
  369. row.n_epochs_kept = NaN;
  370. row.n_freq_bins = NaN;
  371. row.n_paf_bins = NaN;
  372. row.channels_used = "";
  373. row.alpha_power = NaN;
  374. row.beta_power = NaN;
  375. row.theta_power = NaN;
  376. row.highbeta_power = NaN;
  377. row.band1015_power = NaN;
  378. row.paf_fixed = NaN;
  379. end
  380. function foi = make_foi(window_sec, min_freq, max_freq)
  381. df = 1 / window_sec;
  382. foi = min_freq:df:max_freq;
  383. foi = round(foi * 1e10) / 1e10;
  384. end
  385. function val = band_sum(power_vec, freqs, band)
  386. idx = freqs >= band(1) & freqs <= band(2);
  387. if ~any(idx) || all(~isfinite(power_vec(idx)))
  388. val = NaN;
  389. else
  390. val = sum(power_vec(idx), 'omitnan');
  391. end
  392. end
  393. function paf = compute_paf_cog(power_vec, freqs, band)
  394. idx = freqs >= band(1) & freqs <= band(2);
  395. f = freqs(idx);
  396. p = power_vec(idx);
  397. if isempty(p) || all(~isfinite(p)) || sum(p, 'omitnan') <= 0
  398. paf = NaN;
  399. else
  400. paf = sum(f .* p, 'omitnan') ./ sum(p, 'omitnan');
  401. end
  402. end
  403. function T = add_cohort_labels(T, cohort_map)
  404. T.participant_id = string(T.participant_id);
  405. cohort_map.participant_id = string(cohort_map.participant_id);
  406. cohort_map.cohort = string(cohort_map.cohort);
  407. T.cohort = repmat("unassigned", height(T), 1);
  408. for i = 1:height(cohort_map)
  409. idx = T.participant_id == cohort_map.participant_id(i);
  410. T.cohort(idx) = cohort_map.cohort(i);
  411. end
  412. unassigned_ids = unique(T.participant_id(T.cohort == "unassigned"));
  413. if ~isempty(unassigned_ids)
  414. fprintf('\nParticipants without cohort labels:\n');
  415. disp(unassigned_ids);
  416. end
  417. end
  418. function ICCSummary = compute_pooled_icc_table(T, metric_names, metric_labels)
  419. rows = struct([]);
  420. rr = 0;
  421. analysis_ids = unique(T.analysis_id, 'stable');
  422. for a = 1:numel(analysis_ids)
  423. analysis_id = analysis_ids(a);
  424. Ta = T(T.analysis_id == analysis_id, :);
  425. window_vals = unique(Ta.window_sec, 'stable');
  426. for w = 1:numel(window_vals)
  427. window_sec = window_vals(w);
  428. Tw = Ta(Ta.window_sec == window_sec, :);
  429. overlap_prop = unique(Tw.overlap_prop);
  430. if isempty(overlap_prop)
  431. overlap_prop = NaN;
  432. else
  433. overlap_prop = overlap_prop(1);
  434. end
  435. for m = 1:numel(metric_names)
  436. metric_name = metric_names{m};
  437. if strcmp(metric_name, 'paf_fixed') && window_sec < 2
  438. continue
  439. end
  440. X = build_stack_matrix(Tw, metric_name);
  441. [icc31, icc3k, n_sessions, n_recordings] = compute_icc31_icc3k(X);
  442. rr = rr + 1;
  443. rows(rr).analysis_id = analysis_id;
  444. rows(rr).window_sec = window_sec;
  445. rows(rr).overlap_prop = overlap_prop;
  446. rows(rr).update_interval_sec = window_sec * (1 - overlap_prop);
  447. rows(rr).nominal_freq_resolution_hz = 1 / window_sec;
  448. rows(rr).metric = string(metric_name);
  449. rows(rr).metric_label = string(metric_labels(metric_name));
  450. rows(rr).n_participants = numel(unique(Tw.participant_id));
  451. rows(rr).n_sessions = n_sessions;
  452. rows(rr).n_recordings = n_recordings;
  453. rows(rr).icc31 = icc31;
  454. rows(rr).icc3k = icc3k;
  455. end
  456. end
  457. end
  458. ICCSummary = struct2table(rows);
  459. end
  460. function ParticipantICCs = compute_participant_icc_table(T, metric_names, metric_labels)
  461. rows = struct([]);
  462. rr = 0;
  463. participant_ids = unique(T.participant_id, 'stable');
  464. for p = 1:numel(participant_ids)
  465. participant_id = participant_ids(p);
  466. Tp = T(T.participant_id == participant_id, :);
  467. cohort_vals = unique(Tp.cohort);
  468. if isempty(cohort_vals)
  469. cohort = "unassigned";
  470. else
  471. cohort = cohort_vals(1);
  472. end
  473. for m = 1:numel(metric_names)
  474. metric_name = metric_names{m};
  475. if strcmp(metric_name, 'paf_fixed') && all(isnan(Tp.(metric_name)))
  476. continue
  477. end
  478. X = build_matrix_one_participant(Tp, metric_name);
  479. [icc31, icc3k, n_sessions, n_recordings] = compute_icc31_icc3k(X);
  480. rr = rr + 1;
  481. rows(rr).participant_id = participant_id;
  482. rows(rr).cohort = cohort;
  483. rows(rr).analysis_id = unique_scalar_string(Tp.analysis_id);
  484. rows(rr).window_sec = unique_scalar_numeric(Tp.window_sec);
  485. rows(rr).overlap_prop = unique_scalar_numeric(Tp.overlap_prop);
  486. rows(rr).metric = string(metric_name);
  487. rows(rr).metric_label = string(metric_labels(metric_name));
  488. rows(rr).n_sessions = n_sessions;
  489. rows(rr).n_recordings = n_recordings;
  490. rows(rr).icc31 = icc31;
  491. rows(rr).icc3k = icc3k;
  492. end
  493. end
  494. if isempty(rows)
  495. ParticipantICCs = table();
  496. else
  497. ParticipantICCs = struct2table(rows);
  498. end
  499. end
  500. function CohortICCs = compute_cohort_icc_table(T, metric_names, metric_labels)
  501. rows = struct([]);
  502. rr = 0;
  503. cohort_vals = unique(T.cohort, 'stable');
  504. for c = 1:numel(cohort_vals)
  505. cohort = cohort_vals(c);
  506. Tc = T(T.cohort == cohort, :);
  507. for m = 1:numel(metric_names)
  508. metric_name = metric_names{m};
  509. if strcmp(metric_name, 'paf_fixed') && all(isnan(Tc.(metric_name)))
  510. continue
  511. end
  512. X = build_stack_matrix(Tc, metric_name);
  513. [icc31, icc3k, n_sessions, n_recordings] = compute_icc31_icc3k(X);
  514. rr = rr + 1;
  515. rows(rr).cohort = cohort;
  516. rows(rr).analysis_id = unique_scalar_string(Tc.analysis_id);
  517. rows(rr).window_sec = unique_scalar_numeric(Tc.window_sec);
  518. rows(rr).overlap_prop = unique_scalar_numeric(Tc.overlap_prop);
  519. rows(rr).metric = string(metric_name);
  520. rows(rr).metric_label = string(metric_labels(metric_name));
  521. rows(rr).n_participants = numel(unique(Tc.participant_id));
  522. rows(rr).n_sessions = n_sessions;
  523. rows(rr).n_recordings = n_recordings;
  524. rows(rr).icc31 = icc31;
  525. rows(rr).icc3k = icc3k;
  526. end
  527. end
  528. if isempty(rows)
  529. CohortICCs = table();
  530. else
  531. CohortICCs = struct2table(rows);
  532. end
  533. end
  534. function X = build_matrix_one_participant(T, metric_name)
  535. if isempty(T)
  536. X = [];
  537. return
  538. end
  539. n_sessions = max(T.session_index);
  540. n_recordings = max(T.recording_index);
  541. if isempty(n_sessions) || isnan(n_sessions) || isempty(n_recordings) || isnan(n_recordings)
  542. X = [];
  543. return
  544. end
  545. X = nan(n_sessions, n_recordings);
  546. for i = 1:height(T)
  547. s = T.session_index(i);
  548. r = T.recording_index(i);
  549. if s >= 1 && r >= 1
  550. X(s, r) = T.(metric_name)(i);
  551. end
  552. end
  553. X = clean_icc_matrix(X);
  554. end
  555. function Xstack = build_stack_matrix(T, metric_name)
  556. participant_ids = unique(T.participant_id, 'stable');
  557. Xstack = [];
  558. for p = 1:numel(participant_ids)
  559. participant_id = participant_ids(p);
  560. Tp = T(T.participant_id == participant_id, :);
  561. Xp = build_matrix_one_participant(Tp, metric_name);
  562. Xstack = [Xstack; Xp]; %#ok<AGROW>
  563. end
  564. Xstack = clean_icc_matrix(Xstack);
  565. end
  566. function X = clean_icc_matrix(X)
  567. if isempty(X)
  568. return
  569. end
  570. X = X(:, ~all(isnan(X), 1));
  571. X = X(~any(isnan(X), 2), :);
  572. end
  573. function [icc31, icc3k, n, k] = compute_icc31_icc3k(X)
  574. n = size(X, 1);
  575. k = size(X, 2);
  576. if n < 2 || k < 2
  577. icc31 = NaN;
  578. icc3k = NaN;
  579. return
  580. end
  581. grand_mean = mean(X(:), 'omitnan');
  582. mean_per_session = mean(X, 2, 'omitnan');
  583. mean_per_record = mean(X, 1, 'omitnan');
  584. SS_session = k * sum((mean_per_session - grand_mean).^2, 'omitnan');
  585. SS_record = n * sum((mean_per_record - grand_mean).^2, 'omitnan');
  586. SS_total = sum(sum((X - grand_mean).^2, 'omitnan'), 'omitnan');
  587. SS_error = SS_total - SS_session - SS_record;
  588. MS_session = SS_session / (n - 1);
  589. MS_error = SS_error / ((n - 1) * (k - 1));
  590. if ~isfinite(MS_session) || ~isfinite(MS_error) || MS_session == 0
  591. icc31 = NaN;
  592. icc3k = NaN;
  593. return
  594. end
  595. icc31 = (MS_session - MS_error) / (MS_session + (k - 1) * MS_error);
  596. icc3k = (MS_session - MS_error) / MS_session;
  597. end
  598. function value = unique_scalar_string(x)
  599. u = unique(string(x), 'stable');
  600. if isempty(u)
  601. value = "";
  602. else
  603. value = u(1);
  604. end
  605. end
  606. function value = unique_scalar_numeric(x)
  607. u = unique(x, 'stable');
  608. if isempty(u)
  609. value = NaN;
  610. else
  611. value = u(1);
  612. end
  613. end
  614. function files_out = sort_files_by_name(files_in)
  615. files_out = files_in;
  616. if isempty(files_in)
  617. return
  618. end
  619. names = string({files_in.name});
  620. [~, idx] = sort(names);
  621. files_out = files_in(idx);
  622. end

clinical_data_analysis_final.m at commit ffcc1bd, no license · at the source

Overview

Authors: Nahian S Chowdhury1,2, Joshua Rawsthorne2, Negin Hesam-Shariati1,2, Yann Quidé1,2, Lara Alexander1,2, Ekaterina Ermolenko1,2, Angus McIntyre2, Sebastian Restrepo2, Kevin Chen2, Chin-Teng Lin3, Toby Newton-John4, James W Middleton5, Ashley Craig5, Mark P Jensen6, James H McAuley1,7, Sylvia M Gustin1,2
ORCID iDs: Mark P Jensen
  1. Centre for Pain IMPACT, Neuroscience Research Australia, Sydney, NSW Australia
  2. NeuroRecovery Research Hub, School of Psychology, University of New South Wales, Sydney, NSW Australia
  3. School of Computer Science, Faculty of Engineering and Information Technology, University of Technology Sydney, Sydney, NSW Australia
  4. Faculty of Health, University of Technology Sydney, Sydney, NSW Australia
  5. John Walsh Centre for Rehabilitation Research, The Kolling Institute, Faculty of Medicine and Health, University of Sydney, Sydney, NSW Australia
  6. Department of Rehabilitation Medicine, University of Washington, Seattle, WA USA
  7. School of Health Science, University of New South Wales, Sydney, NSW Australia
Institutions: UNSW Sydney (Australia); Neuroscience Research Australia (Australia); University of Technology Sydney (Australia); The University of Sydney (Australia); University of Washington (United States)
Journal: NPJ digital medicine, volume 9, issue 1, article 864
Dates: received 6 February 2026; accepted 7 October 2026; published online 6 November 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41746-026-03424-5 · PMID 42995173 · PMCID PMC13743753 · OpenAlex W7138277128
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: EEG (modality), pain (population)
Methods: Spectral & time-frequency, Statistics, Preprocessing, Connectivity, Physiology & signal measures
Keywords: Engineering, Health care, Medical research, Neurology, Neuroscience
Topic: EEG and Brain-Computer Interfaces (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 83 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repository

Its files are read in the Code ↔ Paper reader above, with 6 matches between paragraphs and lines of code.

DrNahianC/PainWaive-Evaluation-Paper

License: none: the authors keep all their rights
State: the link answers, verified on 9 November 2026
Evidence: files inventoried
Commit: ffcc1bd0ee8bcaf5cae9449bf9a74a5036e70c55, 24 June 2026
Languages: MATLAB (2)
Size: 2 files, 2 scripts
Software Heritage: not archived
Found in: “Code availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: EEGLAB (2 files), FieldTrip (2 files), Statistics and Machine Learning Toolbox (2 files)
Availability: 1 check, the latest on 9 November 2026: the link answers
  • 9 November 2026: the link answers
2 files

Code availability statement

The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41746-026-03424-5.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 2 scripts, each with its path and the digest of its content;
  • 6 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

The paper has a data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

  • it says that the data are available on request

Read it in the paper: doi.org/10.1038/s41746-026-03424-5.

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, 9 November 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 16 authors, 5 keywords, 2 funders, 69 references.

Cite

This paper

Chowdhury, N. S., Rawsthorne, J., Hesam-Shariati, N., Quidé, Y., Alexander, L., Ermolenko, E., McIntyre, A., Restrepo, S., Chen, K., Lin, C.-T., Newton-John, T., Middleton, J. W., Craig, A., Jensen, M. P., McAuley, J. H., & Gustin, S. M. (2026). Evaluation of PainWaive: a consumer-grade EEG headset for remote EEG monitoring and neurofeedback research in chronic pain. NPJ digital medicine, 9(1), 864. https://doi.org/10.1038/s41746-026-03424-5

BibTeX

@article{chowdhury2026evaluation,
author = {Chowdhury, Nahian S and Rawsthorne, Joshua and Hesam-Shariati, Negin and Quidé, Yann and Alexander, Lara and Ermolenko, Ekaterina and McIntyre, Angus and Restrepo, Sebastian and Chen, Kevin and Lin, Chin-Teng and Newton-John, Toby and Middleton, James W and Craig, Ashley and Jensen, Mark P and McAuley, James H and Gustin, Sylvia M},
title = {{Evaluation of PainWaive: a consumer-grade EEG headset for remote EEG monitoring and neurofeedback research in chronic pain}},
journal = {NPJ digital medicine},
year = {2026},
month = nov,
volume = {9},
number = {1},
pages = {864},
publisher = {Nature Publishing Group},
issn = {2398-6352},
doi = {10.1038/s41746-026-03424-5},
url = {https://doi.org/10.1038/s41746-026-03424-5},
pmid = {42995173},
pmcid = {PMC13743753}
}

RIS

TY - JOUR
AU - Chowdhury, Nahian S
AU - Rawsthorne, Joshua
AU - Hesam-Shariati, Negin
AU - Quidé, Yann
AU - Alexander, Lara
AU - Ermolenko, Ekaterina
AU - McIntyre, Angus
AU - Restrepo, Sebastian
AU - Chen, Kevin
AU - Lin, Chin-Teng
AU - Newton-John, Toby
AU - Middleton, James W
AU - Craig, Ashley
AU - Jensen, Mark P
AU - McAuley, James H
AU - Gustin, Sylvia M
TI - Evaluation of PainWaive: a consumer-grade EEG headset for remote EEG monitoring and neurofeedback research in chronic pain
T2 - NPJ digital medicine
J2 - NPJ Digit Med
PY - 2026
DA - 2026/11/06
VL - 9
IS - 1
SP - 864
SN - 2398-6352
PB - Nature Publishing Group
DO - 10.1038/s41746-026-03424-5
UR - https://doi.org/10.1038/s41746-026-03424-5
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41746-026-03424-5",
"type": "article-journal",
"title": "Evaluation of PainWaive: a consumer-grade EEG headset for remote EEG monitoring and neurofeedback research in chronic pain",
"container-title": "NPJ digital medicine",
"author": [
{
"family": "Chowdhury",
"given": "Nahian S"
},
{
"family": "Rawsthorne",
"given": "Joshua"
},
{
"family": "Hesam-Shariati",
"given": "Negin"
},
{
"family": "Quidé",
"given": "Yann"
},
{
"family": "Alexander",
"given": "Lara"
},
{
"family": "Ermolenko",
"given": "Ekaterina"
},
{
"family": "McIntyre",
"given": "Angus"
},
{
"family": "Restrepo",
"given": "Sebastian"
},
{
"family": "Chen",
"given": "Kevin"
},
{
"family": "Lin",
"given": "Chin-Teng"
},
{
"family": "Newton-John",
"given": "Toby"
},
{
"family": "Middleton",
"given": "James W"
},
{
"family": "Craig",
"given": "Ashley"
},
{
"family": "Jensen",
"given": "Mark P"
},
{
"family": "McAuley",
"given": "James H"
},
{
"family": "Gustin",
"given": "Sylvia M"
}
],
"container-title-short": "NPJ Digit Med",
"volume": "9",
"issue": "1",
"page": "864",
"DOI": "10.1038/s41746-026-03424-5",
"PMID": "42995173",
"PMCID": "PMC13743753",
"ISSN": "2398-6352",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41746-026-03424-5",
"language": "en",
"issued": {
"date-parts": [
[
2026,
11,
6
]
]
}
}

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.1016/j.ebiom.2025.105955 [code]
Exploring electroencephalographic chronic pain biomarkers: a mega-analysis.
Journal: EBioMedicine
In common: EEGLAB, FieldTrip, Statistics and Machine Learning Toolbox, pain, EEG, 1 reference, author Mark P Jensen
[2] doi:10.1016/j.neuroimage.2023.120116 [code]
Stability, change, and reliable individual differences in electroencephalography measures: A lifespan perspective on progress and opportunities.
Journal: NeuroImage
In common: EEG, 6 references
[3] doi:10.7717/peerj.20416 [code]
Evaluating portable EEG: a comparison between two wireless systems (EPOC Flex and LiveAmp) and the wired BrainAmp system.
Journal: PeerJ
In common: EEG, 6 references
[4] doi:10.1097/j.pain.0000000000003571 [code]
Assessing the predictive value of peak alpha frequency for the sensitivity to pain.
Journal: Pain
In common: EEGLAB, FieldTrip, Statistics and Machine Learning Toolbox, pain, EEG, 3 references
[5] doi:10.1186/s12859-024-05865-9 [code]
HEROIC: a platform for remote collection of electroencephalographic data using consumer-grade brain wearables.
Journal: BMC bioinformatics
In common: EEG, 5 references
[6] doi:10.1038/s41598-026-52672-8 [code]
Validation of portable, semi-dry electrode-based electroencephalography device for its application in brain-computer interface solutions.
Journal: Scientific reports
In common: EEG, 5 references
[7] doi:10.1007/s10548-026-01210-w [code]
Resting-State Theta and Alpha Oscillations in Amputation and Phantom Limb Pain: A Pre-Registered High-Density EEG Study.
Journal: Brain topography
In common: EEGLAB, FieldTrip, Statistics and Machine Learning Toolbox, pain, EEG, 2 references
[8] doi:10.1371/journal.pone.0279881 [code]
Stimulus-induced narrow-band gamma oscillations in humans can be recorded using open-hardware low-cost EEG amplifier.
Journal: PloS one
In common: EEGLAB, Statistics and Machine Learning Toolbox, EEG, 3 references
[9] doi:10.1371/journal.pone.0291186
A scoping review on the use of consumer-grade EEG devices for research.
Journal: PloS one
In common: EEG, 4 references
[10] doi:10.1002/brb3.70448
The Feasibility and Test-Retest Reliability of Wireless Dry-Electrode EEG During a Dynamic Psychomotor Virtual Reality Task.
Journal: Brain and behavior
In common: EEG, 4 references

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.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1038/s41746-026-03424-5.svg)](https://openscicode.org/badge/doi/10.1038/s41746-026-03424-5)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1038/s41746-026-03424-5.svg
   :target: https://openscicode.org/badge/doi/10.1038/s41746-026-03424-5
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1038/s41746-026-03424-5"><img src="https://openscicode.org/badge/doi/10.1038/s41746-026-03424-5.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.