Skip to content

ENH: Add optional warpkit MEDIC support for multi-echo BOLD distortion correction#3645

Open
DVSneuro wants to merge 16 commits into
nipreps:masterfrom
DVSneuro:warpkit-medic
Open

ENH: Add optional warpkit MEDIC support for multi-echo BOLD distortion correction#3645
DVSneuro wants to merge 16 commits into
nipreps:masterfrom
DVSneuro:warpkit-medic

Conversation

@DVSneuro

@DVSneuro DVSneuro commented May 8, 2026

Copy link
Copy Markdown

Changes proposed in this pull request

I think this is a little related to #3336 and maybe some other work from @tsalo , but this PR addresses a narrower use case than that issue: optional MEDIC-based susceptibility distortion correction for compatible multi-echo BOLD runs with part-phase data, using warpkit following the logic from Andrew Van's ( @vanandrew ) preprint on bioRxiv: https://www.biorxiv.org/content/10.1101/2023.11.28.568744v1

When --me-use-warpkit is enabled, fMRIPrep will:

  • identify eligible multi-echo BOLD runs with corresponding part-phase data
  • estimate a time-varying B0 fieldmap series with MEDIC from phase evolution across echoes
  • use that fieldmap series during resampling
  • report the run as MEDIC (warpkit) in the functional summary

This path is opt-in and does not change default behavior for existing workflows.

Main changes in this PR:

  • add a new CLI/config option: --me-use-warpkit
  • add an interface for calling warpkit.api.medic
  • wire MEDIC into the multi-echo BOLD workflow for eligible runs
  • ensure MEDIC-managed runs do not also use standard fmap selection for the same run
  • add support for applying a 4D fieldmap series volume-by-volume during resampling
  • add reportlets for a representative fieldmap view and an SDC before/after panel
  • add citation/boilerplate text describing the MEDIC-based correction path
  • add tests covering parser/config behavior, workflow wiring, BIDS helpers, and resampling behavior

Current scope / limitations:

  • this feature is opt-in
  • it is currently intended for compatible multi-echo datasets with part-phase companions
  • MEDIC-managed runs currently prefer the MEDIC path rather than falling back to standard fieldmaps for the same run
  • report output currently includes representative static views rather than a time-resolved QC plot of the full 4D fieldmap series

