Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP, ENH] Add ability to read surface .gii files #22

Merged
merged 21 commits into from
May 10, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
628bf64
[STY] Mostly stylistic, a few enhancements...
rmarkello Apr 27, 2018
cdc8cf0
[FIX] Screwed up nibabel import
rmarkello Apr 27, 2018
a94692b
[RF] Breaks everything; working to add gifti support
rmarkello Apr 27, 2018
4e735bb
[RF] Still very broken fixing fitmodels_direct
rmarkello May 4, 2018
61b96ae
Merge remote-tracking branch 'upstream/master' into gifti
rmarkello May 6, 2018
10ef6d7
[RF] niwrite --> filewrite, better gifti support
rmarkello May 7, 2018
9785e42
Update csstepdata format as JSON
emdupre May 7, 2018
a4d9f5b
Remove currently unused imports for linting
emdupre May 7, 2018
e9994cd
Address review comments
emdupre May 7, 2018
b210d2b
Merge pull request #1 from emdupre/master
rmarkello May 7, 2018
047242d
Patch errors
emdupre May 7, 2018
07ebb15
Merge pull request #2 from emdupre/master
rmarkello May 7, 2018
bb7eb17
[DOC] Doc-strings galore! Very basic but a start
rmarkello May 8, 2018
0e9e938
[RF] Addressed reviews and minor updates
rmarkello May 9, 2018
a784e86
[FIX] Needed order=F for when zcat data provided
rmarkello May 9, 2018
4d76812
[FIX] Allow N-dimensional arrays in `utils.andb`
rmarkello May 9, 2018
7f70268
[FIX] Unmask not retaining dtype
rmarkello May 9, 2018
b8b5957
[FIX] Bugs in selcomps for gifti/nifti
rmarkello May 9, 2018
ddafa7c
[FIX] Minor updates to doc-strings + names
rmarkello May 10, 2018
f9810ad
[FIX] float32 / float64 bug in `fitmodels_direct()`
rmarkello May 10, 2018
ef6c34d
[FIX] Address review comments for #22
rmarkello May 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
data/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
4 changes: 3 additions & 1 deletion tedana/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
def get_parser():
"""
Parses command line inputs for tedana

Returns
-------
parser.parse_args() : argparse dict
"""

