Source code for dcmri.ui_tissue


from copy import deepcopy

import matplotlib.pyplot as plt
from matplotlib.animation import ArtistAnimation
import numpy as np

import dcmri.ui as ui
import dcmri.sig as sig
import dcmri.rel as rel
import dcmri.tissue as tissue
import dcmri.utils as utils


[docs] class TissueArray(ui.ArrayModel): """Pixel-based vascular-interstitial tissue. This is the most common tissue type as found in for instance brain, cancer, lung, muscle, prostate, skin, and more. For more detail see :ref:`two-site-exchange`. Usage of this class is mostly identical to `dcmri.Tissue`. Args: shape (array-like): shape of the tissue array in dimensions. Any number of dimensions is allowed but the last must be time. kinetics (str, optional): Tracer-kinetic model. Possible values are '2CX', '2CU', 'HF', 'HFU', 'NX', 'FX', 'WV', 'U'. Defaults to 'HF'. water_exchange (str, optional): Water exchange regime, Any combination of two of the letters 'F', 'N', 'R' is allowed. Defaults to 'FF'. sequence (str, optional): imaging sequence. Possible values are 'SS' and 'SR'. Defaults to 'SS'. 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. 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. parallel (bool, optional): If True, computations are parallelized. Defaults to False. verbose (int, optional): verbosity of the computation. With verbose=0, no feedback is given; with verbose=1, a status bar is shown. Defaults to 0. params (dict, optional): values for the parameters of the tissue, specified as keyword parameters. Defaults are used for any that are not provided. The parameters are the same as in `dcmri.Tissue`. See Also: `Tissue` Example: Fit a coarse image of the brain using a 2-compartment exchange model. .. plot:: :include-source: :context: close-figs >>> import numpy as np >>> import dcmri as dc Use `fake_brain` to generate synthetic test data: >>> n=8 >>> time, signal, aif, gt = dc.fake_brain(n) Build a tissue array and set the constants to match the experimental conditions of the synthetic test data: >>> shape = (n,n) >>> tissue = dc.TissueArray( ... shape, ... kinetics = '2CX', ... aif = aif, ... dt = time[1], ... r1 = dc.relaxivity(3, 'blood', 'gadodiamide'), ... TR = 0.005, ... FA = 15, ... R10a = 1/dc.T1(3.0,'blood'), ... R10 = 1/gt['T1'], ... n0 = 10, ... ) Train the tissue on the data: >>> tissue.train(time, signal) Plot the reconstructed maps, along with their standard deviations and the ground truth for reference: >>> tissue.plot(time, signal, ref=gt) """ def __init__(self, shape, kinetics='HF', water_exchange='FF', sequence='SS', aif=None, ca=None, t=None, dt=1.0, free=None, parallel=False, verbose=0, **params): # Array model params self.shape = shape self.parallel = parallel self.verbose = verbose # Define model self.kinetics = kinetics self.water_exchange = water_exchange self.sequence = sequence _check_config(self) # Input function self.aif = aif self.ca = ca self.t = t self.dt = dt # Model parameters model_pars = _model_pars(kinetics, water_exchange, sequence) for p in model_pars: if PARAMS[p]['pixel_par']: setattr(self, p, np.full(shape, PARAMS[p]['init'])) else: setattr(self, p, PARAMS[p]['init']) self.free = { p: PARAMS[p]['bounds'] for p in model_pars if ( PARAMS[p]['default_free'] and PARAMS[p]['pixel_par']) } # overide defaults self._set_defaults(free=free, **params) # sdevs for par in self.free: setattr(self, 'sdev_' + par, np.zeros(shape).astype(np.float32)) def _params(self, p): return PARAMS[p] def _par_values(self, *args, **kwargs): return _par_values(self, *args, **kwargs) def _pix(self, x): pars = _model_pars(self.kinetics, self.water_exchange, self.sequence) kwargs = {} for p in pars: if PARAMS[p]['pixel_par']: kwargs[p] = getattr(self, p)[x] else: kwargs[p] = getattr(self, p) return Tissue( # Set config kinetics=self.kinetics, water_exchange=self.water_exchange, sequence=self.sequence, # Input function aif=self.aif, ca=self.ca, t=self.t, dt=self.dt, # Parameters **kwargs, )
[docs] def info(self): """ Print detailed information about the tissue Example: List all parameters of a default tissue: >>> import dcmri as dc >>> tissue = dc.Tissue() >>> tissue.info() ------------- Configuration ------------- Kinetics: HF Water exchange regime: FF Imaging sequence: SS ---------- Parameters ---------- r1 --> Full name: Contrast agent relaxivity --> Units: Hz/M --> Initial value: 5000.0 --> Current value: 5000.0 --> Free parameter: No --> Bounds: [0, inf] R10a --> Full name: Arterial precontrast R1 --> Units: Hz --> Initial value: 0.7 --> Current value: 0.7 --> Free parameter: No --> Bounds: [0, inf] B1corr_a --> Full name: Arterial B1-correction factor --> Units: --> Initial value: 1 --> Current value: 1 --> Free parameter: No --> Bounds: [0, inf] S0 --> Full name: Signal scaling factor --> Units: a.u. --> Initial value: 1.0 --> Current value: 1.0 --> Free parameter: No --> Bounds: [0, inf] B1corr --> Full name: Tissue B1-correction factor --> Units: --> Initial value: 1 --> Current value: 1 --> Free parameter: No --> Bounds: [0, inf] FA --> Full name: Flip angle --> Units: deg --> Initial value: 15 --> Current value: 15 --> Free parameter: No --> Bounds: [0, inf] TR --> Full name: Repetition time --> Units: sec --> Initial value: 0.005 --> Current value: 0.005 --> Free parameter: No --> Bounds: [0, inf] TS --> Full name: Sampling time --> Units: sec --> Initial value: 0 --> Current value: 0 --> Free parameter: No --> Bounds: [0, inf] H --> Full name: Tissue Hematocrit --> Units: --> Initial value: 0.45 --> Current value: 0.45 --> Free parameter: No --> Bounds: [0.001, 0.999] vb --> Full name: Blood volume --> Units: mL/cm3 --> Initial value: 0.1 --> Current value: 0.1 --> Free parameter: Yes --> Bounds: [0.001, 0.999] vi --> Full name: Interstitial volume --> Units: mL/cm3 --> Initial value: 0.3 --> Current value: 0.3 --> Free parameter: Yes --> Bounds: [0.001, 0.999] PS --> Full name: Permeability-surface area product --> Units: mL/sec/cm3 --> Initial value: 0.003 --> Current value: 0.003 --> Free parameter: Yes --> Bounds: [0, inf] R10 --> Full name: Tissue precontrast R1 --> Units: Hz --> Initial value: 0.7 --> Current value: 0.7 --> Free parameter: No --> Bounds: [0, inf] n0 --> Full name: Number of precontrast acquisitions --> Units: --> Initial value: 1 --> Current value: 1 --> Free parameter: No """ info(self)
def _train_curve(self, args): pix, p = super()._train_curve(args) self.S0[p] = pix.S0 return pix, p
[docs] def predict(self, time: np.ndarray) -> np.ndarray: """Predict the data at given time points Args: time (array-like): 1D array with time points. Returns: ndarray: Array of predicted y-values. """ return super().predict(time)
[docs] def train(self, time: np.ndarray, signal: np.ndarray, **kwargs): """Train the free parameters Args: time (array-like): 1D array with time points. signal (array-like): Array of signal curves. Any number of dimensions is allowed but the last dimension must be time. kwargs: any keyword parameters accepted by `Tissue.train`. Returns: TissueArray: A reference to the model instance. """ return super().train(time, signal, **kwargs)
# TODO: Add conc and relax functions
[docs] def cost(self, time, signal, metric='NRMS') -> float: """Return the goodness-of-fit Args: time (array-like): Array with time points. signal (array-like): Array with measured signals for each element of *time*. metric (str, optional): Which metric to use. Possible metrics are 'RMS' (Root-mean-square); 'NRMS' (Normalized root-mean-square); 'AIC' (Akaike information criterion); 'cAIC' (Corrected Akaike information criterion for small models); 'BIC' (Bayesian information criterion). Defaults to 'NRMS'. Returns: ndarray: goodness of fit in each element of the data array. """ return super().cost(time, signal, metric)
[docs] def params(self, *args, round_to=None): """Return the parameter values Args: args (tuple): parameters to get Returns: list or float: values of parameter values, or a scalar value if only one parameter is required. """ return super().params(*args, round_to=round_to)
[docs] def export_params(self): pars = self._par_values(export=True) pars = {p: [PARAMS[p]['name'], pars[p], PARAMS[p]['unit']] for p in pars} return self._add_sdev(pars)
[docs] def plot(self, time, signal, vmin={}, vmax={}, cmap='gray', ref=None, fname=None, show=True): """Plot parameter maps Args: time (array-like): 1D array with time points. signal (array-like): Array of signal curves. Any number of dimensions is allowed but the last dimension is time. vmin (dict, optional): Minimum values on display for given parameters. Defaults to {}. vmax (dict, optional): Maximum values on display for given parameters. Defaults to {}. cmap (str, optional): matplotlib colormap. Defaults to 'gray'. ref (dict, optional): Reference images - typically used to display ground truth data when available. Keys are 'signal' (array of data in the same shape as signal), and the parameter maps to show. Defaults to None. fname (str, optional): File path to save image. Defaults to None. show (bool, optional): Determine whether the image is shown or not. Defaults to True. Raises: NotImplementedError: Features that are not currently implemented. """ if len(self.shape) == 1: raise NotImplementedError('Cannot plot 1D images.') yfit = self.predict(time) params = self._par_values(kin=True) if 'H' in params: del params['H'] params['S0'] = self.S0 if len(self.shape) == 2: ncols = 2 + len(params) nrows = 2 if ref is None else 3 fig = plt.figure(figsize=(ncols * 2, nrows * 2)) figcols = fig.subfigures( 1, 2, wspace=0.0, hspace=0.0, width_ratios=[2, ncols - 2]) # Left panel: signal ax = figcols[0].subplots(nrows, 2) figcols[0].subplots_adjust(hspace=0.0, wspace=0) for i in range(nrows): for j in range(2): ax[i, j].set_yticks([]) ax[i, j].set_xticks([]) # Signal maps ax[0, 0].set_title('max(signal)') ax[0, 0].set_ylabel('reconstruction') ax[0, 0].imshow(np.amax(yfit, axis=-1), vmin=0, vmax=0.5 * np.amax(signal), cmap=cmap) ax[1, 0].set_ylabel('data') ax[1, 0].imshow(np.amax(signal, axis=-1), vmin=0, vmax=0.5 * np.amax(signal), cmap=cmap) if ref is not None: ax[2, 0].set_ylabel('ground truth') ax[2, 0].imshow(np.amax(ref['signal'], axis=-1), vmin=0, vmax=0.5 * np.amax(signal), cmap=cmap) ax[0, 1].set_title('mean(signal)') ax[0, 1].imshow(np.mean(yfit, axis=-1), vmin=0, vmax=0.5 * np.amax(signal), cmap=cmap) ax[1, 1].imshow(np.mean(signal, axis=-1), vmin=0, vmax=0.5 * np.amax(signal), cmap=cmap) if ref is not None: ax[2, 1].imshow(np.mean(ref['signal'], axis=-1), vmin=0, vmax=0.5 * np.amax(signal), cmap=cmap) # Right panel: free parameters ax = figcols[1].subplots(nrows, ncols - 2) figcols[1].subplots_adjust(hspace=0.0, wspace=0) for i in range(nrows): for j in range(ncols - 2): ax[i, j].set_yticks([]) ax[i, j].set_xticks([]) ax[0, 0].set_ylabel('reconstruction') ax[1, 0].set_ylabel('std devs') if ref is not None: ax[2, 0].set_ylabel('ground truth') for i, par in enumerate(params.keys()): v0 = vmin[par] if par in vmin else np.percentile(params[par], 1) v1 = vmax[par] if par in vmax else np.percentile(params[par], 99) ax[0, i].set_title(par) ax[0, i].imshow(params[par], vmin=v0, vmax=v1, cmap=cmap) if hasattr(self, 'sdev_' + par): ax[1, i].imshow(getattr(self, 'sdev_' + par), vmin=v0, vmax=v1, cmap=cmap) else: ax[1, i].imshow( np.zeros(self.shape).astype(np.int16), cmap=cmap) if ref is not None: ax[2, i].imshow(ref[par], vmin=v0, vmax=v1, cmap=cmap) if len(self.shape) == 3: raise NotImplementedError('3D plot not yet implemented') if fname is not None: plt.savefig(fname=fname) if show: plt.show() else: plt.close()
[docs] def plot_signals(self, time, signal, cmap='gray', ref=None, fname=None, show=True): """Plot measured and reconstructed dynamic signals. Args: time (array-like): 1D array with time points. signal (array-like): Array of signal curves. Any number of dimensions is allowed but the last dimension is time. cmap (str, optional): matplotlib colormap. Defaults to 'gray'. ref (dict, optional): Reference images - typically used to display ground truth data when available. Keys are 'signal' (array of data in the same shape as signal), and the parameter maps to show. Defaults to None. fname (str, optional): File path to save image. Defaults to None. show (bool, optional): Determine whether the image is shown or not. Defaults to True. Raises: NotImplementedError: Features that are not currently implemented. """ if len(self.shape) == 1: raise NotImplementedError('Cannot plot 1D images.') yfit = self.predict(time) if len(self.shape) == 2: ncols = 1 nrows = 2 if ref is None else 3 # fig = plt.figure(figsize=(ncols*2, nrows*2), layout='constrained') fig = plt.figure(figsize=(ncols * 2, nrows * 2)) # Left panel: signal # figcols[0].suptitle('Signal', fontsize='x-large') ax = fig.subplots(nrows, ncols) # figcols[0].subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.00, wspace=0) fig.subplots_adjust(hspace=0.0, wspace=0) for i in range(nrows): ax[i].set_yticks([]) ax[i].set_xticks([]) # data animation ax[0].set_title('signal(time)') ax[0].set_ylabel('data') im = ax[0].imshow(signal[:, :, 0], cmap=cmap, animated=True, vmin=0, vmax=0.5 * np.amax(signal)) ims = [] for i in range(signal.shape[-1]): im = ax[0].imshow(signal[:, :, i], cmap=cmap, animated=True, vmin=0, vmax=0.5 * np.amax(signal)) ims.append([im]) anim_data = ArtistAnimation(fig, ims, interval=50) # fit animation # ax[1,0].set_title('model fit', rotation='vertical', x=-0.1,y=0.5) ax[1].set_ylabel('model fit') im = ax[1].imshow(yfit[:, :, 0], cmap=cmap, animated=True, vmin=0, vmax=0.5 * np.amax(signal)) ims = [] for i in range(yfit.shape[-1]): im = ax[1].imshow(yfit[:, :, i], cmap=cmap, animated=True, vmin=0, vmax=0.5 * np.amax(signal)) ims.append([im]) anim_fit = ArtistAnimation(fig, ims, interval=50) # truth animation if ref is not None: ax[2].set_ylabel('ground truth') im = ax[2].imshow(ref['signal'][:, :, 0], cmap=cmap, animated=True, vmin=0, vmax=0.5 * np.amax(signal)) ims = [] for i in range(ref['signal'].shape[-1]): im = ax[2].imshow(ref['signal'][:, :, i], cmap=cmap, animated=True, vmin=0, vmax=0.5 * np.amax(signal)) ims.append([im]) anim_truth = ArtistAnimation(fig, ims, interval=50) if len(self.shape) == 3: raise NotImplementedError('3D plot not yet implemented') if fname is not None: plt.savefig(fname=fname) if show: plt.show() else: plt.close()
[docs] def plot_params(self, roi=None, vmin={}, vmax={}, ref=None, fname=None, show=True): """Show parameter distributions in regions of interest. Args: roi (dict, optional): Dictionary with masks for regions-of-interest to be shown in the plot. if none is provided, the entire array is shown. Defaults to None. vmin (dict, optional): Minimum values on display for given parameters. Defaults to {}. vmax (dict, optional): Maximum values on display for given parameters. Defaults to {}. cmap (str, optional): matplotlib colormap. Defaults to 'gray'. ref (dict, optional): Reference images - typically used to display ground truth data when available. Keys are 'signal' (array of data in the same shape as ydata), and the parameter maps to show. Defaults to None. fname (str, optional): File path to save image. Defaults to None. show (bool, optional): Determine whether the image is shown or not. Defaults to True. """ params = self._par_values(kin=True) if 'H' in params: del params['H'] params['S0'] = self.S0 ncols = len(params) if roi is None: nrows = 1 fig, ax = plt.subplots( nrows, ncols, figsize=( 2 * ncols, 2 * nrows)) fig.subplots_adjust(hspace=0.0, wspace=0, left=0.2, right=0.8, top=0.9, bottom=0.1) for i, par in enumerate(params): ax[i].set_yticks([]) ax[i].set_xticks([]) ax[i].set_title(par, fontsize=8) data = params[par] if data.size == 0: continue if ref is not None: data = np.concatenate((data, ref[par]), axis=None) v0 = vmin[par] if par in vmin else np.amin(data) v1 = vmax[par] if par in vmax else np.amax(data) if v0 != v1: hrange = [v0, v1] else: hrange = [-1, 1] if v0 == 0 else [0.9 * v0, 1.1 * v0] if ref is not None: ax[i].hist(ref[par], range=[ vmin[par], vmax[par]], label='Truth') ax[i].hist(params[par], range=[vmin[par], vmax[par]], label='Reconstructed') ax[-1].legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) else: nrows = len(roi) fig, ax = plt.subplots( nrows, ncols, figsize=( 2 * ncols, 2 * nrows)) fig.subplots_adjust(hspace=0.0, wspace=0, left=0.2, right=0.8, top=0.9, bottom=0.1) i = 0 for name, mask in roi.items(): ax[i, 0].set_ylabel( name, fontsize=8, rotation='horizontal', labelpad=30) for p, par in enumerate(params): ax[i, p].set_yticks([]) ax[i, p].set_xticks([]) if i == 0: ax[i, p].set_title(par, fontsize=8) data = params[par][mask == 1] if data.size == 0: continue if ref is not None: data = np.concatenate( (data, ref[par][mask == 1]), axis=None) v0 = vmin[par] if par in vmin else np.amin(data) v1 = vmax[par] if par in vmax else np.amax(data) if v0 != v1: hrange = [v0, v1] else: hrange = [-1, 1] if v0 == 0 else [0.9 * v0, 1.1 * v0] if ref is not None: ax[i, p].hist(ref[par][mask == 1], range=hrange, label='Truth') ax[i, p].hist(params[par][mask == 1], range=hrange, label='Reconstructed') i += 1 ax[0, -1].legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) if fname is not None: plt.savefig(fname=fname) if show: plt.show() else: plt.close()
[docs] def plot_fit(self, time, signal, hist_kwargs={}, roi=None, ref=None, fname=None, show=True, ): """Plot time curves and fits in representative pixels. Args: time (array-like): 1D array with time points. signal (array-like): Array of signal curves. Any number of dimensions is allowed but the last dimension is time. hist_kwargs (dict, optional): Keyword arguments to be passed on the matlotlib's hist() finction. Defaults to {}. roi (dict, optional): Dictionary with masks for regions-of-interest to be shown in the plot. if none is provided, the entire array is shown. Defaults to None. ref (dict, optional): Reference images - typically used to display ground truth data when available. Keys are 'signal' (array of data in the same shape as signal), and the parameter maps to show. Defaults to None. fname (str, optional): File path to save image. Defaults to None. show (bool, optional): Determine whether the image is shown or not. Defaults to True. """ nt = signal.shape[-1] signal = signal.reshape((-1, nt)) yfit = self.predict(time).reshape((-1, nt)) # rms = 100*np.linalg.norm(signal-yfit, axis=-1)/np.linalg.norm(signal, axis=-1) rms = np.linalg.norm(signal - yfit, axis=-1) cols = ['fit error histogram', '5th perc', '25th perc', 'median', '75th perc', '95th perc'] if roi is None: nrows = 1 ncols = 6 fig, ax = plt.subplots( nrows, ncols, figsize=( 2 * ncols, 2 * nrows)) fig.subplots_adjust(hspace=0.0, wspace=0, left=0.2, right=0.8, top=0.9, bottom=0.1) for r in range(nrows): for c in range(ncols): ax[r, c].set_xticks([]) ax[r, c].set_yticks([]) for c in range(ncols): ax[0, c].set_title(cols[c], fontsize=8) _plot_roi(time, signal, yfit, ref, hist_kwargs, rms, ax, 'all pixels') else: nrows = len(roi) ncols = 6 fig, ax = plt.subplots( nrows, ncols, figsize=( 2 * ncols, 2 * nrows)) fig.subplots_adjust(hspace=0.0, wspace=0, left=0.2, right=0.8, top=0.9, bottom=0.1) for r in range(nrows): for c in range(ncols): ax[r, c].set_xticks([]) ax[r, c].set_yticks([]) for c in range(ncols): ax[0, c].set_title(cols[c], fontsize=8) i = 0 for name, mask in roi.items(): _plot_roi(time, signal, yfit, ref, hist_kwargs, rms, ax[i, :], name, mask=mask.ravel()) i += 1 legend = ax[0, -1].legend(loc='center left', bbox_to_anchor=(1, 0.5), fontsize=8) labels = ['Truth', 'Prediction', 'Data'] for i, label in enumerate(legend.get_texts()): label.set_text(labels[i]) if fname is not None: plt.savefig(fname=fname) if show: plt.show() else: plt.close()
# TODO: iex and wex instead of kinetics and water_exchange? Water exchange # is also kinetics.
[docs] class Tissue(ui.Model): """Vascular-interstitial tissue. This is the most common tissue type as found in for instance brain, cancer, lung, muscle, prostate, skin, and more. For more detail see :ref:`two-site-exchange`. Args: kinetics (str, optional): Tracer-kinetic model. Possible values are '2CX', '2CU', 'HF', 'HFU', 'NX', 'FX', 'WV', 'U'. Defaults to 'HF'. water_exchange (str, optional): Water exchange regime. Any combination of two of the letters 'F', 'N', 'R' is allowed. Defaults to 'FF'. sequence (str, optional): imaging sequence. Possible values are 'SS' and 'SR'. Defaults to 'SS'. 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. 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:`Tissue-parameters` and :ref:`Tissue-defaults` for a list of tissue parameters and their default values. See Also: `Liver`, `Kidney` Example: Fit an extended Tofts model to data: .. plot:: :include-source: :context: close-figs >>> import dcmri as dc Use `fake_tissue` to generate synthetic test data: >>> time, aif, roi, gt = dc.fake_tissue(CNR=50) Build a tissue and set the parameters to match the experimental conditions of the synthetic data: >>> tissue = dc.Tissue( ... aif = aif, ... dt = time[1], ... r1 = dc.relaxivity(3, 'blood','gadodiamide'), ... TR = 0.005, ... FA = 15, ... n0 = 15, ... ) Train the tissue on the data: >>> tissue.train(time, roi) Print the optimized tissue parameters, their standard deviations and any derived parameters: >>> tissue.print_params(round_to=2) <BLANKLINE> -------------------------------- Free parameters with their stdev -------------------------------- <BLANKLINE> Blood volume (vb): 0.03 (0.0) mL/cm3 Interstitial volume (vi): 0.2 (0.01) mL/cm3 Permeability-surface area product (PS): 0.0 (0.0) mL/sec/cm3 <BLANKLINE> ---------------------------- Fixed and derived parameters ---------------------------- <BLANKLINE> Tissue Hematocrit (H): 0.45 Plasma volume (vp): 0.02 mL/cm3 Interstitial mean transit time (Ti): 71.01 sec B1-corrected Flip Angle (FAcorr): 15 deg Plot the fit to the data and the reconstructed concentrations, using the noise-free ground truth as reference: >>> tissue.plot(time, roi, ref=gt) Notes: Table :ref:`Tissue-parameters` lists the parameters that are relevant in each regime. Alternatively, you can use `dcmri.Tissue.info` to print them out. Table :ref:`Tissue-defaults` list all possible parameters and their default settings. .. _Tissue-parameters: .. list-table:: **Tissue parameters** :widths: 20 30 30 :header-rows: 1 * - Parameters - When to use - Further detail * - n0 - Always - For estimating baseline signal * - r1, R10 - Always - :ref:`relaxation-params` * - R10a, B1corr_a - When aif 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` * - Fb, PS, Ktrans, vb, H, vi, ve, vc, PSe, PSc. - Depends on **kinetics** and **water_exchange** - :ref:`tissue-kinetic-regimes` .. _Tissue-defaults: .. list-table:: **Parameter defaults** :widths: 5 10 10 10 10 :header-rows: 1 * - Parameter - Type - Value - Bounds - Free/Fixed * - r1 - Relaxation - 5000.0 - [0, inf] - Fixed * - R10 - Relaxation - 0.7 - [0, inf] - Fixed * - R10a - Relaxation - 0.7 - [0, inf] - Fixed * - B1corr - Sequence - 1 - [0, inf] - Fixed * - B1corr_a - 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 * - Fb - Kinetic - 0.01 - [0, inf] - Free * - Ktrans - Kinetic - 0.002 - [0, inf] - Free * - PS - Kinetic - 0.003 - [0, inf] - Free * - PSc - Kinetic - 0.03 - [0, inf] - Free * - PSe - Kinetic - 0.03 - [0, inf] - Free * - vb - Kinetic - 0.1 - [0, 1] - Free * - vc - Kinetic - 0.4 - [0, 1] - Free * - ve - Kinetic - 0.355 - [0, 1] - Free * - vi - Kinetic - 0.5 - [0, 1] - Free """ def __init__( self, kinetics='HF', water_exchange='FF', sequence='SS', aif=None, ca=None, t=None, dt=1.0, free=None, **params): # Set configuration self.kinetics = kinetics self.water_exchange = water_exchange self.sequence = sequence _check_config(self) # Input function self.aif = aif self.ca = ca self.t = t self.dt = dt # overide defaults self._set_defaults(free=free, **params) def _params(self): return PARAMS def _model_pars(self): return _model_pars(self.kinetics, self.water_exchange, self.sequence) def _par_values(self, *args, **kwargs): return _par_values(self, *args, **kwargs)
[docs] def info(self): """ Print detailed information about the tissue Example: List all parameters of a default tissue: >>> import dcmri as dc >>> tissue = dc.Tissue() >>> tissue.info() ------------- Configuration ------------- Kinetics: HF Water exchange regime: FF Imaging sequence: SS ---------- Parameters ---------- r1 --> Full name: Contrast agent relaxivity --> Units: Hz/M --> Initial value: 5000.0 --> Current value: 5000.0 --> Free parameter: No --> Bounds: [0, inf] R10a --> Full name: Arterial precontrast R1 --> Units: Hz --> Initial value: 0.7 --> Current value: 0.7 --> Free parameter: No --> Bounds: [0, inf] B1corr_a --> Full name: Arterial B1-correction factor --> Units: --> Initial value: 1 --> Current value: 1 --> Free parameter: No --> Bounds: [0, inf] S0 --> Full name: Signal scaling factor --> Units: a.u. --> Initial value: 1.0 --> Current value: 1.0 --> Free parameter: No --> Bounds: [0, inf] B1corr --> Full name: Tissue B1-correction factor --> Units: --> Initial value: 1 --> Current value: 1 --> Free parameter: No --> Bounds: [0, inf] FA --> Full name: Flip angle --> Units: deg --> Initial value: 15 --> Current value: 15 --> Free parameter: No --> Bounds: [0, inf] TR --> Full name: Repetition time --> Units: sec --> Initial value: 0.005 --> Current value: 0.005 --> Free parameter: No --> Bounds: [0, inf] TS --> Full name: Sampling time --> Units: sec --> Initial value: 0 --> Current value: 0 --> Free parameter: No --> Bounds: [0, inf] H --> Full name: Tissue Hematocrit --> Units: --> Initial value: 0.45 --> Current value: 0.45 --> Free parameter: No --> Bounds: [0.001, 0.999] vb --> Full name: Blood volume --> Units: mL/cm3 --> Initial value: 0.1 --> Current value: 0.1 --> Free parameter: Yes --> Bounds: [0.001, 0.999] vi --> Full name: Interstitial volume --> Units: mL/cm3 --> Initial value: 0.3 --> Current value: 0.3 --> Free parameter: Yes --> Bounds: [0.001, 0.999] PS --> Full name: Permeability-surface area product --> Units: mL/sec/cm3 --> Initial value: 0.003 --> Current value: 0.003 --> Free parameter: Yes --> Bounds: [0, inf] R10 --> Full name: Tissue precontrast R1 --> Units: Hz --> Initial value: 0.7 --> Current value: 0.7 --> Free parameter: No --> Bounds: [0, inf] n0 --> Full name: Number of precontrast acquisitions --> Units: --> Initial value: 1 --> Current value: 1 --> Free parameter: No """ info(self)
[docs] def time(self): """Return an array of time points Returns: np.ndarray: time points in seconds. """ if self.t is None: if self.aif is not None: return self.dt * np.arange(np.size(self.aif)) elif self.ca is not None: return self.dt * np.arange(np.size(self.ca)) else: raise ValueError('Either aif or ca must be provided.') else: return self.t
def _check_ca(self): # Arterial concentrations if self.ca is None: if self.aif is None: raise ValueError( "Either aif or ca must be provided \ to predict signal data.") else: if self.sequence == 'SR': self.ca = sig.conc_src( self.aif, self.TC, 1 / self.R10a, self.r1, self.n0) elif self.sequence == 'SS': self.ca = sig.conc_ss( self.aif, self.TR, self.B1corr_a * self.FA, 1 / self.R10a, self.r1, self.n0)
[docs] def conc(self, sum=True): """Return the tissue concentration Args: sum (bool, optional): If True, returns the total concentrations. Else returns the concentration in the individual compartments. Defaults to True. Returns: np.ndarray: Concentration in M Example: Build a tissue, and plot the tissue concentrations in each compartment: .. plot:: :include-source: :context: close-figs >>> import dcmri as dc >>> import matplotlib.pyplot as plt >>> t, aif, _ = dc.fake_aif() >>> tissue = dc.Tissue('HFU', 'RR', aif=aif, t=t) >>> C = tissue.conc(sum=False) >>> _ = plt.figure() >>> _ = plt.plot(t/60, 1e3*C[0,:], label='Plasma') >>> _ = plt.plot(t/60, 1e3*C[1,:], label='Interstitium') >>> _ = plt.xlabel('Time (min)') >>> _ = plt.ylabel('Concentration (mM)') >>> _ = plt.legend() >>> _ = plt.show() """ self._check_ca() pars = self._par_values(kin=True) return tissue.conc_tissue( self.ca, t=self.t, dt=self.dt, sum=sum, kinetics=self.kinetics, **pars)
[docs] def relax(self): """Compartmental relaxation rates, volume fractions and water-permeability matrix. tuple: relaxation rates of tissue compartments and their volumes. - **R1** (numpy.ndarray): in the fast water exchange limit, the relaxation rates are a 1D array. In all other situations, relaxation rates are a 2D-array with dimensions (k,n), where k is the number of compartments and n is the number of time points in ca. - **v** (numpy.ndarray or None): the volume fractions of the tissue compartments. Returns None in 'FF' regime. - **Fw** (numpy.ndarray or None): 2D array with water exchange rates between tissue compartments. Returns None in 'FF' regime. Example: Build a tissue, print its compartmental volumes and water permeability matrix, and plot the free relaxation rates of each compartment: .. plot:: :include-source: :context: close-figs >>> import dcmri as dc >>> t, aif, _ = dc.fake_aif() >>> tissue = dc.Tissue('2CX', 'RR', aif=aif, t=t) >>> R1, v, Fw = tissue.relax() >>> v array([0.1, 0.3, 0.6]) >>> Fw array([[0. , 0.03, 0. ], [0.03, 0. , 0.03], [0. , 0.03, 0. ]]) >>> import matplotlib.pyplot as plt >>> _ = plt.figure() >>> _ = plt.plot(t/60, R1[0,:], label='Blood') >>> _ = plt.plot(t/60, R1[1,:], label='Interstitium') >>> _ = plt.plot(t/60, R1[2,:], label='Cells') >>> _ = plt.xlabel('Time (min)') >>> _ = plt.ylabel('Relaxation rate (Hz)') >>> _ = plt.legend() >>> plt.show() """ self._check_ca() pars = self._par_values(tiss=True) R1, v, Fw = tissue.relax_tissue( self.ca, self.R10, self.r1, t=self.t, dt=self.dt, kinetics=self.kinetics, water_exchange=self.water_exchange, **pars) return R1, v, Fw
[docs] def magnetization(self) -> np.ndarray: """Pseudocontinuous magnetization Returns: np.ndarray: the magnetization as a 1D array. """ self._check_ca() # TODO do not precompute tpars = self._par_values(tiss=True) spars = self._par_values(seq=True) spars.pop('S0') spars['model'] = self.sequence return tissue.Mz_tissue( self.ca, self.R10, self.r1, t=self.t, dt=self.dt, kinetics=self.kinetics, water_exchange=self.water_exchange, sequence=spars, # inflow = { # 'R10a': self.R10a, # 'B1corr_a': self.B1corr_a, # }, **tpars)
[docs] def signal(self) -> np.ndarray: """Pseudocontinuous signal Returns: np.ndarray: the signal as a 1D array. """ self._check_ca() # TODO do not precompute tpars = self._par_values(tiss=True) spars = self._par_values(seq=True) spars['model'] = self.sequence return tissue.signal_tissue( self.ca, self.R10, self.r1, t=self.t, dt=self.dt, kinetics=self.kinetics, water_exchange=self.water_exchange, sequence=spars, # inflow = { # 'R10a': self.R10a, # 'B1corr_a': self.B1corr_a, # }, **tpars)
# TODO: make time optional (if not provided, assume equal to self.time())
[docs] def predict(self, time: np.ndarray) -> 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, method='NLLS', **kwargs): """Train the free parameters Args: time (array-like): Array with time points. signal (array-like): Array with measured signals for each element of *time*. method (str, optional): Method to use for training. Currently the only option is 'NNLS' (Non-negative least squares). Default is 'NNLS'. kwargs: any keyword parameters accepted by the specified fit method. For 'NNLS' these are all parameters accepted by `scipy.optimize.curve_fit`, except for bounds. Returns: Tissue: A reference to the model instance. """ # Estimate S0 if self.sequence == 'SR': Sref = sig.signal_spgr( 1, self.R10, self.TC, self.TR, self.B1corr * self.FA, self.TP) elif self.sequence == 'SS': Sref = sig.signal_ss(1, self.R10, self.TR, self.B1corr * self.FA) else: raise NotImplementedError( 'Signal model ' + self.sequence + 'is not (yet) supported.') self.S0 = np.mean(signal[:self.n0]) / Sref if Sref > 0 else 0 # If there is no signal, set all free parameters to zero if self.S0 == 0: for par in self.free: setattr(self, par, 0) return self if method == 'NLLS': return ui.train(self, time, signal, **kwargs) if method == 'PSMS': # Work in progress # Fit the complete model ui.train(self, time, signal, **kwargs) # Fit an intravascular model with the same free parameters iv = deepcopy(self) # iv.kinetics = 'NX' setattr(iv, 'PS', 0) for par in ['ve', 'PS', 'PSc']: if par in iv.free: iv.free.pop(par) ui.train(iv, time, signal, **kwargs) # If the intravascular model has a lower AIC, take the free # parameters from there if iv.cost(time, signal, metric='cAIC') < self.cost( time, signal, metric='cAIC'): for par in self.free: setattr(self, par, getattr(iv, par))
# # If the tissue is 1-compartmental and the blood MTT > 30s # # Assume the observed compartment is actually interstitial. # if iv.vp/iv.Fp > 30: # self.vp = 0 # self.Fp = 0 # self.PS = 0 # self.vi = iv.vp # self.Ktrans = iv.Fp # self.v = iv.vp
[docs] def params(self, *args, round_to=None): """Return the parameter values Args: args (tuple): parameters to get round_to (int, optional): Round to how many digits. If this is not provided, the values are not rounded. Defaults to None. Returns: list or float: values of parameter values, or a scalar value if only one parameter is required. Example: Train a tissue on synthetic data and print the compartment volumes: >>> import dcmri as dc >>> t, aif, roi, _ = dc.fake_tissue() >>> tissue = dc.Tissue('HF','RR', t=t, aif=aif).train(t, roi) >>> tissue.params('vp', 'vi', round_to=2) [np.float64(0.03), np.float64(0.24)] """ return super().params(*args, round_to=round_to)
[docs] def export_params(self): """Return model parameters with their descriptions Returns: dict: Dictionary with one item for each model parameter. The key is the parameter symbol (short name), and the value is a 4-element list with [parameter name, value, unit, sdev]. Example: Train a tissue on synthetic data and print the blood volume after training: >>> import dcmri as dc >>> t, aif, roi, _ = dc.fake_tissue() >>> tissue = dc.Tissue('HF','RR', t=t, aif=aif).train(t, roi) >>> pars = tissue.export_params() >>> pars['vb'] ['Blood volume', np.float64(0.05735355675475683), 'mL/cm3', np.float64(0.016039245654090793)] """ return super().export_params()
[docs] def print_params(self, round_to=None): """Print the model parameters and their uncertainties Args: round_to (int, optional): Round to how many digits. If this is not provided, the values are not rounded. Defaults to None. Example: Train a tissue on synthetic data and print the parameters: >>> import dcmri as dc >>> t, aif, roi, _ = dc.fake_tissue() >>> tissue = dc.Tissue('HF','RR', t=t, aif=aif).train(t, roi) >>> tissue.print_params(round_to=2) <BLANKLINE> -------------------------------- Free parameters with their stdev -------------------------------- <BLANKLINE> Transendothelial water PS (PSe): 0.0 (0.22) mL/sec/cm3 Transcytolemmal water PS (PSc): 0.0 (1.39) mL/sec/cm3 Blood volume (vb): 0.06 (0.02) mL/cm3 Interstitial volume (vi): 0.24 (0.07) mL/cm3 Permeability-surface area product (PS): 0.0 (0.0) mL/sec/cm3 <BLANKLINE> ---------------------------- Fixed and derived parameters ---------------------------- <BLANKLINE> Tissue Hematocrit (H): 0.45 Plasma volume (vp): 0.03 mL/cm3 Interstitial mean transit time (Ti): 83.27 sec Intracellular water mean transit time (Twc): 1.3232576345772858e+16 sec Interstitial water mean transit time (Twi): 186652627701867.1 sec Intravascular water mean transit time (Twb): 47283940807705.62 sec B1-corrected Flip Angle (FAcorr): 15 deg """ super().print_params(round_to=round_to)
[docs] def cost(self, time, signal, metric='NRMS') -> float: """Return the goodness-of-fit Args: time (array-like): Array with time points. signal (array-like): Array with measured signals for each element of *time*. metric (str, optional): Which metric to use. Possible metrics are 'RMS' (Root-mean-square); 'NRMS' (Normalized root-mean-square); 'AIC' (Akaike information criterion); 'cAIC' (Corrected Akaike information criterion for small models); 'BIC' (Bayesian information criterion). Defaults to 'NRMS'. Returns: float: goodness of fit. Example: Generate a fake dataset, build two tissues and use the Akaike Information Criterion to decide which configuration is most consistent with the data. >>> import dcmri as dc >>> t, aif, roi, _ = dc.fake_tissue() >>> tissue1 = dc.Tissue('2CX','FF', t=t, aif=aif).train(t, roi) >>> tissue2 = dc.Tissue('HFU','RR', t=t, aif=aif).train(t, roi) >>> tissue1.cost(t, roi, 'AIC') np.float64(-967.4477371121604) >>> tissue2.cost(t, roi, 'AIC') np.float64(-375.7766916566553) tissue1 achieves the lowest cost and is therefore the optimal configuration according to the Akaike Information Criterion. """ return super().cost(time, signal, metric=metric)
[docs] def plot(self, time=None, signal=None, xlim=None, ref=None, fname=None, show=True): """Plot the model fit against data. Args: time (array-like, optional): Array with time points. signal (array-like, optional): Array with measured signals for each element of *time*. xlim (array_like, optional): 2-element array with lower and upper boundaries of the x-axis. Defaults to None. ref (tuple, optional): Tuple of optional test data in the form (x,y), where x is an array with x-values and y is an array with y-values. Defaults to None. fname (path, optional): Filepath to save the image. If no value is provided, the image is not saved. Defaults to None. show (bool, optional): If True, the plot is shown. Defaults to True. """ t = self.time() if time is None: time = t if xlim is None: xlim = [np.amin(t), np.amax(t)] if self.water_exchange != 'FF': fig, ax = plt.subplots(2, 2, figsize=(10, 12)) ax00 = ax[0, 0] ax01 = ax[0, 1] ax10 = ax[1, 0] ax11 = ax[1, 1] else: fig, ax = plt.subplots(1, 2, figsize=(10, 5)) ax00 = ax[0] ax01 = ax[1] ax00.set_title('MRI signals') if ref is not None: if 'signal' in ref: ax00.plot(t / 60, ref['signal'], linestyle='-', linewidth=3.0, color='lightgray', label='Tissue ground truth') ax00.plot(time / 60, self.predict(time), marker='o', linestyle='None', color='cornflowerblue', label='Predicted data') if signal is not None: ax00.plot(time / 60, signal, marker='x', linestyle='None', color='darkblue', label='Data') ax00.plot(t / 60, self.predict(t), linestyle='-', linewidth=3.0, color='darkblue', label='Model') ax00.set(ylabel='MRI signal (a.u.)', xlim=np.array(xlim) / 60) ax00.legend() C = self.conc(sum=False) if C.ndim == 1: C = C.reshape((1, -1)) v, comps = _plot_labels_kin(self.kinetics) pars = self._par_values() ax01.set_title('Concentration in indicator compartments') if ref is not None: # ax01.plot(ref['t']/60, 1000*ref['C'], marker='o', # linestyle='None', color='lightgray', label='Tissue ground truth') ax01.plot(ref['t'] / 60, 1000 * ref['cb'], marker='o', linestyle='None', color='lightcoral', label='Arterial ground truth') ax01.plot(t / 60, 1000 * self.ca, linestyle='-', linewidth=5.0, color='lightcoral', label='Arterial blood') for k, vk in enumerate(v): if vk in pars: ck = C[k, :] / pars[vk] ax01.plot(t / 60, 1000 * ck, linestyle='-', linewidth=3.0, label=comps[k], color=_clr(comps[k])) # ax01.plot(t/60, 1000*np.sum(C,axis=0), linestyle='-', # linewidth=3.0, color='darkblue', label='Tissue') ax01.set(ylabel='Concentration (mM)', xlim=np.array(xlim) / 60) ax01.legend() if self.water_exchange != 'FF': R1, v, Fw = self.relax() c = rel.c_lin(R1, self.r1) comps = _plot_labels_relax(self.kinetics, self.water_exchange) if R1.ndim == 1: c = c.reshape((1, len(c))) ax11.set_title('Concentration in water compartments') for i in range(c.shape[0]): ax11.plot(t / 60, 1000 * c[i, :], linestyle='-', linewidth=3.0, color=_clr(comps[i]), label=comps[i]) ax11.set(xlabel='Time (min)', ylabel='Concentration (mM)', xlim=np.array(xlim) / 60) ax11.legend() M = self.magnetization() ax10.set_title('Magnetization in water compartments') for i in range(M.shape[0]): if np.isscalar(v): # TODO renove this after ref of v Mi = M[i, ...] / v else: Mi = M[i, ...] / v[i] ax10.plot(t / 60, Mi, linestyle='-', linewidth=3.0, color=_clr(comps[i]), label=comps[i]) ax10.set(xlabel='Time (min)', ylabel='Magnetization (a.u.)', xlim=np.array(xlim) / 60) ax10.legend() if fname is not None: plt.savefig(fname=fname) if show: plt.show() else: plt.close()
# Helper functions def _clr(comp): if comp == 'Plasma': return 'darkred' if comp == 'Interstitium': return 'steelblue' if comp == 'Extracellular': return 'dimgrey' if comp == 'Tissue': return 'darkgrey' if comp == 'Blood': return 'darkred' if comp == 'Extravascular': return 'blue' if comp == 'Tissue cells': return 'lightblue' if comp == 'Blood + Interstitium': return 'purple' def _plot_labels_kin(kin): if kin == '2CX': return ['vb', 'vi'], ['Blood', 'Interstitium'] if kin == '2CU': return ['vb', 'vi'], ['Blood', 'Interstitium'] if kin == 'HF': return ['vb', 'vi'], ['Blood', 'Interstitium'] if kin == 'HFU': return ['vb', 'vi'], ['Blood', 'Interstitium'] if kin == 'FX': return ['ve'], ['Extracellular'] if kin == 'NX': return ['vb'], ['Blood'] if kin == 'U': return ['vb'], ['Blood'] if kin == 'WV': return ['vi'], ['Interstitium'] def _plot_labels_relax(kin, wex) -> list: if wex == 'FF': return ['Tissue'] if wex in ['RR', 'NN', 'NR', 'RN']: if kin == 'WV': return ['Interstitium', 'Tissue cells'] else: return ['Blood', 'Interstitium', 'Tissue cells'] if wex in ['RF', 'NF']: if kin == 'WV': return ['Extravascular'] else: return ['Blood', 'Extravascular'] if wex in ['FR', 'FN']: if kin == 'WV': return ['Interstitium', 'Tissue cells'] else: return ['Blood + Interstitium', 'Tissue cells'] def _plot_roi(xdata, ydata, yfit, ref, hist_kwargs, rms, ax, name, mask=None): ax[0].set_ylabel(name, fontsize=8, rotation='horizontal', labelpad=30) if np.size(rms[mask == 1]) == 0: return perc = np.nanpercentile(rms[mask == 1], [5, 25, 50, 75, 95]) if np.count_nonzero(~np.isnan(perc)) == 0: return if mask is None: inroi = np.ones(rms.shape) == 1 else: inroi = mask == 1 loc = [(rms == p) & inroi for p in perc] ax[0].hist(rms[inroi], **hist_kwargs) if ref is not None: style = {'color': 'lightsteelblue', 'linewidth': 5.0} yref = ref['signal'].reshape((-1, ydata.shape[-1])) ax[1].plot(xdata, np.mean(yref[loc[0], :], axis=0), label='Truth (5th perc)', **style) ax[2].plot(xdata, np.mean(yref[loc[1], :], axis=0), label='Truth (25th perc)', **style) ax[3].plot(xdata, np.mean(yref[loc[2], :], axis=0), label='Truth (median)', **style) ax[4].plot(xdata, np.mean(yref[loc[3], :], axis=0), label='Truth (75th perc)', **style) ax[5].plot(xdata, np.mean(yref[loc[4], :], axis=0), label='Truth (95th perc)', **style) style = {'color': 'darkblue'} ax[1].plot(xdata, np.mean(yfit[loc[0], :], axis=0), label='Prediction (5th perc)', **style) ax[2].plot(xdata, np.mean(yfit[loc[1], :], axis=0), label='Prediction (25th perc)', **style) ax[3].plot(xdata, np.mean(yfit[loc[2], :], axis=0), label='Prediction (median)', **style) ax[4].plot(xdata, np.mean(yfit[loc[3], :], axis=0), label='Prediction (75th perc)', **style) ax[5].plot(xdata, np.mean(yfit[loc[4], :], axis=0), label='Prediction (95th perc)', **style) style = {'marker': 'o', 'markersize': 1, 'linestyle': 'None', 'color': 'crimson'} ax[1].plot(xdata, np.mean(ydata[loc[0], :], axis=0), label='Data (5th perc)', **style) ax[2].plot(xdata, np.mean(ydata[loc[1], :], axis=0), label='Data (25th perc)', **style) ax[3].plot(xdata, np.mean(ydata[loc[2], :], axis=0), label='Data (median)', **style) ax[4].plot(xdata, np.mean(ydata[loc[3], :], axis=0), label='Data (75th perc)', **style) ax[5].plot(xdata, np.mean(ydata[loc[4], :], axis=0), label='Data (95th perc)', **style) def _check_config(self: Tissue): if self.sequence not in ['SS', 'SR']: msg = 'Sequence ' + str(self.sequence) + ' is not available.' raise ValueError(msg) def info(self): print('-------------') print('Configuration') print('-------------') print('Kinetics: ' + self.kinetics) print('Water exchange regime: ' + self.water_exchange) print('Imaging sequence: ' + self.sequence) print('----------') print('Parameters') print('----------') for p in _model_pars(self.kinetics, self.water_exchange, self.sequence): free = 'Yes' if p in self.free else 'No' print(p) print('--> Full name: ' + PARAMS[p]['name']) print('--> Units: ' + PARAMS[p]['unit']) print('--> Initial value: ' + str(PARAMS[p]['init'])) print('--> Current value: ' + str(getattr(self, p))) print('--> Free parameter: ' + free) if PARAMS[p]['bounds'] is not None: lb = str(PARAMS[p]['bounds'][0]) ub = str(PARAMS[p]['bounds'][1]) print('--> Bounds: [' + lb + ', ' + ub + ']') def _par_values(self, *args, tiss=False, kin=False, seq=False, export=False): pars = _all_pars( self.kinetics, self.water_exchange, self.sequence, self.__dict__) if args != (): return {p: pars[p] for p in args} if tiss: return {p: pars[p] for p in tissue.params_tissue(self.kinetics, self.water_exchange)} if kin: return {p: pars[p] for p in tissue.params_tissue(self.kinetics, 'FF')} if seq: return {p: pars[p] for p in _seq_pars(self.sequence)} if export: p0 = _model_pars(self.kinetics, self.water_exchange, self.sequence) p1 = tissue.params_tissue(self.kinetics, self.water_exchange) discard = set(p0) - set(p1) - set(self.free.keys()) return {p: pars[p] for p in pars if p not in discard} return pars # CONFIGURATIONS def _model_pars(kin, wex, seq): pars = ['r1'] pars += ['R10a', 'B1corr_a'] pars += _seq_pars(seq) pars += ['TS'] pars += tissue.params_tissue(kin, wex) pars += ['R10', 'n0'] return pars def _seq_pars(seq): if seq == 'SS': return ['S0', 'B1corr', 'FA', 'TR'] elif seq == 'SR': return ['S0', 'B1corr', 'FA', 'TR', 'TC', 'TP'] PARAMS = { 'r1': { 'init': 5000.0, 'default_free': False, 'bounds': [0, np.inf], 'name': 'Contrast agent relaxivity', 'unit': 'Hz/M', 'pixel_par': False, }, '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, }, 'Fb': { 'init': 0.02, 'default_free': True, 'bounds': [0, np.inf], 'name': 'Blood flow', 'unit': 'mL/sec/cm3', 'pixel_par': True, }, 'PS': { 'init': 0.003, 'default_free': True, 'bounds': [0, np.inf], 'name': 'Permeability-surface area product', 'unit': 'mL/sec/cm3', 'pixel_par': True, }, 'vi': { 'init': 0.3, 'default_free': True, 'bounds': [1e-3, 1 - 1e-3], 'name': 'Interstitial volume', 'unit': 'mL/cm3', 'pixel_par': True, }, 'Ktrans': { 'init': 0.003 * 0.01 / (0.003 + 0.01), 'default_free': True, 'bounds': [0, np.inf], 'name': 'Volume transfer constant', 'unit': 'mL/sec/cm3', 'pixel_par': True, }, 've': { 'init': 0.1 * (1 - 0.45) + 0.3, 'default_free': True, 'bounds': [1e-3, 1 - 1e-3], 'name': 'Extracellular volume', 'unit': 'mL/cm3', 'pixel_par': True, }, 'vb': { 'init': 0.1, 'default_free': True, 'bounds': [1e-3, 1 - 1e-3], 'name': 'Blood volume', 'unit': 'mL/cm3', 'pixel_par': True, }, 'vc': { 'init': 0.6, 'default_free': True, 'bounds': [1e-3, 1 - 1e-3], 'name': 'Intracellular volume', 'unit': 'mL/cm3', 'pixel_par': True, }, 'H': { 'init': 0.45, 'default_free': False, 'bounds': [1e-3, 1 - 1e-3], 'name': 'Tissue Hematocrit', 'unit': '', 'pixel_par': True, }, 'PSe': { 'init': 0.03, 'default_free': True, 'bounds': [0, np.inf], 'name': 'Transendothelial water PS', 'unit': 'mL/sec/cm3', 'pixel_par': True, }, 'PSc': { 'init': 0.03, 'default_free': True, 'bounds': [0, np.inf], 'name': 'Transcytolemmal water PS', 'unit': 'mL/sec/cm3', 'pixel_par': True, }, '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, }, # Derived parameters 'vp': { 'name': 'Plasma volume', 'unit': 'mL/cm3', }, 'Fp': { 'name': 'Plasma flow', 'unit': 'mL/cm3', }, 'FAcorr': { 'name': 'B1-corrected Flip Angle', 'unit': 'deg', }, 'E': { 'name': 'Tracer extraction fraction', 'unit': '', }, 'Ti': { 'name': 'Interstitial mean transit time', 'unit': 'sec', }, 'Tp': { 'name': 'Plasma mean transit time', 'unit': 'sec', }, 'Tb': { 'name': 'Blood mean transit time', 'unit': 'sec', }, 'Te': { 'name': 'Extracellular mean transit time', 'unit': 'sec', }, 'Twc': { 'name': 'Intracellular water mean transit time', 'unit': 'sec', }, 'Twi': { 'name': 'Interstitial water mean transit time', 'unit': 'sec', }, 'Twb': { 'name': 'Intravascular water mean transit time', 'unit': 'sec', }, } def _all_pars(kin, wex, seq, p): pars = _model_pars(kin, wex, seq) p = {par: p[par] for par in pars} try: p['Fp'] = p['Fb'] * (1 - p['H']) except KeyError: pass try: p['vp'] = p['vb'] * (1 - p['H']) except KeyError: pass try: p['Ktrans'] = _div(p['Fp'] * p['PS'], p['Fp'] + p['PS']) except KeyError: pass try: p['ve'] = p['vi'] + p['vc'] except KeyError: pass try: p['E'] = _div(p['PS'], p['Fp'] + p['PS']) except KeyError: pass try: p['Ti'] = _div(p['vi'], p['PS']) except KeyError: pass try: p['Tp'] = _div(p['vp'], p['PS'] + p['Fp']) except KeyError: pass try: p['Tb'] = _div(p['vp'], p['Fp']) except KeyError: pass try: p['Te'] = _div(p['ve'], p['Fp']) except KeyError: pass try: p['Twc'] = _div(1 - p['vb'] - p['vi'], p['PSc']) except KeyError: pass try: p['Twi'] = _div(p['vi'], p['PSc'] + p['PSe']) except KeyError: pass try: p['Twb'] = _div(p['vb'], p['PSe']) except KeyError: pass try: p['FAcorr'] = p['B1corr'] * p['FA'] except KeyError: pass return p def _div(a, b): with np.errstate(divide='ignore', invalid='ignore'): return np.where(b == 0, 0, np.divide(a, b))