Validation performed:

  • targeted unit/workflow tests for the new CLI flag, interface wiring, BIDS helpers, and 4D fieldmap resampling
  • container build including warpkit
  • smoke testing of the containerized import path
  • a real multi-echo dataset with phase data from OpenNeuro (https://openneuro.org/datasets/ds005123/versions/1.1.3), confirming successful execution and report generation

In the HTML report, warpkit-enabled runs showed:

  • Susceptibility distortion correction: MEDIC (warpkit)
  • a representative fieldmap reportlet
  • an SDC before/after reportlet

This is my first contribution to fMRIPrep, so I tried to keep the scope focused and follow existing workflow patterns closely.

I used OpenAI Codex to help draft and iterate on portions of the code and tests, and I manually reviewed and tested the resulting changes as carefully as I could, but I wouldn't say I'm an expert on this (maybe a little better than vibe coding, but not by much).

Documentation that should be reviewed

This PR does not add standalone user documentation pages, but it does update user-facing/report-facing text in a few places:

  • a new CLI option, --me-use-warpkit
  • functional reportlets for the MEDIC/warpkit path
  • citation/boilerplate text describing the MEDIC-based distortion-correction procedure

@welcome

welcome Bot commented May 8, 2026

Copy link
Copy Markdown

Thanks for opening this pull request! It looks like this is your first time contributing to fMRIPrep. 😄
We invite you to list yourself as an fMRIPrep contributor. To learn more about what that entails and how we credit our contributors, please check out the contributing guidelines. If your name is not already on the list, please insert it, in alphabetical order, into the CONTRIBUTORS.md file.
Of course, if you want to opt-out this time there is no problem at all with adding your name later. You will be always welcome to add it in the future whenever you feel it should be listed.

=== Do not change lines below ===
{
 "chain": [],
 "cmd": "bash -c '! pixi lock --check || git checkout .'",
 "exit": 0,
 "extra_inputs": [],
 "inputs": [
  "pixi.lock",
  "pyproject.toml"
 ],
 "outputs": [
  "pixi.lock"
 ],
 "pwd": "."
}
^^^ Do not change lines above ^^^
@effigies

effigies commented May 8, 2026

Copy link
Copy Markdown
Member

This is extremely interesting and coincides with some work by @vanandrew in sdcflows (note to self: link PR). I've been meaning to look into how it would integrate and haven't found the time.

Have you run datasets with this workflow? Do the results look good to you? Do you have comparisons with other fieldmap types?

I will try to look at this in some detail soon.

@DVSneuro

DVSneuro commented May 8, 2026

Copy link
Copy Markdown
Author

I've only ran a couple of subjects from our dataset (https://openneuro.org/datasets/ds005123/versions/1.1.3), and here's one example visualization for a single time point, which looks pretty good to me.

sub-10886_ses-01_task-sharedreward_run-1_part-mag_desc-fieldmap_bold

I haven't compared it directly with standard fieldmaps, though we have those in the dataset above.

We were drawn to warpkit/medic since some of our participants move a lot (older adults) and we had a few cases where the participant needed to get out of the scanner for a break but we forgot to get a new fieldmap when they got back in. So, the idea of making the fieldmaps for each frame using the multi-echo phase data was very appealing to us! But when we first started using medic/warpkit a year or so ago, I couldn't figure out how to get it integrated into fmriprep, so we'd generate a fieldmap for the first time point of each run and use that in the normal fmriprep workflow and the results have been good. I'm assuming the framewise implementation should be even better, especially for high-motion participants -- but hopefully I didn't break anything or do anything too silly with this PR!

@vanandrew

Copy link
Copy Markdown

I got you @effigies: nipreps/sdcflows#541 :)

I haven't reviewed @DVSneuro PR yet, but my vague plan was to have MEDIC be called from sdcflows via init_fmap_preproc_wf (this line in fmriprep).

On the fmriprep side, the biggest changes would need to be in how the field map is applied: in MEDIC, you get a field map for every frame of your scan, so the one step resampling would need to be adjusted to account for this. Given that @DVSneuro has some results, I assume your PR already makes a change like this?

Awesome job getting this working btw :).

@DVSneuro

DVSneuro commented May 8, 2026

Copy link
Copy Markdown
Author

Thanks @vanandrew ! I had plenty of help from Codex. And yes, this PR does update the fMRIPrep side so a 4D MEDIC fieldmap series is applied volume-by-volume during one-step resampling. The current implementation calls MEDIC directly from fMRIPrep, but I’d be open to trying to restructure that piece if the preferred long-term architecture is to generate the MEDIC estimates in sdcflows and then consume them from fMRIPrep.

I'm not seeing any other multi-echo datasets on OpenNeuro that have the phase data, so the testing here has been somewhat limited.

@tsalo

tsalo commented May 8, 2026

Copy link
Copy Markdown
Collaborator

@DVSneuro, I suggest checking https://me-ica.github.io/open-multi-echo-data/. I try to keep it up to date, and you can filter by if there's complex-valued data or not.

There are nine on OpenNeuro that I'm aware of:

I think the goal should be to have MEDIC within SDCFlows so that dynamic distortion correction is treated like static distortion correction, though it would be cool if fMRIPrep could use wk-unwrap-phase to produce unwrapped phase data in any desired output spaces.

One change that might be necessary on fMRIPrep's side is (optionally?) swapping the order of HMC and SDC when MEDIC is used, since I think dynamic distortion correction should improve motion correction.

@vanandrew

vanandrew commented May 8, 2026

Copy link
Copy Markdown

For datasets, I've mostly been using this one for my own tests: https://openneuro.org/datasets/ds006926/versions/1.0.0 (Thanks @tsalo for the list of mag/phase datasets).

Getting into some of the more technical details:

  • The plan is to have [FEAT] MEDIC distortion correction via warpkit sdcflows#541 emit fmap_dynamic* fields from init_fmap_workflow_wf.
  • This fieldmap dynamic file is a 4D nifti, where each frame contains a fieldmap in Hz (so extracting a single frame would be equivalent to the regular fmap output).
  • @tsalo suggestion for swapping HMC/SDC, that's definitely worth considering. I don't think I looked into this in my original paper. I'm not sure how much work that is though, so it might be worth shifting into another PR.

