import matplotlib.pyplot as plt
import numpy as np
import dcmri.ui as ui
import dcmri.lib as lib
import dcmri.liver as liver
import dcmri.sig as sig
import dcmri.utils as utils
[docs]
class Liver(ui.Model):
"""General model for liver tissue.
This is the standard interface for liver tissues with known input
function(s). For more detail see :ref:`liver-tissues`.
Args:
kinetics (str, optional): Tracer-kinetic model. See table
:ref:`table-liver-models` for options. Defaults to '2I-EC'.
stationary (str, optional): For intracellular tracers - stationarity
regime of the hepatocytes. The options are 'UE', 'E', 'U' or None.
For more detail see :ref:`liver-tissues`. Defaults to 'UE'.
sequence (str, optional): imaging sequence. Possible values are 'SS'
and 'SR'. Defaults to 'SS'.
config (str, optional): configuration option for using pre-defined
variable values from use cases. Currently, the available options
for this are 'TRISTAN-rat'. Defaults to None.
aif (array-like, optional): Signal-time curve in the blood of the
feeding artery. If *aif* is not provided, the arterial
blood concentration is *ca*. Defaults to None.
ca (array-like, optional): Blood concentration in the arterial
input. *ca* is ignored if *aif* is provided, but is required
otherwise. Defaults to None.
vif (array-like, optional): Signal-time curve in the blood of the
portal vein. If *vif* is not provided, the venous
blood concentration is *cv*. Defaults to None.
cv (array-like, optional): Blood concentration in the portal venous
input. *cv* is ignored if *vif* is provided, but is required
otherwise. Defaults to None.
t (array-like, optional): Time points of the arterial input function.
If *t* is not provided, the temporal sampling is uniform with
interval *dt*. Defaults to None.
dt (float, optional): Time interval between values of the arterial
input function. *dt* is ignored if *t* is provided. Defaults to 1.0.
free (dict, optional): Dictionary with free parameters and their
bounds. If not provided, a default set of free parameters is used.
Defaults to None.
params (dict, optional): values for the parameters of the tissue,
specified as keyword parameters. Defaults are used for any that are
not provided. See tables :ref:`Liver-parameters` and
:ref:`Liver-defaults` for a list of parameters and their
default values.
See Also:
`Tissue`
Example:
Fit a dual-inlet liver model:
.. plot::
:include-source:
:context: close-figs
>>> import matplotlib.pyplot as plt
>>> import dcmri as dc
Use `fake_liver` to generate synthetic test data:
>>> time, aif, vif, roi, gt = dc.fake_liver()
Build a tissue model and set the constants to match the experimental
conditions of the synthetic test data. Note the default model is the
dual-inlet model for extracellular agents (2I-EC). Since the
synthetic data are generated with an intracellular agent, the default
for the kinetic model needs to be overwritten:
>>> model = dc.Liver(
... kinetics = '2I-IC',
... aif = aif,
... vif = vif,
... dt = time[1],
... agent = 'gadoxetate',
... field_strength = 3.0,
... TR = 0.005,
... FA = 15,
... n0 = 10,
... R10 = 1/dc.T1(3.0,'liver'),
... R10a = 1/dc.T1(3.0, 'blood'),
... R10v = 1/dc.T1(3.0, 'blood'),
... )
Train the model on the ROI data:
>>> model.train(time, roi)
Plot the reconstructed signals (left) and concentrations (right) and
compare the concentrations against the noise-free ground truth. Since
the data are analysed with an exact model, and there are no other data
errors present, this should fior the data exactly.
>>> model.plot(time, roi, ref=gt)
Notes:
Table :ref:`Liver-parameters` lists the parameters that are relevant
in each regime. Table :ref:`Liver-defaults` list all possible
parameters and their default settings.
.. _Liver-parameters:
.. list-table:: **Liver parameters**
:widths: 20 30 30
:header-rows: 1
* - Parameters
- When to use
- Further detail
* - n0
- Always
- For estimating baseline signal
* - field_strength, agent, R10
- Always
- :ref:`relaxation-params`
* - R10a, B1corr_a
- When aif is provided
- :ref:`relaxation-params`, :ref:`params-per-sequence`
* - R10v, B1corr_v
- When vif is provided
- :ref:`relaxation-params`, :ref:`params-per-sequence`
* - S0, FA, TR, TS, B1corr
- Always
- :ref:`params-per-sequence`
* - TP, TC
- If **sequence** is 'SR'
- :ref:`params-per-sequence`
* - ve, Fp, fa, Ta, Tg, khe, khe_i, kh_f, Th, Th_i, Th_f.
- Depends on **kinetics** and **stationary**
- :ref:`table-liver-models`
.. _Liver-defaults:
.. list-table:: **Liver parameter defaults**
:widths: 5 10 10 10 10
:header-rows: 1
* - Parameter
- Type
- Value
- Bounds
- Free/Fixed
* - field_strength
- Injection
- 3
- [0, inf]
- Fixed
* - agent
- Injection
- 'gadoxetate'
- None
- Fixed
* - R10
- Relaxation
- 0.7
- [0, inf]
- Fixed
* - R10a
- Relaxation
- 0.7
- [0, inf]
- Fixed
* - R10v
- Relaxation
- 0.7
- [0, inf]
- Fixed
* - B1corr
- Sequence
- 1
- [0, inf]
- Fixed
* - B1corr_a
- Sequence
- 1
- [0, inf]
- Fixed
* - B1corr_v
- Sequence
- 1
- [0, inf]
- Fixed
* - FA
- Sequence
- 15
- [0, inf]
- Fixed
* - S0
- Sequence
- 1
- [0, inf]
- Fixed
* - TC
- Sequence
- 0.1
- [0, inf]
- Fixed
* - TP
- Sequence
- 0
- [0, inf]
- Fixed
* - TR
- Sequence
- 0.005
- [0, inf]
- Fixed
* - TS
- Sequence
- 0
- [0, inf]
- Fixed
* - H
- Kinetic
- 0.45
- [0, 1]
- Fixed
* - Te
- Kinetic
- 30
- [0.1, 60]
- Free
* - De
- Kinetic
- 0.85
- [0, 1]
- Free
* - ve
- Kinetic
- 0.3
- [0.01, 0.6]
- Free
* - Ta
- Kinetic
- 2
- [0, inf]
- Free
* - Tg
- Kinetic
- 10
- [0, inf]
- Free
* - Fp
- Kinetic
- 0.008
- [0, inf]
- Free
* - fa
- Kinetic
- 0.2
- [0, inf]
- Free
* - khe
- Kinetic
- 0.003
- [0, 0.1]
- Free
* - khe_i
- Kinetic
- 0.003
- [0, 0.1]
- Free
* - khe_f
- Kinetic
- 0.003
- [0, 0.1]
- Free
* - Th
- Kinetic
- 1800
- [600, 36000]
- Free
* - Th_i
- Kinetic
- 1800
- [600, 36000]
- Free
* - Th_f
- Kinetic
- 1800
- [600, 36000]
- Free
* - vol
- Kinetic
- 1000
- [0, 10000]
- Free
"""
def __init__(
self,
kinetics='2I-EC', stationary='UE', sequence='SS', config=None,
aif=None, ca=None, vif=None, cv=None, t=None, dt=0.5,
free=None, **params):
# Configuration
if config == 'TRISTAN-rat':
# Acquisition parameters
params['agent'] = 'gadoxetate'
# Kinetic paramaters
self.kinetics = '1I-IC-HF'
self.stationary = 'UE'
self.sequence = 'SS'
params['H'] = 0.418 # Cremer et al, J Cereb Blood Flow
# Metab 3, 254-256 (1983)
params['ve'] = 0.23
params['Fp'] = 0.022019 # mL/sec/cm3
# Fp = (1-H)*Fb, where Fb=2.27 mL/min/mL
# calculated from Table S2 in
# doi: 10.1021/acs.molpharmaceut.1c00206
free = {
'khe': [0, np.inf],
'Th': [0, np.inf],
}
# Tissue paramaters
params['R10'] = 1/lib.T1(params['field_strength'], 'liver'),
else:
self.kinetics = kinetics
self.stationary = stationary
self.sequence = sequence
self._check_config()
# Input function
self.aif = aif
self.ca = ca
self.vif = vif
self.cv = cv
self.t = t
self.dt = dt
# Set defaults
self._set_defaults(free=free, **params)
def _check_config(self):
if self.sequence not in ['SS', 'SR']:
raise ValueError(
'Sequence ' + str(self.sequence) + ' is not available.')
liver.params_liver(self.kinetics, self.stationary)
def _params(self):
return PARAMS
def _model_pars(self):
pars_sequence = {
'SR': ['S0', 'B1corr', 'FA', 'TR', 'TS', 'TC', 'TP'],
'SS': ['S0', 'B1corr', 'FA', 'TR', 'TS'],
}
pars = ['field_strength', 'agent']
pars += ['R10a', 'B1corr_a']
if self.kinetics[0] == '2':
pars += ['R10v', 'B1corr_v']
pars += pars_sequence[self.sequence]
pars += liver.params_liver(self.kinetics, self.stationary)
pars += ['vol']
pars += ['R10', 'n0']
return pars
def _par_values(self, kin=False, export=False):
if kin:
pars = liver.params_liver(self.kinetics, self.stationary)
return {par: getattr(self, par) for par in pars}
if export:
pars = self._par_values()
p0 = self._model_pars()
p1 = liver.params_liver(self.kinetics, self.stationary)
discard = set(p0) - set(p1)
return {p: pars[p] for p in pars if p not in discard}
pars = self._model_pars()
p = {par: getattr(self, par) for par in pars}
try:
p['Fa'] = p['Fp']*p['fa']
except KeyError:
pass
try:
p['Fv'] = p['Fp']*(1-p['fa'])
except KeyError:
pass
try:
p['Te'] = _div(p['ve'], p['Fp'])
except KeyError:
pass
try:
p['Th'] = np.mean([p['Th_i'], p['Th_f']])
except KeyError:
pass
try:
p['khe'] = np.mean([p['khe_i'], p['khe_f']])
except KeyError:
pass
try:
p['Kbh'] = _div(1, p['Th'])
except KeyError:
pass
try:
p['Khe'] = _div(p['khe'], p['ve'])
except KeyError:
pass
try:
p['kbh'] = _div(1-p['ve'], p['Th'])
except KeyError:
pass
try:
p['kbh_i'] = _div(1-p['ve'], p['Th_i'])
except KeyError:
pass
try:
p['kbh_f'] = _div(1-p['ve'], p['Th_f'])
except KeyError:
pass
try:
p['E'] = p['khe']/(p['khe']+p['Fp'])
except KeyError:
try:
p['E'] = p['khe']/(p['khe']+self.Fp)
except:
pass
try:
p['Ktrans'] = (1-p['E'])*p['khe']
except KeyError:
pass
if p['vol'] is not None:
try:
p['CL'] = p['khe']*p['vol']
except KeyError:
pass
return p
[docs]
def time(self):
"""Array of time points.
Returns:
np.ndarray: time points in seconds.
"""
if self.t is None:
if self.aif is None:
return self.dt*np.arange(np.size(self.ca))
else:
return self.dt*np.arange(np.size(self.aif))
else:
return self.t
def _check_ca(self):
if self.ca is None:
if self.aif is None:
raise ValueError(
"Either aif or ca must be provided "
"to predict signal data.")
else:
r1 = lib.relaxivity(self.field_strength, 'blood', self.agent)
if self.sequence == 'SR':
self.ca = sig.conc_src(
self.aif, self.TC, 1 / self.R10a, r1, self.n0)
elif self.sequence == 'SS':
self.ca = sig.conc_ss(
self.aif, self.TR, self.B1corr_a * self.FA,
1 / self.R10a, r1, self.n0)
def _check_cv(self):
if self.kinetics[0] == '1':
return
if self.cv is None:
if self.vif is None:
if self.kinetics[1]=='2':
raise ValueError(
"For a dual-inlet model, either vif or cv must be "
"provided.")
else:
r1 = lib.relaxivity(self.field_strength, 'blood', self.agent)
if self.sequence == 'SR':
self.cv = sig.conc_src(
self.vif, self.TC, 1 / self.R10v, r1, self.n0)
elif self.sequence == 'SS':
self.cv = sig.conc_ss(
self.vif, self.TR, self.B1corr_v * self.FA,
1 / self.R10v, r1, self.n0)
[docs]
def conc(self, sum=True):
"""Tissue concentrations
Args:
sum (bool, optional): If True, returns the total concentrations.
If False, returns concentration in both compartments separately.
Defaults to True.
Returns:
numpy.ndarray: Concentration in M
"""
self._check_ca()
self._check_cv()
pars = self._par_values(kin=True)
return liver.conc_liver(
self.ca, t=self.t, dt=self.dt, sum=sum, cv=self.cv, **pars)
[docs]
def relax(self):
"""Tissue relaxation rate
Returns:
numpy.ndarray: Relaxation rate in 1/sec
"""
r1 = lib.relaxivity(self.field_strength, 'blood', self.agent)
C = self.conc(sum=False)
if 'IC' in self.kinetics:
r1h = lib.relaxivity(self.field_strength, 'hepatocytes',
self.agent)
return self.R10 + r1*C[0, :] + r1h*C[1, :]
else:
return self.R10 + r1*C
[docs]
def signal(self) -> np.ndarray:
"""Pseudocontinuous signal
Returns:
np.ndarray: the signal as a 1D array.
"""
R1 = self.relax()
if self.sequence == 'SR':
return sig.signal_spgr(self.S0, R1, self.TC, self.TR,
self.B1corr * self.FA)
else:
return sig.signal_ss(self.S0, R1, self.TR,
self.B1corr * self.FA)
[docs]
def predict(self, time: np.ndarray):
"""Predict the data at specific time points
Args:
time (array-like): Array of time points.
Returns:
np.ndarray: Array of predicted data for each element of *time*.
"""
t = self.time()
if np.amax(time) > np.amax(t):
raise ValueError(
"The acquisition window is longer than the duration "
"of the AIF. The largest time point that can be "
"predicted is " + str(np.amax(t) / 60) + "min.")
sig = self.signal()
return utils.sample(time, t, sig, self.TS)
[docs]
def train(self, time, signal, **kwargs):
"""Train the free parameters
Args:
time (array-like): Array with time points
signal (array-like): Array with signal values
kwargs: any keyword parameters accepted by
`scipy.optimize.curve_fit`, except for bounds.
Returns:
Liver: A reference to the model instance.
"""
if self.sequence == 'SR':
Sref = sig.signal_spgr(
1, self.R10, self.TC, self.TR, self.B1corr * self.FA)
else:
Sref = sig.signal_ss(1, self.R10, self.TR,
self.B1corr * self.FA)
self.S0 = np.mean(signal[:self.n0]) / Sref if Sref > 0 else 0
return ui.train(self, time, signal, **kwargs)
[docs]
def plot(self,
time: np.ndarray,
signal: np.ndarray,
ref=None, xlim=None, fname=None, show=True):
C = self.conc(sum=True)
t = self.time()
if xlim is None:
xlim = [np.amin(time), np.amax(time)]
fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(12, 5))
ax0.set_title('Prediction of the MRI signals.')
ax0.plot(time/60, signal, marker='o', linestyle='None',
color='cornflowerblue', label='Data')
ax0.plot(t/60, self.predict(t), linestyle='-',
linewidth=3.0, color='darkblue', label='Prediction')
ax0.set(xlabel='Time (min)', ylabel='MRI signal (a.u.)',
xlim=np.array(xlim)/60)
ax0.legend()
ax1.set_title('Reconstruction of concentrations.')
if ref is not None:
ax1.plot(ref['t']/60, 1000*ref['C'], marker='o', linestyle='None',
color='cornflowerblue', label='Tissue ground truth')
ax1.plot(ref['t']/60, 1000*ref['cb'], marker='o', linestyle='None',
color='lightcoral', label='Arterial blood')
ax1.plot(t/60, 1000*C, linestyle='-', linewidth=3.0,
color='darkblue', label='Tissue prediction')
ax1.plot(t/60, 1000*self.ca, linestyle='-', linewidth=3.0,
color='darkred', label='Arterial blood measurement')
if self.cv is not None:
if ref is not None:
ax1.plot(ref['t']/60, 1000*ref['cv'], marker='o', linestyle='None',
color='orchid', label='Portal venous blood')
ax1.plot(t/60, 1000*self.cv, linestyle='-', linewidth=3.0,
color='purple', label='Portal-venous blood measurement')
ax1.set(xlabel='Time (min)', ylabel='Concentration (mM)',
xlim=np.array(xlim)/60)
ax1.legend()
if fname is not None:
plt.savefig(fname=fname)
if show:
plt.show()
else:
plt.close()
PARAMS = {
'field_strength': {
'init': 3.0,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Magnetic field strength',
'unit': 'T',
},
'agent': {
'init': 'gadoxetate',
'default_free': False,
'bounds': None,
'name': 'Contrast agent',
'unit': None,
},
'R10a': {
'init': 0.7,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Arterial precontrast R1',
'unit': 'Hz',
'pixel_par': False,
},
'B1corr_a': {
'init': 1,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Arterial B1-correction factor',
'unit': '',
'pixel_par': False,
},
'R10v': {
'init': 0.7,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Portal venous precontrast R1',
'unit': 'Hz',
'pixel_par': False,
},
'B1corr_v': {
'init': 1,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Portal venous B1-correction factor',
'unit': '',
'pixel_par': False,
},
'B1corr': {
'init': 1,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Tissue B1-correction factor',
'unit': '',
'pixel_par': True,
},
'FA': {
'init': 15,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Flip angle',
'unit': 'deg',
'pixel_par': False,
},
'TR': {
'init': 0.005,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Repetition time',
'unit': 'sec',
'pixel_par': False,
},
'TC': {
'init': 0.2,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Time to k-space center',
'unit': 'sec',
'pixel_par': False,
},
'TP': {
'init': 0.05,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Preparation delay',
'unit': 'sec',
'pixel_par': False,
},
'TS': {
'init': 0,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Sampling time',
'unit': 'sec',
'pixel_par': False,
},
'R10': {
'init': 0.7,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Tissue precontrast R1',
'unit': 'Hz',
'pixel_par': True,
},
'S0': {
'init': 1.0,
'default_free': False,
'bounds': [0, np.inf],
'name': 'Signal scaling factor',
'unit': 'a.u.',
'pixel_par': True,
},
'n0': {
'init': 1,
'default_free': False,
'bounds': None,
'name': 'Number of precontrast acquisitions',
'unit': '',
'pixel_par': False,
},
'H': {
'init': 0.45,
'default_free': False,
'bounds': [0, 1],
'name': 'Hematocrit',
'unit': '',
'pixel_par': False,
},
'Te': {
'init': 30.0,
'default_free': True,
'bounds': [0.1, 60],
'name': 'Extracellular mean transit time',
'unit': 'sec',
'pixel_par': True,
},
'De': {
'init': 0.85,
'default_free': True,
'bounds': [0, 1],
'name': 'Extracellular dispersion',
'unit': '',
'pixel_par': True,
},
've': {
'init': 0.3,
'default_free': True,
'bounds': [0.01, 0.6],
'name': 'Liver extracellular volume fraction',
'unit': 'mL/cm3',
'pixel_par': True,
},
'Ta': {
'init': 2,
'default_free': True,
'bounds': [0, np.inf],
'name': 'Arterial mean transit time',
'unit': 'sec',
'pixel_par': True,
},
'Tg': {
'init': 10,
'default_free': True,
'bounds': [0, np.inf],
'name': 'Gut mean transit time',
'unit': 'sec',
'pixel_par': True,
},
'Fp': {
'init': 0.008,
'default_free': True,
'bounds': [0, np.inf],
'name': 'Liver plasma flow',
'unit': 'mL/sec/cm3',
'pixel_par': True,
},
'fa': {
'init': 0.2,
'default_free': True,
'bounds': [0, 1],
'name': 'Arterial flow fraction',
'unit': '',
'pixel_par': True,
},
'khe': {
'init': 0.003,
'default_free': True,
'bounds': [0.0, 0.1],
'name': 'Hepatocellular uptake rate',
'unit': 'mL/sec/cm3',
'pixel_par': True,
},
'khe_i': {
'init': 0.003,
'default_free': True,
'bounds': [0.0, 0.1],
'name': 'Initial hepatocellular uptake rate',
'unit': 'mL/sec/cm3',
'pixel_par': True,
},
'khe_f': {
'init': 0.003,
'default_free': True,
'bounds': [0.0, 0.1],
'name': 'Final hepatocellular uptake rate',
'unit': 'mL/sec/cm3',
'pixel_par': True,
},
'Th': {
'init': 30*60,
'default_free': True,
'bounds': [10*60, 10*60*60],
'name': 'Hepatocellular mean transit time',
'unit': 'sec',
'pixel_par': True,
},
'Th_i': {
'init': 30*60,
'default_free': True,
'bounds': [10*60, 10*60*60],
'name': 'Initial hepatocellular mean transit time',
'unit': 'sec',
'pixel_par': True,
},
'Th_f': {
'init': 30*60,
'default_free': True,
'bounds': [10*60, 10*60*60],
'name': 'Final hepatocellular mean transit time',
'unit': 'sec',
'pixel_par': True,
},
'vol': {
'init': None,
'default_free': False,
'bounds': [0, 10000],
'name': 'Liver volume',
'unit': 'cm3',
'pixel_par': False,
},
# Derived parameters
'Fa': {
'name': 'Arterial plasma flow',
'unit': 'mL/sec/cm3',
},
'Fv': {
'name': 'Venous plasma flow',
'unit': 'mL/sec/cm3',
},
'kbh': {
'name': 'Biliary excretion rate',
'unit': 'mL/sec/cm3',
},
'kbh_i': {
'name': 'Initial biliary excretion rate',
'unit': 'mL/sec/cm3',
},
'kbh_f': {
'name': 'Final biliary excretion rate',
'unit': 'mL/sec/cm3',
},
'Kbh': {
'name': 'Biliary tissue excretion rate',
'unit': 'mL/sec/cm3',
},
'Khe': {
'name': 'Hepatocellular tissue uptake rate',
'unit': 'mL/sec/cm3',
},
'E': {
'name': 'Liver extraction fraction',
'unit': '',
},
'Ktrans': {
'name': 'Hepatic plasma clearance',
'unit': 'mL/sec/cm3',
},
'CL': {
'name': 'Liver blood clearance',
'unit': 'mL/sec',
},
}
def _div(a, b):
with np.errstate(divide='ignore', invalid='ignore'):
return np.divide(a, b)