OSCR

Latent persistence in fMRI dynamics during human memory consolidation.

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 › Preprocessing and ROI extraction ↔ src/preprocessing/preprocess_bids_minimal.m, lines 1–123 · score 0.88 · deformation field, echo preference, multi echo, coregistered, resliced, SPM
  2. [2] § Methods › Preprocessing and ROI extraction ↔ src/preprocessing/preprocess_bids_minimal.m, lines 285–324 · score 0.66 · deformation field, tissue, native, SPM, segmentation, MNI
  3. [3] § Methods › Preprocessing and ROI extraction ↔ src/preprocessing/preprocess_bids_minimal.m, lines 1–123 · score 0.61 · multi echo, minimal, pipeline, SPM12, BIDS, preprocessed

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

MATLAB · 667 lines · 22 KB · MIT · 3 matches

  1. function preprocess_bids_minimal(bids_root, subj_list, keep_tasks, opts)
  2. % preprocess_bids_minimal
  3. %
  4. % ONE-FILE, serial, lightweight SPM12 preprocessing for BIDS fMRI.
  5. %
  6. % Pipeline per selected run:
  7. % 1) Segment T1 (deformation field only; once per sub+ses)
  8. % 2) Realign: Estimate + Reslice mean only (no r*.nii timeseries)
  9. % 3) Coregister mean EPI -> T1 (no "other" list)
  10. % 4) Normalize ORIGINAL 4D BOLD -> MNI (default 3mm), bilinear; writes w*.nii
  11. % 5) Export motion confounds rp_<base>.txt and rp_<base>.tsv into func/confounds/
  12. %
  13. % Robust features:
  14. % - Subject autodetect if subj_list is empty
  15. % - Task autodetect if keep_tasks is empty
  16. % - Handles .nii and .nii.gz (gunzip once)
  17. % - Filters func_dir to ORIGINAL BIDS files only: sub-*_bold.nii(.gz)
  18. % - Optional multi-echo selection via opts.echo_preference (1/2/3)
  19. % - max_runs_per_task (default 1; Inf for all runs)
  20. % - Skip work if output w*.nii already exists (default true)
  21. % - Stamp-based coreg skipping (mean/T1 unchanged)
  22. %
  23. % Inputs:
  24. % bids_root : path to BIDS root (folder containing sub-*)
  25. % subj_list : {} or {'sub-01','sub-02',...}; empty => autodetect from disk
  26. % keep_tasks : {} or {'task-encoding','task-rest'} or {'encoding','rest'}
  27. % opts : struct (optional). Fields:
  28. % opts.spm_dir : REQUIRED. Folder containing spm.m (SPM12).
  29. % opts.max_runs_per_task : default 1. Use Inf for all runs.
  30. % opts.echo_preference : default []. If set to 1/2/3, prefer echo-XX.
  31. % opts.vox : default [3 3 3].
  32. % opts.bb : default [-78 -112 -70; 78 76 85].
  33. % opts.skip_if_w_exists : default true.
  34. % opts.write_tsv_confounds : default true.
  35. % opts.filter_original_bids: default true (startsWith 'sub-').
  36. % opts.verbose : default true.
  37. %
  38. % Example:
  39. % opts = struct('spm_dir','F:\Implementation LKIS\spm', 'max_runs_per_task',1);
  40. % preprocess_bids_minimal('F:\Implementation LKIS\ds003721-download', ...
  41. % {'sub-26','sub-27'}, {'task-BI','task-Film','task-React'}, opts);
  42. %
  43. % Notes:
  44. % - No slice timing, no smoothing (minimal).
  45. % - Normalization uses deformation field from T1 segmentation (y_*.nii).
  46. % - Writes normalized output into the SAME func folder as the input BOLD.
  47. %
  48. % (c) Maria's pipeline consolidation for GitHub: single file, readable + reproducible.
  49. % -------------------------- defaults --------------------------
  50. if nargin < 1 || isempty(bids_root), error('bids_root is required'); end
  51. if nargin < 2, subj_list = {}; end
  52. if nargin < 3, keep_tasks = {}; end
  53. if nargin < 4, opts = struct(); end
  54. opts = fill_defaults(opts, struct( ...
  55. 'spm_dir', '', ...
  56. 'max_runs_per_task', 1, ...
  57. 'echo_preference', [], ...
  58. 'vox', [3 3 3], ...
  59. 'bb', [-78 -112 -70; 78 76 85], ...
  60. 'skip_if_w_exists', true, ...
  61. 'write_tsv_confounds', true, ...
  62. 'filter_original_bids', true, ...
  63. 'verbose', true ));
  64. if isempty(opts.spm_dir)
  65. error('opts.spm_dir is required (folder containing spm.m).');
  66. end
  67. bids_root = resolve_bids_root(char(bids_root));
  68. % -------------------------- init SPM once --------------------------
  69. init_spm_headless(opts.spm_dir);
  70. logmsg(opts, 'Serial minimal preprocessing (one-file).');
  71. logmsg(opts, 'BIDS root: %s', bids_root);
  72. % -------------------------- subject selection --------------------------
  73. subj_on_disk = detect_subjects(bids_root);
  74. if isempty(subj_list)
  75. subj_list = subj_on_disk;
  76. else
  77. subj_list = subj_list(:);
  78. ok = ismember(subj_list, subj_on_disk);
  79. missing = subj_list(~ok);
  80. subj_list = subj_list(ok);
  81. if ~isempty(missing)
  82. warning('Requested subjects not found; skipping: %s', strjoin(missing(:).', ', '));
  83. end
  84. end
  85. if isempty(subj_list)
  86. error('No subjects to process under: %s', bids_root);
  87. end
  88. logmsg(opts, 'Subjects: %d', numel(subj_list));
  89. if ~isempty(opts.echo_preference)
  90. logmsg(opts, 'Echo preference: echo-%02d (fallback enabled).', opts.echo_preference);
  91. end
  92. % Normalize keep_tasks to labels without 'task-' (lowercase)
  93. want_tasks = normalize_keep_tasks(keep_tasks);
  94. % -------------------------- main loop --------------------------
  95. for isub = 1:numel(subj_list)
  96. sub_id = subj_list{isub};
  97. try
  98. run_one_subject(bids_root, sub_id, want_tasks, opts);
  99. catch ME
  100. warning('Subject %s failed: %s', sub_id, ME.message);
  101. end
  102. end
  103. logmsg(opts, 'Done.');
  104. end % ===== end main =====
  105. % =====================================================================
  106. % ======================== PER-SUBJECT LOOP ===========================
  107. % =====================================================================
  108. function run_one_subject(bids_root, sub_id, want_tasks, opts)
  109. logmsg(opts, '\n===== %s =====', sub_id);
  110. sub_dir = fullfile(bids_root, sub_id);
  111. if ~exist(sub_dir,'dir')
  112. warning('Subject folder not found: %s', sub_dir);
  113. return;
  114. end
  115. ses_names = find_sessions(sub_dir); % {''} if none
  116. for ises = 1:numel(ses_names)
  117. ses_name = ses_names{ises};
  118. logmsg(opts, ' Session: %s', ternary(isempty(ses_name),'(none)',ses_name));
  119. % Locate func dir
  120. if isempty(ses_name)
  121. func_dir = fullfile(sub_dir, 'func');
  122. else
  123. func_dir = fullfile(sub_dir, ses_name, 'func');
  124. end
  125. if ~exist(func_dir,'dir')
  126. warning('No func dir for %s %s; skipping.', sub_id, ses_name);
  127. continue;
  128. end
  129. % Locate T1 (supports .nii and .nii.gz; checks ses/anat then sub/anat)
  130. T1 = find_T1w_for_session(sub_dir, ses_name);
  131. T1 = ensure_nii_unzipped(T1, opts);
  132. % Segment T1 deformation (once per session)
  133. def_field = segment_T1_if_needed_minimal(T1, opts);
  134. % List original BOLD files in func
  135. bold = list_bold_files(func_dir, opts);
  136. if isempty(bold)
  137. warning('No usable BOLD files in %s', func_dir);
  138. continue;
  139. end
  140. % Optional echo selection
  141. if ~isempty(opts.echo_preference)
  142. bold = select_preferred_echo(bold, opts.echo_preference);
  143. if isempty(bold)
  144. warning('No usable BOLD files after echo selection in %s', func_dir);
  145. continue;
  146. end
  147. end
  148. bold_names = {bold.name};
  149. file_tasks = cellfun(@(s) parse_task_label_lower(s), bold_names, 'uni', 0);
  150. % If keep_tasks empty => autodetect
  151. tasks_this_session = want_tasks;
  152. if isempty(tasks_this_session)
  153. tasks_this_session = unique(file_tasks(~cellfun('isempty', file_tasks)));
  154. tasks_this_session = tasks_this_session(:).';
  155. if isempty(tasks_this_session)
  156. warning('No task labels found in BOLD filenames in %s', func_dir);
  157. continue;
  158. end
  159. logmsg(opts, ' Auto-detected tasks: %s', strjoin(tasks_this_session, ', '));
  160. end
  161. for t = 1:numel(tasks_this_session)
  162. want = tasks_this_session{t};
  163. idx = find(strcmp(file_tasks, want));
  164. if isempty(idx), continue; end
  165. idx = sort_by_run_number(bold_names, idx);
  166. maxn = opts.max_runs_per_task;
  167. if isinf(maxn), maxn = numel(idx); end
  168. idx = idx(1:min(maxn, numel(idx)));
  169. for ir = 1:numel(idx)
  170. ii = idx(ir);
  171. bold_name = bold_names{ii};
  172. bold_path = fullfile(func_dir, bold_name);
  173. % If gz => unzip to .nii once
  174. bold_nii = ensure_nii_unzipped(bold_path, opts);
  175. bold_base = strip_nii_ext(bold_name);
  176. % Expected normalized output is w<basename>.nii
  177. w_nii = fullfile(func_dir, ['w', bold_base, '.nii']);
  178. if opts.skip_if_w_exists && exist(w_nii, 'file')
  179. logmsg(opts, ' Skip (exists): %s', w_nii);
  180. export_motion_confounds(func_dir, bold_base, opts);
  181. continue;
  182. end
  183. logmsg(opts, ' %s (task=%s) [run %d/%d]', bold_name, want, ir, numel(idx));
  184. % (1) REALIGN: estimate + reslice mean only
  185. mean_guess = fullfile(func_dir, ['mean', strip_gz(bold_name)]); % mean<file>.nii
  186. need_realign = true;
  187. if exist(mean_guess, 'file')
  188. need_realign = is_older(mean_guess, bold_nii);
  189. end
  190. if need_realign
  191. tic;
  192. realign_estimate_and_reslice_mean_only(bold_nii);
  193. logmsg(opts, ' Realign done in %.2f min', toc/60);
  194. else
  195. logmsg(opts, ' Realign: mean up-to-date, skipping.');
  196. end
  197. mean_ni = find_mean_epi(func_dir, bold_name);
  198. % (motion confounds)
  199. export_motion_confounds(func_dir, bold_base, opts);
  200. % (2) COREGISTER mean -> T1 (stamp)
  201. coreg_stamp = fullfile(func_dir, ['coreg_done_', bold_base, '.stamp']);
  202. need_coreg = true;
  203. if exist(coreg_stamp, 'file')
  204. if ~is_older(coreg_stamp, mean_ni) && ~is_older(coreg_stamp, T1)
  205. need_coreg = false;
  206. end
  207. end
  208. if need_coreg
  209. tic;
  210. coreg_meanEPI_to_T1(mean_ni, T1);
  211. touch(coreg_stamp);
  212. logmsg(opts, ' Coreg done in %.2f min', toc/60);
  213. else
  214. logmsg(opts, ' Coreg: up-to-date, skipping.');
  215. end
  216. % (3) NORMALIZE original 4D -> w*.nii (3mm default)
  217. tic;
  218. normalize_to_mni_write(bold_nii, def_field, opts);
  219. logmsg(opts, ' Normalize done in %.2f min', toc/60);
  220. if exist(w_nii, 'file')
  221. logmsg(opts, ' Wrote %s', w_nii);
  222. else
  223. warning('Normalization finished but expected output not found: %s', w_nii);
  224. end
  225. end
  226. end
  227. end
  228. end
  229. % =====================================================================
  230. % ============================= SPM WRAPS =============================
  231. % =====================================================================
  232. function init_spm_headless(spm_dir)
  233. add_spm_path(spm_dir);
  234. spm('Defaults','fMRI');
  235. if exist('spm_get_defaults','file')
  236. spm_get_defaults('cmdline', true);
  237. end
  238. spm_jobman('initcfg');
  239. end
  240. function def_field = segment_T1_if_needed_minimal(T1, opts)
  241. [pth, nam, ext] = fileparts(T1);
  242. def_field = fullfile(pth, ['y_', nam, ext]);
  243. if exist(def_field, 'file')
  244. logmsg(opts, ' Deformation field exists: %s', def_field);
  245. return;
  246. end
  247. logmsg(opts, ' Segmenting T1 (deformation only): %s', T1);
  248. clear matlabbatch;
  249. matlabbatch{1}.spm.spatial.preproc.channel.vols = {T1};
  250. matlabbatch{1}.spm.spatial.preproc.channel.biasreg = 0.001;
  251. matlabbatch{1}.spm.spatial.preproc.channel.biasfwhm = 60;
  252. matlabbatch{1}.spm.spatial.preproc.channel.write = [0 0];
  253. tpm = fullfile(spm('Dir'),'tpm','TPM.nii');
  254. ngaus = [1 1 2 3 4 2];
  255. for tt = 1:6
  256. matlabbatch{1}.spm.spatial.preproc.tissue(tt).tpm = {sprintf('%s,%d', tpm, tt)};
  257. matlabbatch{1}.spm.spatial.preproc.tissue(tt).ngaus = ngaus(tt);
  258. matlabbatch{1}.spm.spatial.preproc.tissue(tt).native = [0 0];
  259. matlabbatch{1}.spm.spatial.preproc.tissue(tt).warped = [0 0];
  260. end
  261. matlabbatch{1}.spm.spatial.preproc.warp.mrf = 1;
  262. matlabbatch{1}.spm.spatial.preproc.warp.cleanup = 1;
  263. matlabbatch{1}.spm.spatial.preproc.warp.reg = [0 0.001 0.5 0.05 0.2];
  264. matlabbatch{1}.spm.spatial.preproc.warp.affreg = 'mni';
  265. matlabbatch{1}.spm.spatial.preproc.warp.fwhm = 0;
  266. matlabbatch{1}.spm.spatial.preproc.warp.samp = 3;
  267. matlabbatch{1}.spm.spatial.preproc.warp.write = [0 1]; % deformation only
  268. spm_jobman('run', matlabbatch);
  269. if ~exist(def_field,'file')
  270. error('Deformation field not found after segmentation: %s', def_field);
  271. end
  272. end
  273. function realign_estimate_and_reslice_mean_only(bold_nii)
  274. V = spm_vol(bold_nii);
  275. nV = numel(V);
  276. scans = arrayfun(@(k) sprintf('%s,%d', bold_nii, k), 1:nV, 'uni', 0).';
  277. clear matlabbatch;
  278. matlabbatch{1}.spm.spatial.realign.estwrite.data = {scans};
  279. % Conservative settings (fast-ish, robust)
  280. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.quality = 0.3;
  281. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.sep = 8;
  282. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.fwhm = 6;
  283. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.rtm = 1;
  284. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.interp = 2;
  285. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.wrap = [0 0 0];
  286. matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.weight = '';
  287. % mean only; no r*.nii series
  288. matlabbatch{1}.spm.spatial.realign.estwrite.roptions.which = [0 1];
  289. matlabbatch{1}.spm.spatial.realign.estwrite.roptions.interp = 2;
  290. matlabbatch{1}.spm.spatial.realign.estwrite.roptions.wrap = [0 0 0];
  291. matlabbatch{1}.spm.spatial.realign.estwrite.roptions.mask = 0;
  292. matlabbatch{1}.spm.spatial.realign.estwrite.roptions.prefix = 'r';
  293. spm_jobman('run', matlabbatch);
  294. end
  295. function coreg_meanEPI_to_T1(mean_ni, T1)
  296. clear matlabbatch;
  297. matlabbatch{1}.spm.spatial.coreg.estimate.ref = {T1};
  298. matlabbatch{1}.spm.spatial.coreg.estimate.source = {mean_ni};
  299. matlabbatch{1}.spm.spatial.coreg.estimate.other = {''};
  300. matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.cost_fun = 'nmi';
  301. matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.sep = [4 2];
  302. matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.tol = ...
  303. [0.02 0.02 0.02 0.001 0.001 0.001 0.01 0.01 0.01 0.001 0.001 0.001];
  304. matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.fwhm = [7 7];
  305. spm_jobman('run', matlabbatch);
  306. end
  307. function normalize_to_mni_write(bold_nii, def_field, opts)
  308. V = spm_vol(bold_nii);
  309. nV = numel(V);
  310. scans = arrayfun(@(k) sprintf('%s,%d', bold_nii, k), 1:nV, 'uni', 0).';
  311. clear matlabbatch;
  312. matlabbatch{1}.spm.spatial.normalise.write.subj.def = {def_field};
  313. matlabbatch{1}.spm.spatial.normalise.write.subj.resample = scans;
  314. matlabbatch{1}.spm.spatial.normalise.write.woptions.bb = opts.bb;
  315. matlabbatch{1}.spm.spatial.normalise.write.woptions.vox = opts.vox;
  316. matlabbatch{1}.spm.spatial.normalise.write.woptions.interp = 2; % bilinear
  317. matlabbatch{1}.spm.spatial.normalise.write.woptions.prefix = 'w';
  318. spm_jobman('run', matlabbatch);
  319. end
  320. % =====================================================================
  321. % ============================= BIDS HELPERS ==========================
  322. % =====================================================================
  323. function bids_root = resolve_bids_root(bids_root)
  324. if ~exist(bids_root,'dir')
  325. error('BIDS root does not exist: %s', bids_root);
  326. end
  327. if ~isempty(dir(fullfile(bids_root,'sub-*')))
  328. return;
  329. end
  330. % try one level down
  331. dd = dir(bids_root); dd = dd([dd.isdir]);
  332. dd = dd(~ismember({dd.name},{'.','..'}));
  333. for i = 1:numel(dd)
  334. cand = fullfile(bids_root, dd(i).name);
  335. if ~isempty(dir(fullfile(cand,'sub-*')))
  336. bids_root = cand;
  337. return;
  338. end
  339. end
  340. error('Could not find sub-* under %s (or one level down).', bids_root);
  341. end
  342. function subj_list = detect_subjects(bids_root)
  343. dd = dir(fullfile(bids_root,'sub-*'));
  344. dd = dd([dd.isdir]);
  345. if isempty(dd), error('No subject folders found under: %s', bids_root); end
  346. names = {dd.name};
  347. nums = nan(size(names));
  348. for i = 1:numel(names)
  349. tok = regexp(names{i}, 'sub-(\d+)', 'tokens', 'once');
  350. if ~isempty(tok), nums(i) = str2double(tok{1}); else, nums(i) = i; end
  351. end
  352. [~,ord] = sort(nums,'ascend');
  353. subj_list = names(ord);
  354. end
  355. function ses_names = find_sessions(sub_dir)
  356. ses = dir(fullfile(sub_dir,'ses-*'));
  357. ses = ses([ses.isdir]);
  358. if isempty(ses)
  359. ses_names = {''};
  360. else
  361. ses_names = {ses.name};
  362. end
  363. end
  364. function T1path = find_T1w_for_session(sub_dir, ses_name)
  365. % Prefer ses/anat, fallback to sub/anat. Accept .nii and .nii.gz
  366. cand_dirs = {};
  367. if ~isempty(ses_name)
  368. cand_dirs{end+1} = fullfile(sub_dir, ses_name, 'anat'); %#ok<AGROW>
  369. end
  370. cand_dirs{end+1} = fullfile(sub_dir, 'anat');
  371. for i = 1:numel(cand_dirs)
  372. ad = cand_dirs{i};
  373. if ~exist(ad,'dir'), continue; end
  374. t1a = dir(fullfile(ad, '*_T1w.nii'));
  375. t1b = dir(fullfile(ad, '*_T1w.nii.gz'));
  376. t1 = [t1a; t1b];
  377. if ~isempty(t1)
  378. T1path = fullfile(ad, t1(1).name);
  379. return;
  380. end
  381. end
  382. error('No T1w found for %s (session %s).', sub_dir, ses_name);
  383. end
  384. function bold = list_bold_files(func_dir, opts)
  385. nii1 = dir(fullfile(func_dir, '*_bold.nii'));
  386. nii2 = dir(fullfile(func_dir, '*_bold.nii.gz'));
  387. bold = [nii1; nii2];
  388. if isempty(bold), return; end
  389. if opts.filter_original_bids
  390. names_all = {bold.name};
  391. is_orig = startsWith(names_all, 'sub-', 'IgnoreCase', true);
  392. bold = bold(is_orig);
  393. end
  394. % Also exclude files that are clearly derived (extra safety)
  395. if ~isempty(bold)
  396. names = lower({bold.name});
  397. bad = startsWith(names,'w') | startsWith(names,'rw') | startsWith(names,'mean') | startsWith(names,'r');
  398. bold = bold(~bad);
  399. end
  400. end
  401. function bold_out = select_preferred_echo(bold_in, echo_preference)
  402. % If no echo- present => keep as is. Otherwise choose ONE echo bank.
  403. names = lower({bold_in.name});
  404. if ~any(contains(names,'echo-'))
  405. bold_out = bold_in;
  406. return;
  407. end
  408. pref = sprintf('echo-%02d', echo_preference);
  409. m = contains(names, pref);
  410. if any(m), bold_out = bold_in(m); return; end
  411. order = {'echo-02','echo-01','echo-03'};
  412. for k = 1:numel(order)
  413. m = contains(names, order{k});
  414. if any(m), bold_out = bold_in(m); return; end
  415. end
  416. % fallback: keep any echo-*
  417. bold_out = bold_in(contains(names,'echo-'));
  418. end
  419. function want_tasks = normalize_keep_tasks(keep_tasks)
  420. want_tasks = {};
  421. if isempty(keep_tasks), return; end
  422. kt = lower(string(keep_tasks));
  423. want_tasks = cell(1, numel(kt));
  424. for i = 1:numel(kt)
  425. s = char(kt(i));
  426. s = strrep(s,'task-','');
  427. want_tasks{i} = s;
  428. end
  429. end
  430. function lab = parse_task_label_lower(fname)
  431. tok = regexp(fname, 'task-([A-Za-z0-9]+)', 'tokens', 'once');
  432. if isempty(tok), lab = ''; else, lab = lower(tok{1}); end
  433. end
  434. function idx_sorted = sort_by_run_number(bold_names, idx_in)
  435. runs = nan(size(idx_in));
  436. for k = 1:numel(idx_in)
  437. nm = bold_names{idx_in(k)};
  438. tok = regexp(nm, 'run-(\d+)', 'tokens', 'once');
  439. if ~isempty(tok), runs(k) = str2double(tok{1}); else, runs(k) = k; end
  440. end
  441. [~, ord] = sort(runs, 'ascend');
  442. idx_sorted = idx_in(ord);
  443. end
  444. % =====================================================================
  445. % ============================= IO HELPERS ============================
  446. % =====================================================================
  447. function nii_path = ensure_nii_unzipped(path_in, opts)
  448. if endsWith(path_in, '.nii.gz', 'IgnoreCase', true)
  449. nii_path = regexprep(path_in, '\.gz$', '', 'ignorecase');
  450. if ~exist(nii_path,'file') || is_older(nii_path, path_in)
  451. logmsg(opts, ' gunzip: %s', path_in);
  452. gunzip(path_in, fileparts(path_in));
  453. end
  454. else
  455. nii_path = path_in;
  456. end
  457. end
  458. function base = strip_nii_ext(name)
  459. % Remove .nii or .nii.gz from a filename (no path)
  460. if endsWith(name, '.nii.gz', 'IgnoreCase', true)
  461. base = extractBefore(name, strlength(name) - 7);
  462. elseif endsWith(name, '.nii', 'IgnoreCase', true)
  463. base = extractBefore(name, strlength(name) - 4);
  464. else
  465. [~, base] = fileparts(name);
  466. end
  467. end
  468. function out = strip_gz(name)
  469. out = regexprep(name, '\.gz$', '', 'ignorecase');
  470. end
  471. function mean_ni = find_mean_epi(func_dir, bold_name)
  472. % Find actual mean EPI written by SPM after realign.
  473. name_no_gz = strip_gz(bold_name);
  474. cand = fullfile(func_dir, ['mean', name_no_gz]);
  475. if exist(cand, 'file'), mean_ni = cand; return; end
  476. base = regexprep(name_no_gz, '\.nii$', '', 'ignorecase');
  477. dd = dir(fullfile(func_dir, ['mean', base, '*.nii']));
  478. if ~isempty(dd), mean_ni = fullfile(func_dir, dd(1).name); return; end
  479. taskTok = regexp(bold_name,'task-[A-Za-z0-9]+','match','once');
  480. if ~isempty(taskTok)
  481. dd = dir(fullfile(func_dir, ['mean*', taskTok, '*.nii']));
  482. if ~isempty(dd), mean_ni = fullfile(func_dir, dd(1).name); return; end
  483. end
  484. error('Mean EPI not found after realign in %s (for %s).', func_dir, bold_name);
  485. end
  486. function export_motion_confounds(func_dir, bold_base, opts)
  487. % Copy rp_*.txt and optionally write rp_*.tsv with header to func/confounds/
  488. src = fullfile(func_dir, ['rp_', bold_base, '.txt']);
  489. if ~exist(src,'file')
  490. % If realign hasn't produced it (rare), just skip silently.
  491. return;
  492. end
  493. conf_dir = fullfile(func_dir, 'confounds');
  494. if ~exist(conf_dir,'dir')
  495. try mkdir(conf_dir); catch, return; end
  496. end
  497. dst_txt = fullfile(conf_dir, ['rp_', bold_base, '.txt']);
  498. try
  499. if ~exist(dst_txt,'file') || is_older(dst_txt, src)
  500. copyfile(src, dst_txt);
  501. end
  502. catch
  503. end
  504. if ~opts.write_tsv_confounds
  505. return;
  506. end
  507. dst_tsv = fullfile(conf_dir, ['rp_', bold_base, '.tsv']);
  508. try
  509. need_write = ~exist(dst_tsv,'file') || is_older(dst_tsv, src);
  510. if need_write
  511. M = readmatrix(src);
  512. if size(M,2) >= 6
  513. fid = fopen(dst_tsv,'w'); if fid==-1, return; end
  514. fprintf(fid, 'trans_x\ttrans_y\ttrans_z\trot_x\trot_y\trot_z\n');
  515. fmt = '%g\t%g\t%g\t%g\t%g\t%g\n';
  516. for i = 1:size(M,1)
  517. fprintf(fid, fmt, M(i,1), M(i,2), M(i,3), M(i,4), M(i,5), M(i,6));
  518. end
  519. fclose(fid);
  520. end
  521. end
  522. catch
  523. end
  524. end
  525. function tf = is_older(target, reference)
  526. if ~exist(target,'file'), tf = true; return; end
  527. dt_target = dir(target); dt_ref = dir(reference);
  528. tf = dt_target.datenum < dt_ref.datenum;
  529. end
  530. function touch(fname)
  531. fid = fopen(fname,'w');
  532. if fid>0, fclose(fid); end
  533. end
  534. % =====================================================================
  535. % ============================ UTIL HELPERS ===========================
  536. % =====================================================================
  537. function add_spm_path(spm_path)
  538. % Accept either a folder containing spm.m OR a direct path to spm.m
  539. if exist(spm_path,'file') == 2
  540. [spm_folder,~,~] = fileparts(spm_path);
  541. else
  542. spm_folder = spm_path;
  543. end
  544. if exist(spm_folder,'dir') ~= 7
  545. error('SPM folder not found: %s', spm_folder);
  546. end
  547. if exist(fullfile(spm_folder,'spm.m'),'file') ~= 2
  548. error('spm.m not found in: %s', spm_folder);
  549. end
  550. addpath(spm_folder);
  551. end
  552. function s = ternary(cond, a, b)
  553. if cond, s = a; else, s = b; end
  554. end
  555. function out = fill_defaults(in, defs)
  556. out = in;
  557. f = fieldnames(defs);
  558. for i = 1:numel(f)
  559. if ~isfield(out, f{i}) || isempty(out.(f{i}))
  560. out.(f{i}) = defs.(f{i});
  561. end
  562. end
  563. end
  564. function logmsg(opts, varargin)
  565. if isfield(opts,'verbose') && ~opts.verbose
  566. return;
  567. end
  568. fprintf([varargin{1}, '\n'], varargin{2:end});
  569. end

preprocess_bids_minimal.m at commit d3fb84b, under MIT · at the source

Overview

  1. Delft Center for Systems and Control, Delft University of Technology, Mekelweg 2, Delft, 2628 CD, the Netherlands
Institutions: Delft University of Technology (Netherlands)
Journal: Neuroimage. Reports, volume 6, issue 3, article 100398
Dates: received 16 April 2026; accepted 12 August 2026; published online 17 August 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.ynirp.2026.100398 · PMID 42656631 · PMCID PMC13507544 · OpenAlex W7203649266
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: fMRI (modality), human (organism), cognitive (subfield)
Methods: Connectivity, Smoothing, state filtering, decompositions, Preprocessing, fMRI & imaging
Keywords: Memory consolidation, Koopman operator, fMRI, Dynamic mode decomposition, Systems neuroscience
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 46 references in the paper

Abstract

Human memory consolidation involves the gradual stabilization and reorganization of memory traces over time. Despite numerous empirical and computational accounts emphasizing different aspects of this process, an integrated framework for evaluating consolidation theories against brain data remains limited. We propose a biologically informed, data-driven framework based on Koopman operator analysis to examine latent dynamical structure in fMRI signals associated with memory consolidation. The Koopman framework lifts nonlinear brain dynamics into a linear function space, enabling spectral characterization of persistence and stability. In practice, we employ Dynamic Mode Decomposition (DMD) together with an observability-aware extension tailored to consolidation-related neural dynamics. We organize existing theories into three functional clusters: standard consolidation, episodic replay during rest, and distributed long-term storage, and then align open-access fMRI datasets with each cluster to assess their dynamical plausibility. Across datasets, delayed or repeated retrieval conditions generally tend to show greater spectral persistence than early encoding-related conditions. Among the three analyses, Cluster 3 yielded the clearest statistically reliable subject-level contrast, with semantically abstracted autobiographical content exhibiting higher mean eigenvalue magnitude and more near-unit modes than event-specific episodic content. This finding is compatible with transformation-oriented and distributed-storage accounts but does not constitute a direct temporal test of consolidation. Replay-related conditions show strong spectral differentiation across task states, although part of this separation likely reflects task structure in addition to consolidation-related dynamics. For the standard consolidation cluster, effects are directionally consistent with theory but remain small and not statistically significant at the subject level. Overall, the proposed framework provides an interpretable operator-theoretic approach for linking memory consolidation theory to latent brain dynamics and for comparing competing accounts in a common spectral language.

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

Repository

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

mbartzioka/koopman-for-memory-consolidation

License: MIT
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d3fb84b1cf4fce0411a869fa96e6e4c32a5e625b, 6 March 2026
Languages: Python (14), MATLAB (4)
Size: 22 files, 18 scripts
Software Heritage: not archived
Found in: “Data and code availability statement”
Holds: README, license file, environment (requirements.txt)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (14 files), Nilearn (3 files), PyTorch (3 files), NiBabel (2 files), SPM (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
20 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:

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

Datasets cited

Data availability

The data used were open source.

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

Data Availability Statement

All neuroimaging datasets analyzed in this study were obtained from publicly available repositories adhering to the Brain Imaging Data Structure (BIDS) standard and were downloaded from OpenNeuro (https://openneuro.org). Datasets were selected based on their alignment with the consolidation clusters described above, together with task metadata completeness, preprocessing compatibility, and sufficient sample size. Raw imaging data and metadata were retrieved in BIDS format to support standardized preprocessing and reproducibility.

Preprocessing was performed using SPM12 (Wellcome Centre for Human Neuroimaging, London, UK), followed by time-series extraction and Koopman spectral analysis using custom MATLAB and Python scripts. All analysis scripts are publicly available at https://github.com/mbartzioka/koopman-for-memory-consolidation, including preprocessing pipelines, ROI extraction procedures, Koopman operator estimation routines, and documentation needed to reproduce the reported spectral metrics and figures.

No new data were collected for this study, and all analyses were conducted on de-identified, publicly available datasets in accordance with the original ethical approvals obtained by the respective study authors.

The data used were open source.

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

  • Authors: added Maria Bartzioka (0009-0005-3728-5841); Mohammad Khosravi (0000-0002-4873-1115); removed Maria Bartzioka; Mohammad Khosravi
  • Funding: added Technische Universiteit Delft

Version 1, 27 September 2026: the first record

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

Cite

This paper

Bartzioka, M., & Khosravi, M. (2026). Latent persistence in fMRI dynamics during human memory consolidation. Neuroimage. Reports, 6(3), 100398. https://doi.org/10.1016/j.ynirp.2026.100398

BibTeX

@article{bartzioka2026latent,
author = {Bartzioka, Maria and Khosravi, Mohammad},
title = {{Latent persistence in fMRI dynamics during human memory consolidation}},
journal = {Neuroimage. Reports},
year = {2026},
month = aug,
volume = {6},
number = {3},
pages = {100398},
publisher = {Elsevier},
issn = {2666-9560},
doi = {10.1016/j.ynirp.2026.100398},
url = {https://doi.org/10.1016/j.ynirp.2026.100398},
pmid = {42656631},
pmcid = {PMC13507544}
}

RIS

TY - JOUR
AU - Bartzioka, Maria
AU - Khosravi, Mohammad
TI - Latent persistence in fMRI dynamics during human memory consolidation
T2 - Neuroimage. Reports
J2 - Neuroimage Rep
PY - 2026
DA - 2026/08/17
VL - 6
IS - 3
SP - 100398
SN - 2666-9560
PB - Elsevier
DO - 10.1016/j.ynirp.2026.100398
UR - https://doi.org/10.1016/j.ynirp.2026.100398
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.ynirp.2026.100398",
"type": "article-journal",
"title": "Latent persistence in fMRI dynamics during human memory consolidation",
"container-title": "Neuroimage. Reports",
"author": [
{
"family": "Bartzioka",
"given": "Maria"
},
{
"family": "Khosravi",
"given": "Mohammad"
}
],
"container-title-short": "Neuroimage Rep",
"volume": "6",
"issue": "3",
"page": "100398",
"DOI": "10.1016/j.ynirp.2026.100398",
"PMID": "42656631",
"PMCID": "PMC13507544",
"ISSN": "2666-9560",
"publisher": "Elsevier",
"URL": "https://doi.org/10.1016/j.ynirp.2026.100398",
"language": "en",
"issued": {
"date-parts": [
[
2026,
8,
17
]
]
}
}

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.isci.2026.116903 [code]
Neurobiological and behavioral relevance of intrinsic functional connectome constraints on task-evoked neural activation.
Journal: iScience
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI, cognitive
[2] doi:10.21203/rs.3.rs-9326213/v1 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: Research Square (preprint)
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI, cognitive
[3] doi:10.7554/elife.107933 [code]
Modality-agnostic decoding of vision and language from fMRI.
Journal: eLife
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI, cognitive
[4] doi:10.64898/2026.03.09.710558 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: bioRxiv (preprint)
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI, cognitive
[5] doi:10.1162/imag.a.1266 [code]
Multimodal subspace independent vector analysis effectively captures latent relationships between brain structure and function.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, SPM, NiBabel, 1 other tool, fMRI, 1 reference
[6] doi:10.1162/imag.a.1321 [code]
Phase similarity between similar objects indicates representational merging across retrieval training but not sleep.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Nilearn, NiBabel, NumPy, cognitive, 2 references
[7] doi:10.1016/j.neuroimage.2026.122171 [code]
A conserved node degree-based backbone and flexible hub organization of brain connectome during naturalistic movie watching.
Journal: NeuroImage
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI
[8] doi:10.1002/epi.70323 [code]
Individual-specific resting-state networks predict language dominance in drug-resistant epilepsy.
Journal: Epilepsia
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI
[9] doi:10.1038/s41467-026-73668-y [code]
Convergent and divergent brain-cognition development in early adolescence.
Journal: Nature communications
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI
[10] doi:10.1016/j.neuron.2026.04.011 [code]
Precision fMRI reveals densely interdigitated network patches with conserved motifs in the lateral prefrontal cortex.
Journal: Neuron
In common: Nilearn, SPM, NiBabel, 2 other tools, fMRI

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.