Latent persistence in fMRI dynamics during human memory consolidation.
The 3 matches
- [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] § 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] § 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
- function preprocess_bids_minimal(bids_root, subj_list, keep_tasks, opts)
- % preprocess_bids_minimal
- %
- % ONE-FILE, serial, lightweight SPM12 preprocessing for BIDS fMRI.
- %
- % Pipeline per selected run:
- % 1) Segment T1 (deformation field only; once per sub+ses)
- % 2) Realign: Estimate + Reslice mean only (no r*.nii timeseries)
- % 3) Coregister mean EPI -> T1 (no "other" list)
- % 4) Normalize ORIGINAL 4D BOLD -> MNI (default 3mm), bilinear; writes w*.nii
- % 5) Export motion confounds rp_<base>.txt and rp_<base>.tsv into func/confounds/
- %
- % Robust features:
- % - Subject autodetect if subj_list is empty
- % - Task autodetect if keep_tasks is empty
- % - Handles .nii and .nii.gz (gunzip once)
- % - Filters func_dir to ORIGINAL BIDS files only: sub-*_bold.nii(.gz)
- % - Optional multi-echo selection via opts.echo_preference (1/2/3)
- % - max_runs_per_task (default 1; Inf for all runs)
- % - Skip work if output w*.nii already exists (default true)
- % - Stamp-based coreg skipping (mean/T1 unchanged)
- %
- % Inputs:
- % bids_root : path to BIDS root (folder containing sub-*)
- % subj_list : {} or {'sub-01','sub-02',...}; empty => autodetect from disk
- % keep_tasks : {} or {'task-encoding','task-rest'} or {'encoding','rest'}
- % opts : struct (optional). Fields:
- % opts.spm_dir : REQUIRED. Folder containing spm.m (SPM12).
- % opts.max_runs_per_task : default 1. Use Inf for all runs.
- % opts.echo_preference : default []. If set to 1/2/3, prefer echo-XX.
- % opts.vox : default [3 3 3].
- % opts.bb : default [-78 -112 -70; 78 76 85].
- % opts.skip_if_w_exists : default true.
- % opts.write_tsv_confounds : default true.
- % opts.filter_original_bids: default true (startsWith 'sub-').
- % opts.verbose : default true.
- %
- % Example:
- % opts = struct('spm_dir','F:\Implementation LKIS\spm', 'max_runs_per_task',1);
- % preprocess_bids_minimal('F:\Implementation LKIS\ds003721-download', ...
- % {'sub-26','sub-27'}, {'task-BI','task-Film','task-React'}, opts);
- %
- % Notes:
- % - No slice timing, no smoothing (minimal).
- % - Normalization uses deformation field from T1 segmentation (y_*.nii).
- % - Writes normalized output into the SAME func folder as the input BOLD.
- %
- % (c) Maria's pipeline consolidation for GitHub: single file, readable + reproducible.
- % -------------------------- defaults --------------------------
- if nargin < 1 || isempty(bids_root), error('bids_root is required'); end
- if nargin < 2, subj_list = {}; end
- if nargin < 3, keep_tasks = {}; end
- if nargin < 4, opts = struct(); end
- opts = fill_defaults(opts, struct( ...
- 'spm_dir', '', ...
- 'max_runs_per_task', 1, ...
- 'echo_preference', [], ...
- 'vox', [3 3 3], ...
- 'bb', [-78 -112 -70; 78 76 85], ...
- 'skip_if_w_exists', true, ...
- 'write_tsv_confounds', true, ...
- 'filter_original_bids', true, ...
- 'verbose', true ));
- if isempty(opts.spm_dir)
- error('opts.spm_dir is required (folder containing spm.m).');
- end
- bids_root = resolve_bids_root(char(bids_root));
- % -------------------------- init SPM once --------------------------
- init_spm_headless(opts.spm_dir);
- logmsg(opts, 'Serial minimal preprocessing (one-file).');
- logmsg(opts, 'BIDS root: %s', bids_root);
- % -------------------------- subject selection --------------------------
- subj_on_disk = detect_subjects(bids_root);
- if isempty(subj_list)
- subj_list = subj_on_disk;
- else
- subj_list = subj_list(:);
- ok = ismember(subj_list, subj_on_disk);
- missing = subj_list(~ok);
- subj_list = subj_list(ok);
- if ~isempty(missing)
- warning('Requested subjects not found; skipping: %s', strjoin(missing(:).', ', '));
- end
- end
- if isempty(subj_list)
- error('No subjects to process under: %s', bids_root);
- end
- logmsg(opts, 'Subjects: %d', numel(subj_list));
- if ~isempty(opts.echo_preference)
- logmsg(opts, 'Echo preference: echo-%02d (fallback enabled).', opts.echo_preference);
- end
- % Normalize keep_tasks to labels without 'task-' (lowercase)
- want_tasks = normalize_keep_tasks(keep_tasks);
- % -------------------------- main loop --------------------------
- for isub = 1:numel(subj_list)
- sub_id = subj_list{isub};
- try
- run_one_subject(bids_root, sub_id, want_tasks, opts);
- catch ME
- warning('Subject %s failed: %s', sub_id, ME.message);
- end
- end
- logmsg(opts, 'Done.');
- end % ===== end main =====
- % =====================================================================
- % ======================== PER-SUBJECT LOOP ===========================
- % =====================================================================
- function run_one_subject(bids_root, sub_id, want_tasks, opts)
- logmsg(opts, '\n===== %s =====', sub_id);
- sub_dir = fullfile(bids_root, sub_id);
- if ~exist(sub_dir,'dir')
- warning('Subject folder not found: %s', sub_dir);
- return;
- end
- ses_names = find_sessions(sub_dir); % {''} if none
- for ises = 1:numel(ses_names)
- ses_name = ses_names{ises};
- logmsg(opts, ' Session: %s', ternary(isempty(ses_name),'(none)',ses_name));
- % Locate func dir
- if isempty(ses_name)
- func_dir = fullfile(sub_dir, 'func');
- else
- func_dir = fullfile(sub_dir, ses_name, 'func');
- end
- if ~exist(func_dir,'dir')
- warning('No func dir for %s %s; skipping.', sub_id, ses_name);
- continue;
- end
- % Locate T1 (supports .nii and .nii.gz; checks ses/anat then sub/anat)
- T1 = find_T1w_for_session(sub_dir, ses_name);
- T1 = ensure_nii_unzipped(T1, opts);
- % Segment T1 deformation (once per session)
- def_field = segment_T1_if_needed_minimal(T1, opts);
- % List original BOLD files in func
- bold = list_bold_files(func_dir, opts);
- if isempty(bold)
- warning('No usable BOLD files in %s', func_dir);
- continue;
- end
- % Optional echo selection
- if ~isempty(opts.echo_preference)
- bold = select_preferred_echo(bold, opts.echo_preference);
- if isempty(bold)
- warning('No usable BOLD files after echo selection in %s', func_dir);
- continue;
- end
- end
- bold_names = {bold.name};
- file_tasks = cellfun(@(s) parse_task_label_lower(s), bold_names, 'uni', 0);
- % If keep_tasks empty => autodetect
- tasks_this_session = want_tasks;
- if isempty(tasks_this_session)
- tasks_this_session = unique(file_tasks(~cellfun('isempty', file_tasks)));
- tasks_this_session = tasks_this_session(:).';
- if isempty(tasks_this_session)
- warning('No task labels found in BOLD filenames in %s', func_dir);
- continue;
- end
- logmsg(opts, ' Auto-detected tasks: %s', strjoin(tasks_this_session, ', '));
- end
- for t = 1:numel(tasks_this_session)
- want = tasks_this_session{t};
- idx = find(strcmp(file_tasks, want));
- if isempty(idx), continue; end
- idx = sort_by_run_number(bold_names, idx);
- maxn = opts.max_runs_per_task;
- if isinf(maxn), maxn = numel(idx); end
- idx = idx(1:min(maxn, numel(idx)));
- for ir = 1:numel(idx)
- ii = idx(ir);
- bold_name = bold_names{ii};
- bold_path = fullfile(func_dir, bold_name);
- % If gz => unzip to .nii once
- bold_nii = ensure_nii_unzipped(bold_path, opts);
- bold_base = strip_nii_ext(bold_name);
- % Expected normalized output is w<basename>.nii
- w_nii = fullfile(func_dir, ['w', bold_base, '.nii']);
- if opts.skip_if_w_exists && exist(w_nii, 'file')
- logmsg(opts, ' Skip (exists): %s', w_nii);
- export_motion_confounds(func_dir, bold_base, opts);
- continue;
- end
- logmsg(opts, ' %s (task=%s) [run %d/%d]', bold_name, want, ir, numel(idx));
- % (1) REALIGN: estimate + reslice mean only
- mean_guess = fullfile(func_dir, ['mean', strip_gz(bold_name)]); % mean<file>.nii
- need_realign = true;
- if exist(mean_guess, 'file')
- need_realign = is_older(mean_guess, bold_nii);
- end
- if need_realign
- tic;
- realign_estimate_and_reslice_mean_only(bold_nii);
- logmsg(opts, ' Realign done in %.2f min', toc/60);
- else
- logmsg(opts, ' Realign: mean up-to-date, skipping.');
- end
- mean_ni = find_mean_epi(func_dir, bold_name);
- % (motion confounds)
- export_motion_confounds(func_dir, bold_base, opts);
- % (2) COREGISTER mean -> T1 (stamp)
- coreg_stamp = fullfile(func_dir, ['coreg_done_', bold_base, '.stamp']);
- need_coreg = true;
- if exist(coreg_stamp, 'file')
- if ~is_older(coreg_stamp, mean_ni) && ~is_older(coreg_stamp, T1)
- need_coreg = false;
- end
- end
- if need_coreg
- tic;
- coreg_meanEPI_to_T1(mean_ni, T1);
- touch(coreg_stamp);
- logmsg(opts, ' Coreg done in %.2f min', toc/60);
- else
- logmsg(opts, ' Coreg: up-to-date, skipping.');
- end
- % (3) NORMALIZE original 4D -> w*.nii (3mm default)
- tic;
- normalize_to_mni_write(bold_nii, def_field, opts);
- logmsg(opts, ' Normalize done in %.2f min', toc/60);
- if exist(w_nii, 'file')
- logmsg(opts, ' Wrote %s', w_nii);
- else
- warning('Normalization finished but expected output not found: %s', w_nii);
- end
- end
- end
- end
- end
- % =====================================================================
- % ============================= SPM WRAPS =============================
- % =====================================================================
- function init_spm_headless(spm_dir)
- add_spm_path(spm_dir);
- spm('Defaults','fMRI');
- if exist('spm_get_defaults','file')
- spm_get_defaults('cmdline', true);
- end
- spm_jobman('initcfg');
- end
- function def_field = segment_T1_if_needed_minimal(T1, opts)
- [pth, nam, ext] = fileparts(T1);
- def_field = fullfile(pth, ['y_', nam, ext]);
- if exist(def_field, 'file')
- logmsg(opts, ' Deformation field exists: %s', def_field);
- return;
- end
- logmsg(opts, ' Segmenting T1 (deformation only): %s', T1);
- clear matlabbatch;
- matlabbatch{1}.spm.spatial.preproc.channel.vols = {T1};
- matlabbatch{1}.spm.spatial.preproc.channel.biasreg = 0.001;
- matlabbatch{1}.spm.spatial.preproc.channel.biasfwhm = 60;
- matlabbatch{1}.spm.spatial.preproc.channel.write = [0 0];
- tpm = fullfile(spm('Dir'),'tpm','TPM.nii');
- ngaus = [1 1 2 3 4 2];
- for tt = 1:6
- matlabbatch{1}.spm.spatial.preproc.tissue(tt).tpm = {sprintf('%s,%d', tpm, tt)};
- matlabbatch{1}.spm.spatial.preproc.tissue(tt).ngaus = ngaus(tt);
- matlabbatch{1}.spm.spatial.preproc.tissue(tt).native = [0 0];
- matlabbatch{1}.spm.spatial.preproc.tissue(tt).warped = [0 0];
- end
- matlabbatch{1}.spm.spatial.preproc.warp.mrf = 1;
- matlabbatch{1}.spm.spatial.preproc.warp.cleanup = 1;
- matlabbatch{1}.spm.spatial.preproc.warp.reg = [0 0.001 0.5 0.05 0.2];
- matlabbatch{1}.spm.spatial.preproc.warp.affreg = 'mni';
- matlabbatch{1}.spm.spatial.preproc.warp.fwhm = 0;
- matlabbatch{1}.spm.spatial.preproc.warp.samp = 3;
- matlabbatch{1}.spm.spatial.preproc.warp.write = [0 1]; % deformation only
- spm_jobman('run', matlabbatch);
- if ~exist(def_field,'file')
- error('Deformation field not found after segmentation: %s', def_field);
- end
- end
- function realign_estimate_and_reslice_mean_only(bold_nii)
- V = spm_vol(bold_nii);
- nV = numel(V);
- scans = arrayfun(@(k) sprintf('%s,%d', bold_nii, k), 1:nV, 'uni', 0).';
- clear matlabbatch;
- matlabbatch{1}.spm.spatial.realign.estwrite.data = {scans};
- % Conservative settings (fast-ish, robust)
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.quality = 0.3;
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.sep = 8;
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.fwhm = 6;
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.rtm = 1;
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.interp = 2;
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.wrap = [0 0 0];
- matlabbatch{1}.spm.spatial.realign.estwrite.eoptions.weight = '';
- % mean only; no r*.nii series
- matlabbatch{1}.spm.spatial.realign.estwrite.roptions.which = [0 1];
- matlabbatch{1}.spm.spatial.realign.estwrite.roptions.interp = 2;
- matlabbatch{1}.spm.spatial.realign.estwrite.roptions.wrap = [0 0 0];
- matlabbatch{1}.spm.spatial.realign.estwrite.roptions.mask = 0;
- matlabbatch{1}.spm.spatial.realign.estwrite.roptions.prefix = 'r';
- spm_jobman('run', matlabbatch);
- end
- function coreg_meanEPI_to_T1(mean_ni, T1)
- clear matlabbatch;
- matlabbatch{1}.spm.spatial.coreg.estimate.ref = {T1};
- matlabbatch{1}.spm.spatial.coreg.estimate.source = {mean_ni};
- matlabbatch{1}.spm.spatial.coreg.estimate.other = {''};
- matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.cost_fun = 'nmi';
- matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.sep = [4 2];
- matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.tol = ...
- [0.02 0.02 0.02 0.001 0.001 0.001 0.01 0.01 0.01 0.001 0.001 0.001];
- matlabbatch{1}.spm.spatial.coreg.estimate.eoptions.fwhm = [7 7];
- spm_jobman('run', matlabbatch);
- end
- function normalize_to_mni_write(bold_nii, def_field, opts)
- V = spm_vol(bold_nii);
- nV = numel(V);
- scans = arrayfun(@(k) sprintf('%s,%d', bold_nii, k), 1:nV, 'uni', 0).';
- clear matlabbatch;
- matlabbatch{1}.spm.spatial.normalise.write.subj.def = {def_field};
- matlabbatch{1}.spm.spatial.normalise.write.subj.resample = scans;
- matlabbatch{1}.spm.spatial.normalise.write.woptions.bb = opts.bb;
- matlabbatch{1}.spm.spatial.normalise.write.woptions.vox = opts.vox;
- matlabbatch{1}.spm.spatial.normalise.write.woptions.interp = 2; % bilinear
- matlabbatch{1}.spm.spatial.normalise.write.woptions.prefix = 'w';
- spm_jobman('run', matlabbatch);
- end
- % =====================================================================
- % ============================= BIDS HELPERS ==========================
- % =====================================================================
- function bids_root = resolve_bids_root(bids_root)
- if ~exist(bids_root,'dir')
- error('BIDS root does not exist: %s', bids_root);
- end
- if ~isempty(dir(fullfile(bids_root,'sub-*')))
- return;
- end
- % try one level down
- dd = dir(bids_root); dd = dd([dd.isdir]);
- dd = dd(~ismember({dd.name},{'.','..'}));
- for i = 1:numel(dd)
- cand = fullfile(bids_root, dd(i).name);
- if ~isempty(dir(fullfile(cand,'sub-*')))
- bids_root = cand;
- return;
- end
- end
- error('Could not find sub-* under %s (or one level down).', bids_root);
- end
- function subj_list = detect_subjects(bids_root)
- dd = dir(fullfile(bids_root,'sub-*'));
- dd = dd([dd.isdir]);
- if isempty(dd), error('No subject folders found under: %s', bids_root); end
- names = {dd.name};
- nums = nan(size(names));
- for i = 1:numel(names)
- tok = regexp(names{i}, 'sub-(\d+)', 'tokens', 'once');
- if ~isempty(tok), nums(i) = str2double(tok{1}); else, nums(i) = i; end
- end
- [~,ord] = sort(nums,'ascend');
- subj_list = names(ord);
- end
- function ses_names = find_sessions(sub_dir)
- ses = dir(fullfile(sub_dir,'ses-*'));
- ses = ses([ses.isdir]);
- if isempty(ses)
- ses_names = {''};
- else
- ses_names = {ses.name};
- end
- end
- function T1path = find_T1w_for_session(sub_dir, ses_name)
- % Prefer ses/anat, fallback to sub/anat. Accept .nii and .nii.gz
- cand_dirs = {};
- if ~isempty(ses_name)
- cand_dirs{end+1} = fullfile(sub_dir, ses_name, 'anat'); %#ok<AGROW>
- end
- cand_dirs{end+1} = fullfile(sub_dir, 'anat');
- for i = 1:numel(cand_dirs)
- ad = cand_dirs{i};
- if ~exist(ad,'dir'), continue; end
- t1a = dir(fullfile(ad, '*_T1w.nii'));
- t1b = dir(fullfile(ad, '*_T1w.nii.gz'));
- t1 = [t1a; t1b];
- if ~isempty(t1)
- T1path = fullfile(ad, t1(1).name);
- return;
- end
- end
- error('No T1w found for %s (session %s).', sub_dir, ses_name);
- end
- function bold = list_bold_files(func_dir, opts)
- nii1 = dir(fullfile(func_dir, '*_bold.nii'));
- nii2 = dir(fullfile(func_dir, '*_bold.nii.gz'));
- bold = [nii1; nii2];
- if isempty(bold), return; end
- if opts.filter_original_bids
- names_all = {bold.name};
- is_orig = startsWith(names_all, 'sub-', 'IgnoreCase', true);
- bold = bold(is_orig);
- end
- % Also exclude files that are clearly derived (extra safety)
- if ~isempty(bold)
- names = lower({bold.name});
- bad = startsWith(names,'w') | startsWith(names,'rw') | startsWith(names,'mean') | startsWith(names,'r');
- bold = bold(~bad);
- end
- end
- function bold_out = select_preferred_echo(bold_in, echo_preference)
- % If no echo- present => keep as is. Otherwise choose ONE echo bank.
- names = lower({bold_in.name});
- if ~any(contains(names,'echo-'))
- bold_out = bold_in;
- return;
- end
- pref = sprintf('echo-%02d', echo_preference);
- m = contains(names, pref);
- if any(m), bold_out = bold_in(m); return; end
- order = {'echo-02','echo-01','echo-03'};
- for k = 1:numel(order)
- m = contains(names, order{k});
- if any(m), bold_out = bold_in(m); return; end
- end
- % fallback: keep any echo-*
- bold_out = bold_in(contains(names,'echo-'));
- end
- function want_tasks = normalize_keep_tasks(keep_tasks)
- want_tasks = {};
- if isempty(keep_tasks), return; end
- kt = lower(string(keep_tasks));
- want_tasks = cell(1, numel(kt));
- for i = 1:numel(kt)
- s = char(kt(i));
- s = strrep(s,'task-','');
- want_tasks{i} = s;
- end
- end
- function lab = parse_task_label_lower(fname)
- tok = regexp(fname, 'task-([A-Za-z0-9]+)', 'tokens', 'once');
- if isempty(tok), lab = ''; else, lab = lower(tok{1}); end
- end
- function idx_sorted = sort_by_run_number(bold_names, idx_in)
- runs = nan(size(idx_in));
- for k = 1:numel(idx_in)
- nm = bold_names{idx_in(k)};
- tok = regexp(nm, 'run-(\d+)', 'tokens', 'once');
- if ~isempty(tok), runs(k) = str2double(tok{1}); else, runs(k) = k; end
- end
- [~, ord] = sort(runs, 'ascend');
- idx_sorted = idx_in(ord);
- end
- % =====================================================================
- % ============================= IO HELPERS ============================
- % =====================================================================
- function nii_path = ensure_nii_unzipped(path_in, opts)
- if endsWith(path_in, '.nii.gz', 'IgnoreCase', true)
- nii_path = regexprep(path_in, '\.gz$', '', 'ignorecase');
- if ~exist(nii_path,'file') || is_older(nii_path, path_in)
- logmsg(opts, ' gunzip: %s', path_in);
- gunzip(path_in, fileparts(path_in));
- end
- else
- nii_path = path_in;
- end
- end
- function base = strip_nii_ext(name)
- % Remove .nii or .nii.gz from a filename (no path)
- if endsWith(name, '.nii.gz', 'IgnoreCase', true)
- base = extractBefore(name, strlength(name) - 7);
- elseif endsWith(name, '.nii', 'IgnoreCase', true)
- base = extractBefore(name, strlength(name) - 4);
- else
- [~, base] = fileparts(name);
- end
- end
- function out = strip_gz(name)
- out = regexprep(name, '\.gz$', '', 'ignorecase');
- end
- function mean_ni = find_mean_epi(func_dir, bold_name)
- % Find actual mean EPI written by SPM after realign.
- name_no_gz = strip_gz(bold_name);
- cand = fullfile(func_dir, ['mean', name_no_gz]);
- if exist(cand, 'file'), mean_ni = cand; return; end
- base = regexprep(name_no_gz, '\.nii$', '', 'ignorecase');
- dd = dir(fullfile(func_dir, ['mean', base, '*.nii']));
- if ~isempty(dd), mean_ni = fullfile(func_dir, dd(1).name); return; end
- taskTok = regexp(bold_name,'task-[A-Za-z0-9]+','match','once');
- if ~isempty(taskTok)
- dd = dir(fullfile(func_dir, ['mean*', taskTok, '*.nii']));
- if ~isempty(dd), mean_ni = fullfile(func_dir, dd(1).name); return; end
- end
- error('Mean EPI not found after realign in %s (for %s).', func_dir, bold_name);
- end
- function export_motion_confounds(func_dir, bold_base, opts)
- % Copy rp_*.txt and optionally write rp_*.tsv with header to func/confounds/
- src = fullfile(func_dir, ['rp_', bold_base, '.txt']);
- if ~exist(src,'file')
- % If realign hasn't produced it (rare), just skip silently.
- return;
- end
- conf_dir = fullfile(func_dir, 'confounds');
- if ~exist(conf_dir,'dir')
- try mkdir(conf_dir); catch, return; end
- end
- dst_txt = fullfile(conf_dir, ['rp_', bold_base, '.txt']);
- try
- if ~exist(dst_txt,'file') || is_older(dst_txt, src)
- copyfile(src, dst_txt);
- end
- catch
- end
- if ~opts.write_tsv_confounds
- return;
- end
- dst_tsv = fullfile(conf_dir, ['rp_', bold_base, '.tsv']);
- try
- need_write = ~exist(dst_tsv,'file') || is_older(dst_tsv, src);
- if need_write
- M = readmatrix(src);
- if size(M,2) >= 6
- fid = fopen(dst_tsv,'w'); if fid==-1, return; end
- fprintf(fid, 'trans_x\ttrans_y\ttrans_z\trot_x\trot_y\trot_z\n');
- fmt = '%g\t%g\t%g\t%g\t%g\t%g\n';
- for i = 1:size(M,1)
- fprintf(fid, fmt, M(i,1), M(i,2), M(i,3), M(i,4), M(i,5), M(i,6));
- end
- fclose(fid);
- end
- end
- catch
- end
- end
- function tf = is_older(target, reference)
- if ~exist(target,'file'), tf = true; return; end
- dt_target = dir(target); dt_ref = dir(reference);
- tf = dt_target.datenum < dt_ref.datenum;
- end
- function touch(fname)
- fid = fopen(fname,'w');
- if fid>0, fclose(fid); end
- end
- % =====================================================================
- % ============================ UTIL HELPERS ===========================
- % =====================================================================
- function add_spm_path(spm_path)
- % Accept either a folder containing spm.m OR a direct path to spm.m
- if exist(spm_path,'file') == 2
- [spm_folder,~,~] = fileparts(spm_path);
- else
- spm_folder = spm_path;
- end
- if exist(spm_folder,'dir') ~= 7
- error('SPM folder not found: %s', spm_folder);
- end
- if exist(fullfile(spm_folder,'spm.m'),'file') ~= 2
- error('spm.m not found in: %s', spm_folder);
- end
- addpath(spm_folder);
- end
- function s = ternary(cond, a, b)
- if cond, s = a; else, s = b; end
- end
- function out = fill_defaults(in, defs)
- out = in;
- f = fieldnames(defs);
- for i = 1:numel(f)
- if ~isfield(out, f{i}) || isempty(out.(f{i}))
- out.(f{i}) = defs.(f{i});
- end
- end
- end
- function logmsg(opts, varargin)
- if isfield(opts,'verbose') && ~opts.verbose
- return;
- end
- fprintf([varargin{1}, '\n'], varargin{2:end});
- end
preprocess_bids_minimal.m at commit d3fb84b, under MIT · at the source
Overview
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
d3fb84b1cf4fce0411a869fa96e6e4c32a5e625b, 6 March 2026Availability: 1 check, the latest on 27 September 2026: the link answers
- 27 September 2026: the link answers
20 files
- src/
analysis/ , Python, 43 linesanalyze_koopman_compare. py - src/
analysis/ , Python, 25 linesanalyze_koopman_encoding _task.py - src/
analysis/ , Python, 25 linesanalyze_koopman_rest_tas k.py - src/
analysis/ , Python, 11 linesanalyze_koopman_similari ty_task.py - src/
extraction/ , Python, 261 linesextract_timeseries_c1.py - src/
extraction/ , Python, 215 linesextract_timeseries_c2.py - src/
extraction/ , Python, 252 linesextract_timeseries_c3.py - src/
preparation/ , Python, 159 linesbuild_pairs_c1_by_task_s ession.py - src/
preparation/ , Python, 61 linesprepare_pairs_by_task.py - src/
preparation/ , Python, 253 linesprepare_pairs_c2_int_ext .py - src/
preparation/ , Python, 114 linesprepare_pairs_c3_tempora l.py - src/
preprocessing/ , MATLAB, 667 lines, 3 matchespreprocess_bids_minimal. m - src/
preprocessing/ , MATLAB, 19 linesrun_ds003511.m - src/
preprocessing/ , MATLAB, 20 linesrun_ds003721.m - src/
preprocessing/ , MATLAB, 17 linesrun_ds005581.m - src/
training/ , Python, 252 linestrain_c1.py - src/
training/ , Python, 246 linestrain_c2_updated.py - src/
training/ , Python, 245 linestrain_c3_temporal.py - LICENSE, License, 21 lines
- README.md, Text, 90 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 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
- doi:10.18112/
openneuro.ds003511.v1.1. , at OpenNeuro; found in the references1 - doi:10.18112/
openneuro.ds003721.v1.0. , at OpenNeuro; found in the references0 - doi:10.18112/
openneuro.ds005581.v1.0. , at OpenNeuro; found in the references0 - openneuro:ds003511, at OpenNeuro; found in the text, “Cluster 3: Distributed long-term storage (H 3 )”
- openneuro:ds003721, at OpenNeuro; found in the text, “Cluster 2: Episodic replay during rest (H 2 )”
- openneuro:ds005581, at OpenNeuro; found in the text, “Cluster 1: Standard consolidation (H 1 )”
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://
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://
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://
BibTeX
@article{bartzioka2026la
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/
url = {https://
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/
VL - 6
IS - 3
SP - 100398
SN - 2666-9560
PB - Elsevier
DO - 10.1016/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.1016/
"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":
"volume": "6",
"issue": "3",
"page": "100398",
"DOI": "10.1016/
"PMID": "42656631",
"PMCID": "PMC13507544",
"ISSN": "2666-9560",
"publisher": "Elsevier",
"URL": "https://
"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: iScienceIn 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 brainJournal: 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: eLifeIn 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 brainJournal: 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: NeuroImageIn 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: EpilepsiaIn 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 communicationsIn 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: NeuronIn 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.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 18 scripts, and 3 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:f23b9bb3dba92853…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
