def test_multiple_b0(): # Generate a signal with multiple b0 # This gives an isotropic signal. signal = multi_tensor(gtab_with_multiple_b0, mevals, snr=None, S0=S0, fractions=[f * 100, 100 * (1 - f)]) # Single voxel data data_single = signal[0] ivim_model_multiple_b0 = IvimModel(gtab_with_multiple_b0, fit_method='LM') ivim_model_multiple_b0.fit(data_single)
def test_multiple_b0(): # Generate a signal with multiple b0 # This gives an isotropic signal. signal = multi_tensor(gtab_with_multiple_b0, mevals, snr=None, S0=S0, fractions=[f * 100, 100 * (1 - f)]) # Single voxel data data_single = signal[0] ivim_model_multiple_b0 = IvimModel(gtab_with_multiple_b0) ivim_model_multiple_b0.fit(data_single)
def test_ivim_errors(): """ Test if errors raised in the module are working correctly. Scipy introduced bounded least squares fitting in the version 0.17 and is not supported by the older versions. Initializing an IvimModel with bounds for older Scipy versions should raise an error. """ # Run the test for Scipy versions less than 0.17 if SCIPY_VERSION < LooseVersion('0.17'): assert_raises(ValueError, IvimModel, gtab, bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.]), fit_method='LM') else: ivim_model_LM = IvimModel(gtab, bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.]), fit_method='LM') ivim_fit = ivim_model_LM.fit(data_multi) est_signal = ivim_fit.predict(gtab, S0=1.) assert_array_equal(est_signal.shape, data_multi.shape) assert_array_almost_equal(ivim_fit.model_params, ivim_params_LM) assert_array_almost_equal(est_signal, data_multi)
def test_noisy_fit(): """ Test fitting for noisy signals. This tests whether the threshold condition applies correctly and returns the linear fitting parameters. For older scipy versions, the returned value of `f` from a linear fit is around 135 and D and D_star values are equal. Hence doing a test based on Scipy version. """ model_one_stage = IvimModel(gtab) fit_one_stage = model_one_stage.fit(noisy_single) assert_array_less(fit_one_stage.model_params, [10000., 0.3, .01, 0.001])
def get_fitted_ivim(self, data, mask, bval, bvec, b0_threshold=50): logging.info('Intra-Voxel Incoherent Motion Estimation...') bvals, bvecs = read_bvals_bvecs(bval, bvec) if b0_threshold < bvals.min(): warn("b0_threshold (value: {0}) is too low, increase your " "b0_threshold. It should be higher than the first b0 value " "({1}).".format(b0_threshold, bvals.min())) gtab = gradient_table(bvals, bvecs, b0_threshold=b0_threshold) ivimmodel = IvimModel(gtab) ivimfit = ivimmodel.fit(data, mask) return ivimfit, gtab
def get_fitted_ivim(self, data, mask, bval, bvec, b0_threshold=50): logging.info('Intra-Voxel Incoherent Motion Estimation...') bvals, bvecs = read_bvals_bvecs(bval, bvec) if b0_threshold < bvals.min(): warn("b0_threshold (value: {0}) is too low, increase your " "b0_threshold. It should higher than the first b0 value " "({1}).".format(b0_threshold, bvals.min())) gtab = gradient_table(bvals, bvecs, b0_threshold=b0_threshold) ivimmodel = IvimModel(gtab) ivimfit = ivimmodel.fit(data, mask) return ivimfit, gtab
def test_ivim_errors(): """ Test if errors raised in the module are working correctly. Scipy introduced bounded least squares fitting in the version 0.17 and is not supported by the older versions. Initializing an IvimModel with bounds for older Scipy versions should raise an error. """ ivim_model_trr = IvimModel(gtab, bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.]), fit_method='trr') ivim_fit = ivim_model_trr.fit(data_multi) est_signal = ivim_fit.predict(gtab, S0=1.) assert_array_equal(est_signal.shape, data_multi.shape) assert_array_almost_equal(ivim_fit.model_params, ivim_params_trr) assert_array_almost_equal(est_signal, data_multi)
def test_fit_one_stage(): """ Test to check the results for the one_stage linear fit. """ model = IvimModel(gtab, two_stage=False) fit = model.fit(data_single) linear_fit_params = [9.88834140e+02, 1.19707191e-01, 7.91176970e-03, 9.30095210e-04] linear_fit_signal = [988.83414044, 971.77122546, 955.46786293, 939.87125905, 924.93258982, 896.85182201, 870.90346447, 846.81187693, 824.34108781, 803.28900104, 783.48245048, 764.77297789, 747.03322866, 669.54798887, 605.03328304, 549.00852235, 499.21077611, 454.40299244, 413.83192296, 376.98072773, 343.45531017] assert_array_almost_equal(fit.model_params, linear_fit_params) assert_array_almost_equal(fit.predict(gtab), linear_fit_signal)
def test_ivim_errors(): """ Test if errors raised in the module are working correctly. Scipy introduced bounded least squares fitting in the version 0.17 and is not supported by the older versions. Initializing an IvimModel with bounds for older Scipy versions should raise an error. """ # Run the test for Scipy versions less than 0.17 if SCIPY_VERSION < LooseVersion('0.17'): assert_raises(ValueError, IvimModel, gtab, bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.])) else: ivim_model = IvimModel(gtab, bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.])) ivim_fit = ivim_model.fit(data_multi) est_signal = ivim_fit.predict(gtab, S0=1.) assert_array_equal(est_signal.shape, data_multi.shape) assert_array_almost_equal(ivim_fit.model_params, ivim_params) assert_array_almost_equal(est_signal, data_multi)
def test_noisy_fit(): """ Test fitting for noisy signals. This tests whether the threshold condition applies correctly and returns the linear fitting parameters. For older scipy versions, the returned value of `f` from a linear fit is around 135 and D and D_star values are equal. Hence doing a test based on Scipy version. """ model_one_stage = IvimModel(gtab, fit_method='LM') with warnings.catch_warnings(record=True) as w: fit_one_stage = model_one_stage.fit(noisy_single) assert_equal(len(w), 3) for l_w in w: assert_(issubclass(l_w.category, UserWarning)) assert_("" in str(w[0].message)) assert_("x0 obtained from linear fitting is not feasibile" in str(w[0].message)) assert_("x0 is unfeasible" in str(w[1].message)) assert_("Bounds are violated for leastsq fitting" in str(w[2].message)) assert_array_less(fit_one_stage.model_params, [10000., 0.3, .01, 0.001])
depending on which Scipy version you are using. All initializations for the model such as ``split_b_D`` are passed while creating the ``IvimModel``. If you are using Scipy 0.17, you can also set bounds by setting ``bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.]))`` while initializing the ``IvimModel``. It is recommeded that you upgrade to Scipy 0.17 since the fitting results might at times return values which do not make sense physically (for example, a negative $\mathbf{f}$). """ ivimmodel = IvimModel(gtab) """ To fit the model, call the `fit` method and pass the data for fitting. """ ivimfit = ivimmodel.fit(data_slice) """ The fit method creates a IvimFit object which contains the parameters of the model obtained after fitting. These are accessible through the `model_params` attribute of the IvimFit object. The parameters are arranged as a 4D array, corresponding to the spatial dimensions of the data, and the last dimension (of length 4) corresponding to the model parameters according to the following order : $\mathbf{S_{0}, f, D^*, D}$. """ ivimparams = ivimfit.model_params print("ivimparams.shape : {}".format(ivimparams.shape)) """
initializing the model, a final non-linear least squares fitting is performed for all the parameters. All initializations for the model such as ``split_b_D`` are passed while creating the ``IvimModel``. If you are using Scipy 0.17, you can also set bounds by setting ``bounds=([0., 0., 0.,0.], [np.inf, 1., 1., 1.]))`` while initializing the ``IvimModel``. For brevity, we focus on a small section of the slice as selected aboove, to fit the IVIM model. First, we instantiate the IvimModel object. """ ivimmodel = IvimModel(gtab, fit_method='LM') """ To fit the model, call the `fit` method and pass the data for fitting. """ ivimfit = ivimmodel.fit(data_slice) """ The fit method creates a IvimFit object which contains the parameters of the model obtained after fitting. These are accessible through the `model_params` attribute of the IvimFit object. The parameters are arranged as a 4D array, corresponding to the spatial dimensions of the data, and the last dimension (of length 4) corresponding to the model parameters according to the following order : $\mathbf{S_{0}, f, D^*, D}$. """ ivimparams = ivimfit.model_params print("ivimparams.shape : {}".format(ivimparams.shape)) """ As we see, we have a 20x20 slice at the height z = 33. Thus we have 400 voxels. We will now plot the parameters obtained from the
signal = multi_tensor(gtab, mevals, snr=None, S0=S0, fractions=[f * 100, 100 * (1 - f)]) # Single voxel data data_single = signal[0] data_multi = np.zeros((2, 2, 1, len(gtab.bvals))) data_multi[0, 0, 0] = data_multi[0, 1, 0] = data_multi[ 1, 0, 0] = data_multi[1, 1, 0] = data_single ivim_params = np.zeros((2, 2, 1, 4)) ivim_params[0, 0, 0] = ivim_params[0, 1, 0] = params ivim_params[1, 0, 0] = ivim_params[1, 1, 0] = params ivim_model = IvimModel(gtab) ivim_model_one_stage = IvimModel(gtab) ivim_fit_single = ivim_model.fit(data_single) ivim_fit_multi = ivim_model.fit(data_multi) ivim_fit_single_one_stage = ivim_model_one_stage.fit(data_single) ivim_fit_multi_one_stage = ivim_model_one_stage.fit(data_multi) bvals_no_b0 = np.array([5., 10., 20., 30., 40., 60., 80., 100., 120., 140., 160., 180., 200., 300., 400., 500., 600., 700., 800., 900., 1000.]) bvecs_no_b0 = generate_bvecs(N) gtab_no_b0 = gradient_table(bvals_no_b0, bvecs.T) bvals_with_multiple_b0 = np.array([0., 0., 0., 0., 40., 60., 80., 100., 120., 140., 160., 180., 200., 300., 400., 500., 600., 700., 800., 900., 1000.])
def setup_module(): global gtab, ivim_fit_single, ivim_model_LM, data_single, params_LM, \ data_multi, ivim_params_LM, D_star, D, f, S0, gtab_with_multiple_b0, \ noisy_single, mevals, gtab_no_b0, ivim_fit_multi, ivim_model_VP, \ f_VP, D_star_VP, D_VP, params_VP # Let us generate some data for testing. bvals = np.array([0., 10., 20., 30., 40., 60., 80., 100., 120., 140., 160., 180., 200., 300., 400., 500., 600., 700., 800., 900., 1000.]) N = len(bvals) bvecs = generate_bvecs(N) gtab = gradient_table(bvals, bvecs.T, b0_threshold=0) S0, f, D_star, D = 1000.0, 0.132, 0.00885, 0.000921 # params for a single voxel params_LM = np.array([S0, f, D_star, D]) mevals = np.array(([D_star, D_star, D_star], [D, D, D])) # This gives an isotropic signal. signal = multi_tensor(gtab, mevals, snr=None, S0=S0, fractions=[f * 100, 100 * (1 - f)]) # Single voxel data data_single = signal[0] data_multi = np.zeros((2, 2, 1, len(gtab.bvals))) data_multi[0, 0, 0] = data_multi[0, 1, 0] = data_multi[ 1, 0, 0] = data_multi[1, 1, 0] = data_single ivim_params_LM = np.zeros((2, 2, 1, 4)) ivim_params_LM[0, 0, 0] = ivim_params_LM[0, 1, 0] = params_LM ivim_params_LM[1, 0, 0] = ivim_params_LM[1, 1, 0] = params_LM ivim_model_LM = IvimModel(gtab, fit_method='LM') ivim_model_one_stage = IvimModel(gtab, fit_method='LM') ivim_fit_single = ivim_model_LM.fit(data_single) ivim_fit_multi = ivim_model_LM.fit(data_multi) ivim_model_one_stage.fit(data_single) ivim_model_one_stage.fit(data_multi) bvals_no_b0 = np.array([5., 10., 20., 30., 40., 60., 80., 100., 120., 140., 160., 180., 200., 300., 400., 500., 600., 700., 800., 900., 1000.]) _ = generate_bvecs(N) # bvecs_no_b0 gtab_no_b0 = gradient_table(bvals_no_b0, bvecs.T, b0_threshold=0) bvals_with_multiple_b0 = np.array([0., 0., 0., 0., 40., 60., 80., 100., 120., 140., 160., 180., 200., 300., 400., 500., 600., 700., 800., 900., 1000.]) bvecs_with_multiple_b0 = generate_bvecs(N) gtab_with_multiple_b0 = gradient_table(bvals_with_multiple_b0, bvecs_with_multiple_b0.T, b0_threshold=0) noisy_single = np.array([4243.71728516, 4317.81298828, 4244.35693359, 4439.36816406, 4420.06201172, 4152.30078125, 4114.34912109, 4104.59375, 4151.61914062, 4003.58374023, 4013.68408203, 3906.39428711, 3909.06079102, 3495.27197266, 3402.57006836, 3163.10180664, 2896.04003906, 2663.7253418, 2614.87695312, 2316.55371094, 2267.7722168]) noisy_multi = np.zeros((2, 2, 1, len(gtab.bvals))) noisy_multi[0, 1, 0] = noisy_multi[ 1, 0, 0] = noisy_multi[1, 1, 0] = noisy_single noisy_multi[0, 0, 0] = data_single ivim_model_VP = IvimModel(gtab, fit_method='VarPro') f_VP, D_star_VP, D_VP = 0.13, 0.0088, 0.000921 # params for a single voxel params_VP = np.array([f, D_star, D]) ivim_params_VP = np.zeros((2, 2, 1, 3)) ivim_params_VP[0, 0, 0] = ivim_params_VP[0, 1, 0] = params_VP ivim_params_VP[1, 0, 0] = ivim_params_VP[1, 1, 0] = params_VP
for all the parameters. All initializations for the model such as ``split_b_D`` are passed while creating the ``IvimModel``. If you are using Scipy 0.17, you can also set bounds by setting ``bounds=([0., 0., 0.,0.], [np.inf, 1., 1., 1.]))`` while initializing the ``IvimModel``. For brevity, we focus on a small section of the slice as selected aboove, to fit the IVIM model. First, we instantiate the IvimModel object. """ ivimmodel = IvimModel(gtab, fit_method='LM') """ To fit the model, call the `fit` method and pass the data for fitting. """ ivimfit = ivimmodel.fit(data_slice) """ The fit method creates a IvimFit object which contains the parameters of the model obtained after fitting. These are accessible through the `model_params` attribute of the IvimFit object. The parameters are arranged as a 4D array, corresponding to the spatial dimensions of the data, and the last dimension (of length 4) corresponding to the model parameters according to the following order : $\mathbf{S_{0}, f, D^*, D}$. """ ivimparams = ivimfit.model_params print("ivimparams.shape : {}".format(ivimparams.shape)) """
for all the parameters using Scipy's ``leastsq`` or ``least_square`` function depending on which Scipy version you are using. All initializations for the model such as ``split_b_D`` are passed while creating the ``IvimModel``. If you are using Scipy 0.17, you can also set bounds by setting ``bounds=([0., 0., 0., 0.], [np.inf, 1., 1., 1.]))`` while initializing the ``IvimModel``. It is recommeded that you upgrade to Scipy 0.17 since the fitting results might at times return values which do not make sense physically (for example, a negative $\mathbf{f}$). """ ivimmodel = IvimModel(gtab) """ To fit the model, call the `fit` method and pass the data for fitting. """ ivimfit = ivimmodel.fit(data_slice) """ The fit method creates a IvimFit object which contains the parameters of the model obtained after fitting. These are accessible through the `model_params` attribute of the IvimFit object. The parameters are arranged as a 4D array, corresponding to the spatial dimensions of the data, and the last dimension (of length 4) corresponding to the model parameters according to the following order : $\mathbf{S_{0}, f, D^*, D}$. """ ivimparams = ivimfit.model_params print("ivimparams.shape : {}".format(ivimparams.shape)) """ As we see, we have a 20x20 slice at the height z = 33. Thus we have 400 voxels. We will now plot the parameters obtained from the
class IvimTensorModel(ReconstModel): def __init__(self, gtab, split_b_D=200.0, n_threads=1): """ Model to reconstruct an IVIM tensor Parameters ---------- gtab : GradientTable class instance split_b_D : float The value of b that separates perfusion from diffusion """ ReconstModel.__init__(self, gtab) self.split_b_D = split_b_D # Use two separate tensors for initial estimation: self.diffusion_idx = np.hstack( [np.where(gtab.bvals > self.split_b_D), np.where(gtab.b0s_mask)]).squeeze() # The first tensor represents diffusion self.diffusion_gtab = gradient_table( self.gtab.bvals[self.diffusion_idx], self.gtab.bvecs[self.diffusion_idx]) self.diffusion_model = TensorModel(self.diffusion_gtab) # The second tensor represents perfusion: self.perfusion_idx = np.array( np.where(gtab.bvals <= self.split_b_D)).squeeze() self.perfusion_gtab = gradient_table( self.gtab.bvals[self.perfusion_idx], self.gtab.bvecs[self.perfusion_idx]) self.perfusion_model = TensorModel(self.perfusion_gtab) # We'll need a "vanilla" IVIM model: self.ivim_model = IvimModel(self.gtab) # How many threads in parallel execution: self.n_threads = n_threads def model_eq1(self, b, *params): """ The model with a fixed perfusion fraction """ bvecs = self.gtab.bvecs beta = self._ivim_pf Q, Q_star = _reconstruct_tensors(params) return _ivim_tensor_equation(beta, b, bvecs, Q_star, Q) def model_eq2(self, b, *params): """ The full model, including perfusion fraction as free parameter """ beta = params[0] bvecs = self.gtab.bvecs Q, Q_star = _reconstruct_tensors(params[1:]) return _ivim_tensor_equation(beta, b, bvecs, Q_star, Q) def _inner_loop(self, vox_chunk): model_params = np.zeros((vox_chunk.shape[0], 13)) for ii, vox in enumerate(vox_chunk): # Extract initial guess of Euler angles for the diffusion fit: dt_evecs = self.diffusion_fit.evecs[vox] angles_dti = calc_euler(dt_evecs) # Extract initial guess of Euler angles for the perfusion fit: perfusion_evecs = self.perfusion_fit.evecs[vox] angles_perfusion = calc_euler(perfusion_evecs) # Initial guess of perfusion fraction based on "vanilla" IVIM: self._ivim_pf = np.clip( np.min([ self.ivim_fit.perfusion_fraction[vox], 1 - self.ivim_fit.perfusion_fraction[vox] ]), 0, 1) # If diffusivity is lower than this, it's not perfusion! min_D_star = 0.003 # Put together initial guess for 13 parameters of full model: initial = [ self._ivim_pf, np.min([self.diffusion_fit.evals[vox, 0], min_D_star]), np.min([self.diffusion_fit.evals[vox, 1], min_D_star]), np.min([self.diffusion_fit.evals[vox, 2], min_D_star]), angles_dti[0], angles_dti[1], angles_dti[2], np.max([self.perfusion_fit.evals[vox, 0], min_D_star]), np.max([self.perfusion_fit.evals[vox, 1], min_D_star]), np.max([self.perfusion_fit.evals[vox, 2], min_D_star]), angles_perfusion[0], angles_perfusion[1], angles_perfusion[2] ] # Bounds on the parameters: lb = (0, 0, 0, 0, -np.pi, -np.pi, -np.pi, 0.003, 0.003, 0.003, -np.pi, -np.pi, -np.pi) ub = (0.5, 0.003, 0.003, 0.003, np.pi, np.pi, np.pi, np.inf, np.inf, np.inf, np.pi, np.pi, np.pi) # Fit the full model to the data with initial guess and bounds try: popt, pcov = curve_fit( self.model_eq2, self.gtab.bvals, self.mask_data[vox] / np.mean(self.mask_data[vox, self.gtab.b0s_mask]), p0=initial, bounds=(lb, ub), xtol=0.05, ftol=0.05, maxfev=10000) # Sometimes it can't fit the data: except RuntimeError: popt = np.ones(len(initial)) * np.nan model_params[ii] = popt return model_params def fit(self, data, mask=None): """ Fit the IVIM tensor model """ if mask is None: mask = np.ones(data.shape[:-1], dtype=bool) self.mask_data = data[mask] # Fit diffusion tensor to diffusion-weighted data: diffusion_data = self.mask_data[:, self.diffusion_idx] self.diffusion_fit = self.diffusion_model.fit(diffusion_data) # Fit "vanilla" IVIM to all of the data: self.ivim_fit = self.ivim_model.fit(self.mask_data) # Fit perfusion tensor to perfusion-weighted data: perfusion_data = self.mask_data[:, self.perfusion_idx] self.perfusion_fit = self.perfusion_model.fit(perfusion_data) # Pre-allocate parameters #model_params = np.zeros((self.mask_data.shape[0], 13)) voxel_indices = np.arange(self.mask_data.shape[0]) if self.n_threads > 1: # Loop over voxels: vox_chunks = np.array_split(voxel_indices, self.n_threads) with ThreadPoolExecutor(max_workers=self.n_threads) as executor: loop = asyncio.new_event_loop() tasks = [ loop.run_in_executor( executor, self._inner_loop, vox_chunk, ) for vox_chunk in vox_chunks ] try: model_params = np.concatenate( list( tqdm( loop.run_until_complete( asyncio.gather(*tasks))))) finally: loop.close() else: model_params = self._inner_loop(voxel_indices) return IvimTensorFit(self, model_params)