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

Develop #6

Merged
merged 5 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Created 28 September 2020
- Signal models: spoiled gradient echo
- Water exchange models: FXL, NXL, NXL_be
- T1 fitting using variable flip angle method, IR-SPGR and DESPOT1-HIFI
- T2(*) fitting for multi-TE acquisitions

### Not yet implemented/limitations:
- Additional pharmacokinetic models (add by inheriting from PkModel class)
Expand Down
Binary file added demo/T2s_data/mag4D.nii
Binary file not shown.
2 changes: 1 addition & 1 deletion demo/demo_fit_dce.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -603,4 +603,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
221 changes: 149 additions & 72 deletions demo/demo_fit_t1.ipynb

Large diffs are not rendered by default.

232 changes: 232 additions & 0 deletions demo/demo_fit_t2s.ipynb

Large diffs are not rendered by default.

118 changes: 118 additions & 0 deletions demo/demo_imaging_utilities.ipynb

Large diffs are not rendered by default.

167 changes: 167 additions & 0 deletions src/t2star_fit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""Functions to fit MRI SPGR signal to obtain T2* or T2.

Created 17 October 2023
@authors: Michael Thrippleton
@email: [email protected]
@institution: University of Edinburgh, UK

Classes:
MultiEchoT2sLinear
MultiEchoT2sNonLinear

Functions:
multiecho_signal: get signal
"""

import numpy as np
from scipy.optimize import least_squares
from fitting import Fitter


class MultiEchoT2sLinear(Fitter):
"""Linear multi-echo T2* estimation.

Subclass of Fitter.
"""

def __init__(self, te, min_signal=0.):
"""

Args:
te (ndarray): 1D array containing the echo times (s)
min_signal (float): exclude echoes where signal is below this
value. Defaults to zero.

"""
self.te = te
self.min_signal = min_signal

def output_info(self):
"""Get output info. Overrides superclass method.
"""
return ('s0', False), ('t2s', False)

def proc(self, s):
"""Estimate T2*. Overrides superclass method.

Args:
s (ndarray): 1D array containing the signals

Returns:
tuple: (s0, t1)
s0 (float): signal without T2* decay
t2s (float): T2* (s)

"""
if any(np.isnan(s)):
raise ArithmeticError(
f'Unable to calculate T2s: nan signal received.')

te_mask = s > self.min_signal
if np.sum(te_mask) < 2:
raise ArithmeticError(
f'Unable to calculate T2s: fewer than 2 included TE values.')

y = np.log(s[te_mask])
x = self.te[te_mask]
A = np.stack([x, np.ones(x.shape)], axis=1)
slope, intercept = np.linalg.lstsq(A, y, rcond=None)[0]

if (intercept < 0) or (0. < slope):
raise ArithmeticError('T2s estimation failed.')

t2s, s0 = -1. / slope, np.exp(intercept)

return s0, t2s


class MultiEchoT2sNonLinear(Fitter):
"""Non-linear multi-echo T2* estimation.

Subclass of Fitter.
"""

def __init__(self, te, min_signal=0.):
"""

Args:
te (ndarray): 1D array containing the echo times (s)
min_signal (float): exclude echoes where signal is below this
value. Defaults to zero.

"""
self.te = te
self.min_signal = min_signal
self.linear_fitter = MultiEchoT2sLinear(te, min_signal)

def output_info(self):
"""Get output info. Overrides superclass method.
"""
return ('s0', False), ('t2s', False)

def proc(self, s):
"""Estimate T2*. Overrides superclass method.

Args:
s (ndarray): 1D array containing the signals

Returns:
tuple: (s0, t1)
s0 (float): signal without T2* decay
t2s (float): T2* (s)

"""
if any(np.isnan(s)):
raise ArithmeticError(
f'Unable to calculate T2s: nan signal received.')

te_mask = s > self.min_signal
if np.sum(te_mask) < 2:
raise ArithmeticError(
f'Unable to calculate T2s: fewer than 2 included TE values.')

# use linear fit to obtain initial guess, otherwise set arbitrary values
try:
x0 = np.array(self.linear_fitter.proc(s))
except ArithmeticError:
x0 = np.array([s[0], 10e-3])

s = s[te_mask]
te = self.te[te_mask]

result = least_squares(self.__residuals, x0, args=(s, te), bounds=(
(1e-8, 1e-8), (np.inf, np.inf)), method='trf', x_scale=x0)
if result.success is False:
raise ArithmeticError(f'Unable to fit multiecho data:'
f' {result.message}')

s0, t2s = result.x
return s0, t2s

@staticmethod
def __residuals(x, s, te):
s0, t2s = x
s_est = multiecho_signal(s0, t2s, te)
return s - s_est


def multiecho_signal(s0, t2s, te):
"""Return signal for multiple echoes / echo times.

Parameters
----------
s0 : float
Signal without T2/T2* decay.
t2s : float
T2*/T2 value (s).
te : float
TE value (s).

Returns
-------
s : float
Signal.
"""
s = s0 * np.exp(-te/t2s)

return s