Side question: I noticed that both nipreps project currently support python version 3.10+, but I've made warpkit support 3.11+ (mainly because 3.10 is due to be deprecated in October this year). It is worth lowering the python version requirement for warpkit? I wasn't sure how long you guys support deprecated python versions.

@DVSneuro

Copy link
Copy Markdown
Author

Thanks, @tsalo and @vanandrew ! I'm happy to try and help with the sdcflows approach. In the meantime, I ran additional validation with an Apptainer image built from this branch using --me-use-warpkit on several public OpenNeuro datasets suggested above. I skipped a couple of the datasets since they are from my own lab and use the same parameters as my original test (ds007486 and ds006707).

Successful runs so far:

Dataset Subject Notes
ds007637 sub-01 Completed successfully; logs show MEDIC/warpkit for eligible multi-echo runs and standard PEPOLAR handling for other runs.
ds006926 sub-a01 Completed successfully with MEDIC/warpkit workflows.
ds006131 sub-20188 Completed successfully with MEDIC/warpkit workflows.
ds002278 sub-Blossom Completed successfully with multiple MEDIC/warpkit workflows.
ds005085 sub-10015 Completed successfully; this subject/run is from my lab and has a mix of single echo and multi-echo data, so it's a nice compatibility check.

One remaining test case is ds006072. sub-P1 was not eligible because it lacked matching part-phase BOLD files. sub-P1R did start MEDIC workflows, but failed inside warpkit phase unwrapping with an IndexError message -- I'm not sure what's happening there. I had to throttle the resources a lot since it's a big dataset and kept running out of memory (here's my testing script in case it's helpful for anyone: https://github.com/DVS-Lab/rf1-sra-linux2/blob/main/code/fmriprep-warpkit.sh).

@vanandrew

Copy link
Copy Markdown

sub-P1R did start MEDIC workflows, but failed inside warpkit phase unwrapping with an IndexError message

@DVSneuro If it's an error in warpkit, can you create a issue ticket here and post the exception stack? Thanks in advance!

DVSneuro and others added 6 commits May 10, 2026 22:39
`warpkit.api.medic` exposes three outputs: `fieldmap` (undistorted-space,
the consumer-facing output), `fieldmap_native` (a distorted-space debug
output), and `displacement_map`. The Stage-3 wiring used `fieldmap_native`,
which makes fmriprep's pull-resampler look up the field at the wrong
physical location at each target voxel (it's off by the SDC displacement
itself).

`resample_vol` documents fmap_hz as "sampled to the target space, in Hz"
(interfaces/resampling.py:272-273) — i.e., the undistorted/output grid.
That's `fieldmap`, not `fieldmap_native`.

Verified empirically by running `resample_series_async` against both
fields on two datasets:
  * warpkit's 3.5mm fixture: RMS Δ ≈ 358, max|Δ| ≈ 17.5k vs mean signal ~7050
  * ds007637 sub-04 at 2mm: RMS Δ ≈ 211, max|Δ| ≈ 21k vs mean signal ~4175
Relative error grows with resolution, as expected — a fixed physical
displacement spans more voxels in a finer grid.
`func_fit_reports_wf.inputnode.fieldmap` flows into FieldmapReportlet
(workflows/bold/outputs.py:373-401), which expects a 3D static field. The
Stage-3 warpkit branch was passing the 4D HMC-aligned per-frame field
directly, so the reportlet either crashes on the 4D NIfTI or silently
renders frame 0 only.

Pipe the temporal mean (already computed by `warpkit_mean` on line 526)
instead. The 4D series remains the outputnode.fieldmap that the bold
native workflow consumes for per-frame correction.
`mem_gb['resampled']` is the memory footprint of one resampled BOLD
volume (`filesize * 4` in `estimate_bold_mem_usage`). MEDIC actually
holds all N echoes of phase + magnitude in memory as float64, plus
warpkit's internal state (unwrapped phases, masks, per-frame fields).

For typical multi-echo data this underestimates real usage by an order
of magnitude. ds007637 sub-04 is 5 echoes × ~320 MB compressed — close
to 3 GB resident before warpkit's working set. The nipype scheduler
will happily oversubscribe and OOM.

