Skip to content

Commit 240b29f

Browse files
committed
enh: add topup tests
1 parent 19015c4 commit 240b29f

File tree

6 files changed

+298
-363
lines changed

6 files changed

+298
-363
lines changed

sdcflows/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
from pathlib import Path
44
import numpy
5+
import nibabel
56
import pytest
67
from bids.layout import BIDSLayout
78

@@ -23,6 +24,7 @@ def pytest_report_header(config):
2324
@pytest.fixture(autouse=True)
2425
def add_np(doctest_namespace):
2526
doctest_namespace['np'] = numpy
27+
doctest_namespace['nb'] = nibabel
2628
doctest_namespace['os'] = os
2729
doctest_namespace['Path'] = Path
2830
for key, val in list(layouts.items()):

sdcflows/interfaces/epi.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""
4+
Interfaces to deal with the various types of fieldmap sources.
5+
6+
.. testsetup::
7+
8+
>>> tmpdir = getfixture('tmpdir')
9+
>>> tmp = tmpdir.chdir() # changing to a temporary directory
10+
>>> nb.Nifti1Image(np.zeros((90, 90, 60)), None, None).to_filename(
11+
... tmpdir.join('epi.nii.gz').strpath)
12+
13+
14+
"""
15+
16+
from nipype.interfaces.base import (
17+
BaseInterfaceInputSpec,
18+
TraitedSpec,
19+
File,
20+
isdefined,
21+
traits,
22+
SimpleInterface,
23+
)
24+
25+
26+
class _GetReadoutTimeInputSpec(BaseInterfaceInputSpec):
27+
in_file = File(exists=True, desc="EPI image corresponding to the metadata")
28+
metadata = traits.Dict(mandatory=True, desc="metadata corresponding to the inputs")
29+
30+
31+
class _GetReadoutTimeOutputSpec(TraitedSpec):
32+
readout_time = traits.Float
33+
34+
35+
class GetReadoutTime(SimpleInterface):
36+
"""Calculate the readout time from available metadata."""
37+
38+
input_spec = _GetReadoutTimeInputSpec
39+
output_spec = _GetReadoutTimeOutputSpec
40+
41+
def _run_interface(self, runtime):
42+
self._results["readout_time"] = get_trt(
43+
self.inputs.metadata,
44+
self.inputs.in_file if isdefined(self.inputs.in_file) else None,
45+
)
46+
return runtime
47+
48+
49+
def get_trt(in_meta, in_file=None):
50+
r"""
51+
Extract the *total readout time* :math:`t_\text{RO}` from BIDS.
52+
53+
Calculate the *total readout time* for an input
54+
:abbr:`EPI (echo-planar imaging)` scan.
55+
56+
There are several procedures to calculate the total
57+
readout time. The basic one is that a ``TotalReadoutTime``
58+
field is set in the JSON sidecar. The following examples
59+
use an ``'epi.nii.gz'`` file-stub which has 90 pixels in the
60+
j-axis encoding direction.
61+
62+
>>> meta = {'TotalReadoutTime': 0.02596}
63+
>>> get_trt(meta)
64+
0.02596
65+
66+
If the *effective echo spacing* :math:`t_\text{ees}`
67+
(``EffectiveEchoSpacing`` BIDS field) is provided, then the
68+
total readout time can be calculated reading the number
69+
of voxels along the readout direction :math:`T_\text{ro}`
70+
and the parallel acceleration factor of the EPI :math:`f_\text{acc}`.
71+
72+
.. math ::
73+
74+
T_\text{ro} = t_\text{ees} \, (N_\text{PE} / f_\text{acc} - 1)
75+
76+
>>> meta = {'EffectiveEchoSpacing': 0.00059,
77+
... 'PhaseEncodingDirection': 'j-',
78+
... 'ParallelReductionFactorInPlane': 2}
79+
>>> get_trt(meta, in_file='epi.nii.gz')
80+
0.02596
81+
82+
Some vendors, like Philips, store different parameter names:
83+
84+
>>> meta = {'WaterFatShift': 8.129,
85+
... 'MagneticFieldStrength': 3,
86+
... 'PhaseEncodingDirection': 'j-',
87+
... 'ParallelReductionFactorInPlane': 2}
88+
>>> get_trt(meta, in_file='epi.nii.gz')
89+
0.018721183563864822
90+
91+
"""
92+
import nibabel as nb
93+
94+
# Use case 1: TRT is defined
95+
trt = in_meta.get("TotalReadoutTime", None)
96+
if trt is not None:
97+
return trt
98+
99+
# All other cases require the parallel acc and npe (N vox in PE dir)
100+
acc = float(in_meta.get("ParallelReductionFactorInPlane", 1.0))
101+
npe = nb.load(in_file).shape[_get_pe_index(in_meta)]
102+
etl = npe // acc
103+
104+
# Use case 2: TRT is defined
105+
ees = in_meta.get("EffectiveEchoSpacing", None)
106+
if ees is not None:
107+
return ees * (etl - 1)
108+
109+
# Use case 3 (philips scans)
110+
wfs = in_meta.get("WaterFatShift", None)
111+
if wfs is not None:
112+
fstrength = in_meta["MagneticFieldStrength"]
113+
wfd_ppm = 3.4 # water-fat diff in ppm
114+
g_ratio_mhz_t = 42.57 # gyromagnetic ratio for proton (1H) in MHz/T
115+
wfs_hz = fstrength * wfd_ppm * g_ratio_mhz_t
116+
return wfs / wfs_hz
117+
118+
raise ValueError("Unknown total-readout time specification")
119+
120+
121+
def get_ees(in_meta, in_file=None):
122+
r"""
123+
Extract the *effective echo spacing* :math:`t_\text{ees}` from BIDS.
124+
125+
Calculate the *effective echo spacing* :math:`t_\text{ees}`
126+
for an input :abbr:`EPI (echo-planar imaging)` scan.
127+
128+
129+
There are several procedures to calculate the effective
130+
echo spacing. The basic one is that an ``EffectiveEchoSpacing``
131+
field is set in the JSON sidecar. The following examples
132+
use an ``'epi.nii.gz'`` file-stub which has 90 pixels in the
133+
j-axis encoding direction.
134+
135+
>>> meta = {'EffectiveEchoSpacing': 0.00059,
136+
... 'PhaseEncodingDirection': 'j-'}
137+
>>> get_ees(meta)
138+
0.00059
139+
140+
If the *total readout time* :math:`T_\text{ro}` (``TotalReadoutTime``
141+
BIDS field) is provided, then the effective echo spacing can be
142+
calculated reading the number of voxels :math:`N_\text{PE}` along the
143+
readout direction and the parallel acceleration
144+
factor of the EPI
145+
146+
.. math ::
147+
148+
= T_\text{ro} \, (N_\text{PE} / f_\text{acc} - 1)^{-1}
149+
150+
where :math:`N_y` is the number of pixels along the phase-encoding direction
151+
:math:`y`, and :math:`f_\text{acc}` is the parallel imaging acceleration factor
152+
(:abbr:`GRAPPA (GeneRalized Autocalibrating Partial Parallel Acquisition)`,
153+
:abbr:`ARC (Autocalibrating Reconstruction for Cartesian imaging)`, etc.).
154+
155+
>>> meta = {'TotalReadoutTime': 0.02596,
156+
... 'PhaseEncodingDirection': 'j-',
157+
... 'ParallelReductionFactorInPlane': 2}
158+
>>> get_ees(meta, in_file='epi.nii.gz')
159+
0.00059
160+
161+
Some vendors, like Philips, store different parameter names (see
162+
http://dbic.dartmouth.edu/pipermail/mrusers/attachments/20141112/eb1d20e6/attachment.pdf
163+
):
164+
165+
>>> meta = {'WaterFatShift': 8.129,
166+
... 'MagneticFieldStrength': 3,
167+
... 'PhaseEncodingDirection': 'j-',
168+
... 'ParallelReductionFactorInPlane': 2}
169+
>>> get_ees(meta, in_file='epi.nii.gz')
170+
0.00041602630141921826
171+
172+
"""
173+
import nibabel as nb
174+
from sdcflows.interfaces.epi import _get_pe_index
175+
176+
# Use case 1: EES is defined
177+
ees = in_meta.get("EffectiveEchoSpacing", None)
178+
if ees is not None:
179+
return ees
180+
181+
# All other cases require the parallel acc and npe (N vox in PE dir)
182+
acc = float(in_meta.get("ParallelReductionFactorInPlane", 1.0))
183+
npe = nb.load(in_file).shape[_get_pe_index(in_meta)]
184+
etl = npe // acc
185+
186+
# Use case 2: TRT is defined
187+
trt = in_meta.get("TotalReadoutTime", None)
188+
if trt is not None:
189+
return trt / (etl - 1)
190+
191+
# Use case 3 (philips scans)
192+
wfs = in_meta.get("WaterFatShift", None)
193+
if wfs is not None:
194+
fstrength = in_meta["MagneticFieldStrength"]
195+
wfd_ppm = 3.4 # water-fat diff in ppm
196+
g_ratio_mhz_t = 42.57 # gyromagnetic ratio for proton (1H) in MHz/T
197+
wfs_hz = fstrength * wfd_ppm * g_ratio_mhz_t
198+
return wfs / (wfs_hz * etl)
199+
200+
raise ValueError("Unknown effective echo-spacing specification")
201+
202+
203+
def _get_pe_index(meta):
204+
pe = meta["PhaseEncodingDirection"]
205+
try:
206+
return {"i": 0, "j": 1, "k": 2}[pe[0]]
207+
except KeyError:
208+
raise RuntimeError('"%s" is an invalid PE string' % pe)

sdcflows/interfaces/fmap.py

Lines changed: 0 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -379,167 +379,6 @@ def _unwrap(fmap_data, mag_file, mask=None):
379379
return unwrapped
380380

381381

382-
def get_ees(in_meta, in_file=None):
383-
r"""
384-
Extract the *effective echo spacing* :math:`t_\text{ees}` from BIDS.
385-
386-
Calculate the *effective echo spacing* :math:`t_\text{ees}`
387-
for an input :abbr:`EPI (echo-planar imaging)` scan.
388-
389-
390-
There are several procedures to calculate the effective
391-
echo spacing. The basic one is that an ``EffectiveEchoSpacing``
392-
field is set in the JSON sidecar. The following examples
393-
use an ``'epi.nii.gz'`` file-stub which has 90 pixels in the
394-
j-axis encoding direction.
395-
396-
>>> meta = {'EffectiveEchoSpacing': 0.00059,
397-
... 'PhaseEncodingDirection': 'j-'}
398-
>>> get_ees(meta)
399-
0.00059
400-
401-
If the *total readout time* :math:`T_\text{ro}` (``TotalReadoutTime``
402-
BIDS field) is provided, then the effective echo spacing can be
403-
calculated reading the number of voxels :math:`N_\text{PE}` along the
404-
readout direction and the parallel acceleration
405-
factor of the EPI
406-
407-
.. math ::
408-
409-
= T_\text{ro} \, (N_\text{PE} / f_\text{acc} - 1)^{-1}
410-
411-
where :math:`N_y` is the number of pixels along the phase-encoding direction
412-
:math:`y`, and :math:`f_\text{acc}` is the parallel imaging acceleration factor
413-
(:abbr:`GRAPPA (GeneRalized Autocalibrating Partial Parallel Acquisition)`,
414-
:abbr:`ARC (Autocalibrating Reconstruction for Cartesian imaging)`, etc.).
415-
416-
>>> meta = {'TotalReadoutTime': 0.02596,
417-
... 'PhaseEncodingDirection': 'j-',
418-
... 'ParallelReductionFactorInPlane': 2}
419-
>>> get_ees(meta, in_file='epi.nii.gz')
420-
0.00059
421-
422-
Some vendors, like Philips, store different parameter names (see
423-
http://dbic.dartmouth.edu/pipermail/mrusers/attachments/20141112/eb1d20e6/attachment.pdf
424-
):
425-
426-
>>> meta = {'WaterFatShift': 8.129,
427-
... 'MagneticFieldStrength': 3,
428-
... 'PhaseEncodingDirection': 'j-',
429-
... 'ParallelReductionFactorInPlane': 2}
430-
>>> get_ees(meta, in_file='epi.nii.gz')
431-
0.00041602630141921826
432-
433-
"""
434-
435-
import nibabel as nb
436-
from sdcflows.interfaces.fmap import _get_pe_index
437-
438-
# Use case 1: EES is defined
439-
ees = in_meta.get('EffectiveEchoSpacing', None)
440-
if ees is not None:
441-
return ees
442-
443-
# All other cases require the parallel acc and npe (N vox in PE dir)
444-
acc = float(in_meta.get('ParallelReductionFactorInPlane', 1.0))
445-
npe = nb.load(in_file).shape[_get_pe_index(in_meta)]
446-
etl = npe // acc
447-
448-
# Use case 2: TRT is defined
449-
trt = in_meta.get('TotalReadoutTime', None)
450-
if trt is not None:
451-
return trt / (etl - 1)
452-
453-
# Use case 3 (philips scans)
454-
wfs = in_meta.get('WaterFatShift', None)
455-
if wfs is not None:
456-
fstrength = in_meta['MagneticFieldStrength']
457-
wfd_ppm = 3.4 # water-fat diff in ppm
458-
g_ratio_mhz_t = 42.57 # gyromagnetic ratio for proton (1H) in MHz/T
459-
wfs_hz = fstrength * wfd_ppm * g_ratio_mhz_t
460-
return wfs / (wfs_hz * etl)
461-
462-
raise ValueError('Unknown effective echo-spacing specification')
463-
464-
465-
def get_trt(in_meta, in_file=None):
466-
r"""
467-
Extract the *total readout time* :math:`t_\text{RO}` from BIDS.
468-
469-
Calculate the *total readout time* for an input
470-
:abbr:`EPI (echo-planar imaging)` scan.
471-
472-
There are several procedures to calculate the total
473-
readout time. The basic one is that a ``TotalReadoutTime``
474-
field is set in the JSON sidecar. The following examples
475-
use an ``'epi.nii.gz'`` file-stub which has 90 pixels in the
476-
j-axis encoding direction.
477-
478-
>>> meta = {'TotalReadoutTime': 0.02596}
479-
>>> get_trt(meta)
480-
0.02596
481-
482-
If the *effective echo spacing* :math:`t_\text{ees}`
483-
(``EffectiveEchoSpacing`` BIDS field) is provided, then the
484-
total readout time can be calculated reading the number
485-
of voxels along the readout direction :math:`T_\text{ro}`
486-
and the parallel acceleration factor of the EPI :math:`f_\text{acc}`.
487-
488-
.. math ::
489-
490-
T_\text{ro} = t_\text{ees} \, (N_\text{PE} / f_\text{acc} - 1)
491-
492-
>>> meta = {'EffectiveEchoSpacing': 0.00059,
493-
... 'PhaseEncodingDirection': 'j-',
494-
... 'ParallelReductionFactorInPlane': 2}
495-
>>> get_trt(meta, in_file='epi.nii.gz')
496-
0.02596
497-
498-
Some vendors, like Philips, store different parameter names:
499-
500-
>>> meta = {'WaterFatShift': 8.129,
501-
... 'MagneticFieldStrength': 3,
502-
... 'PhaseEncodingDirection': 'j-',
503-
... 'ParallelReductionFactorInPlane': 2}
504-
>>> get_trt(meta, in_file='epi.nii.gz')
505-
0.018721183563864822
506-
507-
"""
508-
# Use case 1: TRT is defined
509-
trt = in_meta.get('TotalReadoutTime', None)
510-
if trt is not None:
511-
return trt
512-
513-
# All other cases require the parallel acc and npe (N vox in PE dir)
514-
acc = float(in_meta.get('ParallelReductionFactorInPlane', 1.0))
515-
npe = nb.load(in_file).shape[_get_pe_index(in_meta)]
516-
etl = npe // acc
517-
518-
# Use case 2: TRT is defined
519-
ees = in_meta.get('EffectiveEchoSpacing', None)
520-
if ees is not None:
521-
return ees * (etl - 1)
522-
523-
# Use case 3 (philips scans)
524-
wfs = in_meta.get('WaterFatShift', None)
525-
if wfs is not None:
526-
fstrength = in_meta['MagneticFieldStrength']
527-
wfd_ppm = 3.4 # water-fat diff in ppm
528-
g_ratio_mhz_t = 42.57 # gyromagnetic ratio for proton (1H) in MHz/T
529-
wfs_hz = fstrength * wfd_ppm * g_ratio_mhz_t
530-
return wfs / wfs_hz
531-
532-
raise ValueError('Unknown total-readout time specification')
533-
534-
535-
def _get_pe_index(meta):
536-
pe = meta['PhaseEncodingDirection']
537-
try:
538-
return {'i': 0, 'j': 1, 'k': 2}[pe[0]]
539-
except KeyError:
540-
raise RuntimeError('"%s" is an invalid PE string' % pe)
541-
542-
543382
def _torads(in_file, fmap_range=None, newpath=None):
544383
"""
545384
Convert a field map to rad/s units.

0 commit comments

Comments
 (0)