parser = argparse.ArgumentParser()
parser.add_argument('-d',
dest='data',
Expand All @@ -18,7 +20,7 @@ def get_parser():
parser.add_argument('-e',
dest='tes',
nargs='+',
help='Echo times (in ms) ex: 15,39,63',
help='Echo times (in ms) ex: 15.0 39.0 63.0',
required=True)
parser.add_argument('--mix',
dest='mixm',
Expand Down
4 changes: 2 additions & 2 deletions tedana/interfaces/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# emacs: -*- mode: python-mode; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 et:

from .t2smap import (t2sadmap, optcom)
from .t2smap import (t2sadmap, make_optcom)

__all__ = [
't2sadmap', 'optcom']
't2sadmap', 'make_optcom']
237 changes: 120 additions & 117 deletions tedana/interfaces/t2smap.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"""
"""
import numpy as np
import nibabel as nib
from tedana.utils import (niwrite, cat2echos, makeadmask, unmask, fmask)
from tedana.utils import (filewrite, load_data, makeadmask, unmask, fmask)

import logging
logging.basicConfig(format='[%(levelname)s]: %(message)s', level=logging.INFO)
lgr = logging.getLogger(__name__)


def fit(data, mask, tes, masksum, start_echo):
"""
Fit voxel- and timepoint-wise monoexponential decay models to estimate
T2* and S0 timeseries.
"""
nx, ny, nz, n_echoes, n_trs = data.shape
nx, ny, nz, n_echos, n_trs = data.shape
echodata = fmask(data, mask)
tes = np.array(tes)

Expand All @@ -20,11 +21,11 @@ def fit(data, mask, tes, masksum, start_echo):
s0vaf_ts = np.zeros([nx, ny, nz, n_trs])

for vol in range(echodata.shape[-1]):
t2ss = np.zeros([nx, ny, nz, n_echoes - 1])
t2ss = np.zeros([nx, ny, nz, n_echos - 1])
s0vs = t2ss.copy()
# Fit monoexponential decay first for first echo only,
# then first two echoes, etc.
for i_echo in range(start_echo, n_echoes + 1):
for i_echo in range(start_echo, n_echos + 1):
B = np.abs(echodata[:, :i_echo, vol]) + 1
B = np.log(B).transpose()
neg_tes = -1 * tes[:i_echo]
Expand All @@ -46,7 +47,7 @@ def fit(data, mask, tes, masksum, start_echo):

# Limited T2* and S0 maps
fl = np.zeros([nx, ny, nz, len(tes)-1], bool)
for i_echo in range(n_echoes - 1):
for i_echo in range(n_echos - 1):
fl_ = np.squeeze(fl[:, :, :, i_echo])
fl_[masksum == i_echo + 2] = True
fl[:, :, :, i_echo] = fl_
Expand All @@ -67,130 +68,133 @@ def fit(data, mask, tes, masksum, start_echo):
return t2sa_ts, s0va_ts, t2saf_ts, s0vaf_ts


def t2sadmap(data, mask, tes, masksum, start_echo):
def t2sadmap(data, tes, mask, masksum, start_echo):
"""
Fit voxelwise monoexponential decay models to estimate T2* and S0 maps.
t2sadmap(data,mask,tes,masksum)

Input:
data : :obj:`numpy.ndarray`
Concatenated BOLD data. Has shape (nx, ny, nz, n_echoes, n_trs)
mask : :obj:`numpy.ndarray`
Brain mask in 3D array. Has shape (nx, ny, nz)
tes : :obj:`numpy.ndarray`
Array of TEs, in milliseconds.
masksum :
Parameters
----------
data : (S x E x T) array_like
Multi-echo data array, where `S` is samples, `E` is echos, and `T` is
time
tes : (E, ) list
Echo times
mask : (S, ) array_like
Boolean array indicating samples that are consistently (i.e., across
time AND echoes) non-zero
masksum : (S, ) array_like
Valued array indicating number of echos that have sufficient signal in
given sample
start_echo : int
First echo to consider

Returns
-------
t2sa : (S x E x T) np.ndarray
Limited T2* map
s0va : (S x E x T) np.ndarray
Limited S0 map
t2ss : (S x E x T) np.ndarray
???
s0vs : (S x E x T) np.ndarray
???
t2saf : (S x E x T) np.ndarray
Full T2* map
s0vaf : (S x E x T) np.ndarray
Full S0 map
"""
nx, ny, nz, n_echoes, n_trs = data.shape
echodata = fmask(data, mask)
n_voxels = echodata.shape[0]
tes = np.array(tes)
t2ss = np.zeros([nx, ny, nz, n_echoes - 1])
s0vs = t2ss.copy()

# Fit monoexponential decay first for first echo only,
# then first two echoes, etc.
for i_echo in range(start_echo, n_echoes + 1):
# Do Log Linear fit
B = np.reshape(np.abs(echodata[:, :i_echo, :]) + 1,
(n_voxels, i_echo*n_trs)).transpose()
B = np.log(B)
neg_tes = -1 * tes[:i_echo]

# First row is constant, second is TEs for decay curve
# Independent variables for least-squares model
x = np.array([np.ones(i_echo), neg_tes])
X = np.tile(x, (1, n_trs))
X = np.sort(X)[:, ::-1].transpose()

beta, _, _, _ = np.linalg.lstsq(X, B)
t2s = 1. / beta[1, :].transpose()
s0 = np.exp(beta[0, :]).transpose()

t2s[np.isinf(t2s)] = 500.
s0[np.isnan(s0)] = 0.

t2ss[:, :, :, i_echo-2] = np.squeeze(unmask(t2s, mask))
s0vs[:, :, :, i_echo-2] = np.squeeze(unmask(s0, mask))

# Limited T2* and S0 maps
fl = np.zeros([nx, ny, nz, len(tes)-1], bool)
for i_echo in range(n_echoes - 1):
fl_ = np.squeeze(fl[:, :, :, i_echo])
fl_[masksum == i_echo + 2] = True
fl[:, :, :, i_echo] = fl_
t2sa = np.squeeze(unmask(t2ss[fl], masksum > 1))
s0va = np.squeeze(unmask(s0vs[fl], masksum > 1))

# Full T2* maps with S0 estimation errors
t2saf = t2sa.copy()
s0vaf = s0va.copy()

n_samp, n_echos, n_vols = data.shape
data = data[mask]
t2ss, s0vs = np.zeros([n_samp, n_echos - 1]), np.zeros([n_samp, n_echos - 1])

for echo in range(start_echo, n_echos + 1):
# perform log linear fit of echo times against MR signal
# make DV matrix: samples x (time series * echos)
B = np.log((np.abs(data[:, :echo, :]) + 1).reshape(len(data), -1).T)
# make IV matrix: intercept/TEs x (time series * echos)
x = np.column_stack([np.ones(echo), [-te for te in tes[:echo]]])
X = np.repeat(x, n_vols, axis=0)

beta, res, rank, sing = np.linalg.lstsq(X, B)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason these variables were added back in ? Previously it was
beta, _, _, _ = np.linalg.lstsq(X, B)

Copy link
Member Author

Choose a reason for hiding this comment

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

No! And indeed, I think a more parsimonious solution than either of the above would probably be:

beta = np.linalg.lstsq(X, B)[0]

Not sure my rationale here, so I'll change it to reflect this.

t2s = 1. / beta[1, :].T
s0 = np.exp(beta[0, :]).T

t2s[np.isinf(t2s)] = 500. # why 500?
s0[np.isnan(s0)] = 0. # why 0?

t2ss[..., echo - 2] = np.squeeze(unmask(t2s, mask))
s0vs[..., echo - 2] = np.squeeze(unmask(s0, mask))

# create limited T2* and S0 maps
fl = np.zeros([n_samp, len(tes) - 1], dtype=bool)
for echo in range(n_echos - 1):
fl_ = np.squeeze(fl[..., echo])
fl_[masksum == echo + 2] = True
fl[..., echo] = fl_
t2sa, s0va = unmask(t2ss[fl], masksum > 1), unmask(s0vs[fl], masksum > 1)
# t2sa[masksum > 1], s0va[masksum > 1] = t2ss[fl], s0vs[fl]

# create full T2* maps with S0 estimation errors
t2saf, s0vaf = t2sa.copy(), s0va.copy()
t2saf[masksum == 1] = t2ss[masksum == 1, 0]
s0vaf[masksum == 1] = s0vs[masksum == 1, 0]

return t2sa, s0va, t2ss, s0vs, t2saf, s0vaf


def optcom(data, t2, tes, mask, combmode, useG=False):
def make_optcom(data, t2s, tes, mask, combmode):
"""
Optimally combine BOLD data across TEs.

out = optcom(data,t2s)
out = make_optcom(data,t2s)

Parameters
----------
data : :obj:`numpy.ndarray`
Concatenated BOLD data. Has shape (nx, ny, nz, n_echoes, n_trs)
t2 : :obj:`numpy.ndarray`
3D map of estimated T2* values. Has shape (nx, ny, nz)
data : (S x E x T) :obj:`numpy.ndarray`
Concatenated BOLD data.
t2 : (S,) :obj:`numpy.ndarray`
Estimated T2* values.
tes : :obj:`numpy.ndarray`
Array of TEs, in seconds.
mask : :obj:`numpy.ndarray`
Brain mask in 3D array. Has shape (nx, ny, nz)
mask : (S,) :obj:`numpy.ndarray`
Brain mask in 3D array.
combmode : :obj:`str`
How to combine data. Either 'ste' or 't2s'.
useG : :obj:`bool`, optional
Use G. Default is False.

Returns
-------
out : :obj:`numpy.ndarray`
Optimally combined data. Has shape (nx, ny, nz, n_trs)
fout : (S x T) :obj:`numpy.ndarray`
Optimally combined data.
"""
_, _, _, _, n_trs = data.shape

if useG:
fdat = fmask(data, mask)
ft2s = fmask(t2, mask)
else:
fdat = fmask(data, mask)
ft2s = fmask(t2, mask)

tes = np.array(tes)
tes = tes[np.newaxis, :]
n_samp, n_echos, n_vols = data.shape
mdata = data[mask]
tes = np.array(tes)[np.newaxis] # (1 x E) array_like

if len(t2.shape) == 3:
print('Optimally combining with voxel-wise T2 estimates')
ft2s = ft2s[:, np.newaxis]
if t2s.ndim == 1:
lgr.info('Optimally combining with voxel-wise T2 estimates')
ft2s = t2s[mask, np.newaxis]
else:
print('Optimally combining with voxel- and volume-wise T2 estimates')
ft2s = ft2s[:, :, np.newaxis]
lgr.info('Optimally combining with voxel- and volume-wise T2 estimates')
ft2s = t2s[mask, :, np.newaxis]

if combmode == 'ste':
alpha = fdat.mean(-1) * tes
alpha = mdata.mean(axis=-1) * tes
else:
alpha = tes * np.exp(-tes / ft2s)

if len(t2.shape) == 3:
alpha = np.tile(alpha[:, :, np.newaxis], (1, 1, n_trs))
if t2s.ndim == 1:
alpha = np.tile(alpha[:, :, np.newaxis], (1, 1, n_vols))
else:
alpha = np.swapaxes(alpha, 1, 2)
ax0_idx, ax2_idx = np.where(np.all(alpha == 0, axis=1))
alpha[ax0_idx, :, ax2_idx] = 1.

fout = np.average(fdat, axis=1, weights=alpha)
out = unmask(fout, mask)
return out
fout = np.average(mdata, axis=1, weights=alpha)
fout = unmask(fout, mask)

return fout


def main(options):
Expand All @@ -208,28 +212,27 @@ def main(options):
suf = '_%s' % str(options.label)
else:
suf = ''
tes, data, combmode = options.tes, options.data, options.combmode

tes = [float(te) for te in tes]
n_echos = len(tes)

catd = load_data(data, n_echos=n_echos)
n_samp, n_echos, n_trs = catd.shape

ref_img = data[0] if isinstance(data, list) else data

tes = [float(te) for te in options.tes]
n_echoes = len(tes)
catim = nib.load(options.data[0])
head = catim.get_header()
head.extensions = []
head.set_sform(head.get_sform(), code=1)
aff = catim.get_affine()
catd = cat2echos(catim.get_data(), n_echoes)
nx, ny, nz, n_echoes, n_trs = catd.shape

print("++ Computing Mask")
lgr.info("++ Computing Mask")
mask, masksum = makeadmask(catd, minimum=False, getsum=True)
niwrite(masksum, aff, 'masksum%s.nii' % suf)
filewrite(masksum, 'masksum%s' % suf, ref_img, copy_header=False)

print("++ Computing Adaptive T2* map")
t2s, s0, t2ss, s0vs, t2saf, s0vaf = t2sadmap(catd, mask, tes, masksum, 2)
niwrite(t2ss, aff, 't2ss%s.nii' % suf)
niwrite(s0vs, aff, 's0vs%s.nii' % suf)
lgr.info("++ Computing Adaptive T2* map")
t2s, s0, t2ss, s0vs, t2saf, s0vaf = t2sadmap(catd, tes, mask, masksum, 2)
filewrite(t2ss, 't2ss%s' % suf, ref_img, copy_header=False)
filewrite(s0vs, 's0vs%s' % suf, ref_img, copy_header=False)

print("++ Computing optimal combination")
tsoc = np.array(optcom(catd, t2s, tes, mask, options.combmode),
lgr.info("++ Computing optimal combination")
tsoc = np.array(make_optcom(catd, t2s, tes, mask, combmode),
dtype=float)

# Clean up numerical errors
Expand All @@ -241,7 +244,7 @@ def main(options):
t2s[t2s < 0] = 0
t2sm[t2sm < 0] = 0

niwrite(tsoc, aff, 'ocv%s.nii' % suf)
niwrite(s0, aff, 's0v%s.nii' % suf)
niwrite(t2s, aff, 't2sv%s.nii' % suf)
niwrite(t2sm, aff, 't2svm%s.nii' % suf)
filewrite(tsoc, 'ocv%s' % suf, ref_img, copy_header=False)
filewrite(s0, 's0v%s' % suf, ref_img, copy_header=False)
filewrite(t2s, 't2sv%s' % suf, ref_img, copy_header=False)
filewrite(t2sm, 't2svm%s' % suf, ref_img, copy_header=False)
Loading