""" Utility functions for returning slice times from number of slices and TR
Slice timing routines in nipy need a vector of slice times.
Slice times are vectors $t_i$ with $i = 0 ... N$ of times, one for each slice, where
$t_i$ gives the time at which slice number $i$ was acquired, relative to the
beginning of the volume acquisition.
We like these vectors because they are unambiguous; the indices $i$ refer to
positions in space, and the values $t_i$ refer to times.
But, there are many common slice timing regimes for which it's easy to get the
slice times once you know the volume acquisition time (the TR) and the number of
slices.
For example, if you acquired the slices in a simple ascending order, and you
have 10 slices and the TR was 2.0, then the slice times are:
>>> import numpy as np
>>> np.arange(10) / 10. * 2.0
array([ 0. , 0.2, 0.4, 0.6, 0.8, 1. , 1.2, 1.4, 1.6, 1.8])
These are small convenience functions that accept the number of slices and the
TR as input, and return a vector of slice times:
>>> ascending(10, 2.)
array([ 0. , 0.2, 0.4, 0.6, 0.8, 1. , 1.2, 1.4, 1.6, 1.8])
"""
from __future__ import division, print_function, absolute_import
import numpy as np
# Legacy repr printing from numpy.
from nipy.testing import legacy_printing as setup_module # noqa
# Dictionary (key, value) == (name, function) for slice timing functions
SLICETIME_FUNCTIONS = {}
def _dec_filldoc(func):
""" Fill docstring of slice time function
"""
func._doc_template = func.__doc__
func.__doc__ = func.__doc__.format(
**dict(
name = func.__name__,
pstr=
"""Note: slice 0 is the first slice in the voxel data block
Parameters
----------
n_slices : int
Number of slices in volume
TR : float
Time to acquire one full volume
Returns
-------
slice_times : (n_slices,) ndarray
Vectors $t_i i = 0 ... N$ of times, one for each slice, where $t_i$
gives the time at which slice number $i$ was acquired, relative to the
beginning of the volume acquisition.
"""))
return func
def _dec_register_stf(func):
""" Register slice time function in module dictionary """
name = func.__name__
SLICETIME_FUNCTIONS[name] = func
if name.startswith('st_'):
short_name = name[3:]
if short_name in SLICETIME_FUNCTIONS:
raise ValueError(
"Duplicate short / long function name {0}".format(short_name))
SLICETIME_FUNCTIONS[short_name] = func
return func
def _dec_stfunc(func):
return _dec_register_stf(_dec_filldoc(func))
def _derived_func(name, func):
def derived(n_slices, TR):
return func(n_slices, TR)
derived.__name__ = name
derived.__doc__ = func._doc_template
return _dec_stfunc(derived)
[docs]@_dec_stfunc
def st_01234(n_slices, TR):
""" Simple ascending slice sequence
slice 0 first, slice 1 second etc.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0. , 0.2, 0.4, 0.6, 0.8])
{pstr}
"""
return np.arange(n_slices) / n_slices * TR
ascending = _derived_func('ascending', st_01234)
[docs]@_dec_stfunc
def st_43210(n_slices, TR):
""" Simple descending slice sequence
slice ``n_slices-1`` first, slice ``n_slices - 2`` second etc.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0.8, 0.6, 0.4, 0.2, 0. ])
{pstr}
"""
return np.arange(n_slices)[::-1] / n_slices * TR
descending = _derived_func('descending', st_43210)
[docs]@_dec_stfunc
def st_02413(n_slices, TR):
"""Ascend alternate every second slice, starting at first slice
Collect slice 0 first, slice 2 second up to top. Then return to collect
slice 1, slice 3 etc.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0. , 0.6, 0.2, 0.8, 0.4])
{pstr}
"""
one_slice = TR / n_slices
time_to_space = list(range(0, n_slices, 2)) + list(range(1, n_slices, 2))
space_to_time = np.argsort(time_to_space)
return space_to_time * one_slice
asc_alt_2 = _derived_func('asc_alt_2', st_02413)
[docs]@_dec_stfunc
def st_13024(n_slices, TR):
"""Ascend alternate every second slice, starting at second slice
Collect slice 1 first, slice 3 second up to top (highest numbered slice).
Then return to collect slice 0, slice 2 etc. This order is rare except on
Siemens acquisitions with an even number of slices. See
:func:`st_odd0_even1` for this logic.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0.4, 0. , 0.6, 0.2, 0.8])
{pstr}
"""
one_slice = TR / n_slices
time_to_space = list(range(1, n_slices, 2)) + list(range(0, n_slices, 2))
space_to_time = np.argsort(time_to_space)
return space_to_time * one_slice
asc_alt_2_1 = _derived_func('asc_alt_2_1', st_13024)
[docs]@_dec_stfunc
def st_42031(n_slices, TR):
"""Descend alternate every second slice, starting at last slice
Collect slice (`n_slices` - 1) first, slice (`nslices` - 3) second down to
bottom (lowest numbered slice). Then return to collect slice (`n_slices`
-2), slice (`n_slices` - 4) etc.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0.4, 0.8, 0.2, 0.6, 0. ])
{pstr}
"""
return st_02413(n_slices, TR)[::-1]
desc_alt_2 = _derived_func('desc_alt_2', st_42031)
[docs]@_dec_stfunc
def st_odd0_even1(n_slices, TR):
"""Ascend alternate starting at slice 0 for odd, slice 1 for even `n_slices`
Acquisitions with alternating ascending slices from Siemens scanners often
seem to have this behavior as default - see:
https://mri.radiology.uiowa.edu/fmri_images.html
This means we use the :func:`st_02413` algorithm if `n_slices` is odd, and
the :func:`st_13024` algorithm if `n_slices` is even.
For example, for 4 slices and a TR of 1:
>>> {name}(4, 1.)
array([ 0.5 , 0. , 0.75, 0.25])
5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0. , 0.6, 0.2, 0.8, 0.4])
{pstr}
"""
if n_slices % 2 == 0:
return st_13024(n_slices, TR)
return st_02413(n_slices, TR)
asc_alt_siemens = _derived_func('asc_alt_siemens', st_odd0_even1)
[docs]@_dec_stfunc
def st_03142(n_slices, TR):
"""Ascend alternate, where alternation is by half the volume
Collect slice 0 then slice ``ceil(n_slices / 2.)`` then slice 1 then slice
``ceil(nslices / 2.) + 1`` etc.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0. , 0.4, 0.8, 0.2, 0.6])
{pstr}
"""
one_slice = TR / n_slices
space_to_time = (list(range(0, n_slices, 2)) +
list(range(1, n_slices, 2)))
return np.array(space_to_time) * one_slice
asc_alt_half = _derived_func('asc_alt_half', st_03142)
[docs]@_dec_stfunc
def st_41302(n_slices, TR):
"""Descend alternate, where alternation is by half the volume
Collect slice ``(n_slices - 1)`` then slice ``floor(nslices / 2.) - 1``
then slice ``(n_slices - 2)`` then slice ``floor(nslices / 2.) - 2`` etc.
For example, for 5 slices and a TR of 1:
>>> {name}(5, 1.)
array([ 0.6, 0.2, 0.8, 0.4, 0. ])
{pstr}
"""
return st_03142(n_slices, TR)[::-1]
desc_alt_half = _derived_func('desc_alt_half', st_41302)