Use `2 * n_echoes * filesize` instead, which roughly doubles the input
footprint to cover outputs and warpkit's internal arrays.
The Stage-3 warpkit branch cleared `fmapref_buffer.sbref_files` and
gated `raw_sbref_wf` with `not warpkit_enabled`. SBRef is acquired
separately from the BOLD run and is orthogonal to MEDIC — dropping it
silently is a regression for sites that acquire it.

If there's a concrete incompatibility between SBRef and the MEDIC
field-application path, it should be a stated limitation with a clear
error, not an invisible behavior change. Removing the gate so SBRef
flows through the same way it does on every other distortion-correction
path.
`bold_series` is sorted by EchoTime in `workflows/base.py:250` and
`phase_files` is independently sorted by EchoTime in `collect_bold_part_files`.
They *should* line up, but a missing or zero-default EchoTime sidecar on
one side and not the other silently transposes the pair set fed to
warpkit. Add an explicit cross-check so that misalignment fails fast with
a readable message instead of producing garbage fieldmaps.
@DVSneuro

Copy link
Copy Markdown
Author

Thanks, @vanandrew! Just tying up this loose end: your suggestion about the noise_frames parameter resolved the ds006072/sub-P1R failure I noted above and reported in vanandrew/warpkit#24.

I pushed a small follow-up commit exposing warpkit’s noise_frames parameter as --me-warpkit-noise-frames in the current fMRIPrep-side implementation: (4eb308f). I rebuilt the container and reran ds006072 v1.0.8, sub-P1R, limited to ses-1, with:

--me-use-warpkit --me-warpkit-noise-frames 3

This resolved the previous warpkit phase-unwrapping failure. The log shows the expected trimming message, both MEDIC nodes completed, and fMRIPrep finished successfully. ds006072_v1_0_8_sub-P1R_run-20260511-132325_fmriprep-warpkit.log

I also saw the draft changes in DVSneuro#1, including the move toward routing MEDIC through SDCFlows. Thanks for doing that, and I'm happy to test whenever it's ready! I understand that move may make --me-use-warpkit unnecessary, and that the exact home/name for a noise-frame option may change or become moot depending on how MEDIC is exposed through SDCFlows. I’m very open to whatever structure the maintainers think is best -- I'm mostly I’m excited about the possibility of getting this working cleanly in fMRIPrep!

@DVSneuro

Copy link
Copy Markdown
Author

Validation update after testing @vanandrew 's correctness-fix branch from DVSneuro#1.

I rebuilt the container and reran the public validation datasets I’ve been using. MEDIC workflows ran and completed for:

  • ds002278/sub-Blossom
  • ds005085/sub-10017
  • ds006131/sub-20188
  • ds006926/sub-a01
  • ds007637/sub-01

For ds005085/sub-10017, which includes a mix of single-echo and multi-echo runs, the single-echo runs followed the standard PHASEDIFF fieldmap path, while the eligible 4-echo runs used MEDIC/warpkit. I previously tested with ds005085/sub-10015, which didn't have any multi-echo data (an oversight on my part), but that test case didn't fail, which may be undesirable with the --me-use-warpkit option?

I also separately reran ds006072/sub-P1R/ses-1 with --me-warpkit-noise-frames 3, which resolved the prior warpkit phase-unwrapping failure from trailing noise frames. But, I did notice that the the masking might be getting messed up when using the --me-warpkit-noise-frames 3 option. This happened on both my tests, and I just missed it last time. The contour lines look ok, so maybe it's just image they are overlaid onto?
sub-P1R_ses-1_task-BOLDREST1_dir-PA_run-1_part-mag_desc-rois_bold

Otherwise, across these runs, I did not see crashes, tracebacks, fieldmap-length mismatches, or OOM failures in the logs.

One small reporting/QC question I noticed while reviewing the HTML: the MEDIC fieldmap reportlet uses the sdcflows FieldmapReportlet hover/toggle behavior, and the Fieldmap (Hz) layer can visually resemble the BOLD/magnitude underlay because the fieldmap is displayed over the reference image. I’m not sure this is incorrect, but the labeling/caption may be worth a maintainer look to make sure it is clear to users.

Thanks again for helping with this! @effigies -- looking forward to your comments and feedback when you get a chance!

Best wishes,
David

@effigies

Copy link
Copy Markdown
Member

Hey @DVSneuro @vanandrew @tsalo I wonder if we could catch up on a call this week. I'm pretty slammed at the moment, so I'm not sure when I can review this thoroughly and don't want to leave you hanging. A call where you could generally walk us through it all might be helpful.

@tsalo

tsalo commented May 20, 2026

Copy link
Copy Markdown
Collaborator

@effigies that would be great. I can make time for a call.

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.24427% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.84%. Comparing base (69ca63c) to head (a57f163).

Files with missing lines Patch % Lines
fmriprep/interfaces/warpkit.py 40.32% 37 Missing ⚠️
fmriprep/workflows/bold/fit.py 82.22% 5 Missing and 3 partials ⚠️
fmriprep/interfaces/resampling.py 20.00% 2 Missing and 2 partials ⚠️
fmriprep/workflows/base.py 66.66% 2 Missing and 1 partial ⚠️
fmriprep/cli/parser.py 75.00% 1 Missing and 1 partial ⚠️
fmriprep/utils/bids.py 81.81% 1 Missing and 1 partial ⚠️
fmriprep/interfaces/maths.py 94.73% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3645      +/-   ##
==========================================
+ Coverage   74.41%   74.84%   +0.42%     
==========================================
  Files          62       64       +2     
  Lines        4987     5235     +248     
  Branches      637      676      +39     
==========================================
+ Hits         3711     3918     +207     
- Misses       1142     1172      +30     
- Partials      134      145      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vanandrew

vanandrew commented May 20, 2026

Copy link
Copy Markdown

@effigies No worries. I'm also pretty busy this week, but can do next week or later if we want to do a call.

Where I think things stand: MEDIC needs to land in SDCflows first (nipreps/sdcflows#541), or at least be stable enough to build off of. I wouldn't merge this PR as-is, it predates the SDCflows work and would lock in a parallel MEDIC path we're about to rework.

Once nipreps/sdcflows#541 is in, we can start the fMRIPrep side. @DVSneuro has a good start here, and pieces of it carry over (the part-phase BIDS filtering, the framewise-4D ResampleSeries path, boilerplate/citations). The main thing to revisit is that with SDCflows owning MEDIC as a first-class estimator, we can simplify by routing it through the normal fieldmap pipeline rather than as a multi-echo-specific branch.

@DVSneuro

Copy link
Copy Markdown
Author

Thanks, @effigies. I’d be happy to chat through it, though no pressure on timing from my end. This isn’t holding anything up for us, and I’m also pretty swamped with a couple of grants right now, so the first week of June or later would work well for me!

I agree with @vanandrew’s read here. If the preferred long-term path is for MEDIC to land in SDCFlows first via nipreps/sdcflows#541, then I’m very happy to treat this PR as a working prototype/validation branch rather than something that should merge as-is.

When the timing makes sense, I’d be happy to walk through what this branch currently does, the datasets I tested, and the pieces that might carry over to the fMRIPrep side, especially the part-phase discovery, framewise/4D resampling path, and reporting/boilerplate pieces. And I’m happy to help refactor or retest once the SDCFlows side settles!

@effigies

Copy link
Copy Markdown
Member

@vanandrew @DVSneuro Sounds good. I've created a when2meet for next week and filled in my availability: https://www.when2meet.com/?36851069-sVt5P

If we can't make that, I will next have some availability after OHBM.

cc @mgxd @tsalo @oesteban (Oscar, I assume you're not available, but I think you'd be interested if you are.)

@effigies

Copy link
Copy Markdown
Member

Meeting invitation sent

@oesteban

oesteban commented May 30, 2026 via email

Copy link
Copy Markdown
Member

@effigies

Copy link
Copy Markdown
Member

3pm ET on Monday. There is also a Thursday slot everyone can make.

jacobian,
hmc_xfms[0] if hmc_xfms else None,
fmap_hz,
fmap_hz[..., 0] if fmap_is_series else fmap_hz,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the inline if/else since line 382 raises an error if fmap_is_series and data.ndim == 3. So this should only be fmap_hz.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants