def multi_tissue_basis(gtab, sh_order, iso_comp): """ Builds a basis for multi-shell multi-tissue CSD model. Parameters ---------- gtab : GradientTable sh_order : int iso_comp: int Number of tissue compartments for running the MSMT-CSD. Minimum number of compartments required is 2. Returns ------- B : ndarray Matrix of the spherical harmonics model used to fit the data m : int ``|m| <= n`` The order of the harmonic. n : int ``>= 0`` The degree of the harmonic. """ if iso_comp < 2: msg = ("Multi-tissue CSD requires at least 2 tissue compartments") raise ValueError(msg) r, theta, phi = geo.cart2sphere(*gtab.gradients.T) m, n = shm.sph_harm_ind_list(sh_order) B = shm.real_sh_descoteaux_from_index(m, n, theta[:, None], phi[:, None]) B[np.ix_(gtab.b0s_mask, n > 0)] = 0. iso = np.empty([B.shape[0], iso_comp]) iso[:] = SH_CONST B = np.concatenate([iso, B], axis=1) return B, m, n
def test_hat_and_lcr(): hemi = hemi_icosahedron.subdivide(3) m, n = sph_harm_ind_list(8) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=descoteaux07_legacy_msg, category=PendingDeprecationWarning) B = real_sh_descoteaux_from_index(m, n, hemi.theta[:, None], hemi.phi[:, None]) H = hat(B) B_hat = np.dot(H, B) assert_array_almost_equal(B, B_hat) R = lcr_matrix(H) d = np.arange(len(hemi.theta)) r = d - np.dot(H, d) lev = np.sqrt(1 - H.diagonal()) r /= lev r -= r.mean() r2 = np.dot(R, d) assert_array_almost_equal(r, r2) r3 = np.dot(d, R.T) assert_array_almost_equal(r, r3)
def test_mcsd_model_delta(): sh_order = 8 gtab = get_3shell_gtab() response = multi_shell_fiber_response(sh_order, [0, 1000, 2000, 3500], wm_response, gm_response, csf_response) model = MultiShellDeconvModel(gtab, response) iso = response.iso theta, phi = default_sphere.theta, default_sphere.phi B = shm.real_sh_descoteaux_from_index( response.m, response.n, theta[:, None], phi[:, None]) wm_delta = model.delta.copy() # set isotropic components to zero wm_delta[:iso] = 0. wm_delta = _expand(model.m, iso, wm_delta) for i, s in enumerate([0, 1000, 2000, 3500]): g = GradientTable(default_sphere.vertices * s) signal = model.predict(wm_delta, g) expected = np.dot(response.response[i, iso:], B.T) npt.assert_array_almost_equal(signal, expected) signal = model.predict(wm_delta, gtab) fit = model.fit(signal) m = model.m npt.assert_array_almost_equal(fit.shm_coeff[m != 0], 0., 2)
def test_smooth_pinv(): hemi = hemi_icosahedron.subdivide(2) m, n = sph_harm_ind_list(4) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message=descoteaux07_legacy_msg, category=PendingDeprecationWarning) B = real_sh_descoteaux_from_index(m, n, hemi.theta[:, None], hemi.phi[:, None]) L = np.zeros(len(m)) C = smooth_pinv(B, L) D = np.dot(npl.inv(np.dot(B.T, B)), B.T) assert_array_almost_equal(C, D) L = n * (n + 1) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D) L = np.arange(len(n)) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D)
def rho_matrix(sh_order, vecs): r"""Compute the SH matrix $\rho$ """ r, theta, phi = cart2sphere(vecs[:, 0], vecs[:, 1], vecs[:, 2]) theta[np.isnan(theta)] = 0 n_c = int((sh_order + 1) * (sh_order + 2) / 2) rho = np.zeros((vecs.shape[0], n_c)) counter = 0 for l in range(0, sh_order + 1, 2): for m in range(-l, l + 1): rho[:, counter] = real_sh_descoteaux_from_index(m, l, theta, phi) counter += 1 return rho
def test_hat_and_lcr(): hemi = hemi_icosahedron.subdivide(3) m, n = sph_harm_ind_list(8) B = real_sh_descoteaux_from_index(m, n, hemi.theta[:, None], hemi.phi[:, None]) H = hat(B) B_hat = np.dot(H, B) assert_array_almost_equal(B, B_hat) R = lcr_matrix(H) d = np.arange(len(hemi.theta)) r = d - np.dot(H, d) lev = np.sqrt(1 - H.diagonal()) r /= lev r -= r.mean() r2 = np.dot(R, d) assert_array_almost_equal(r, r2) r3 = np.dot(d, R.T) assert_array_almost_equal(r, r3)
def shore_matrix_pdf(radial_order, zeta, rtab): r"""Compute the SHORE propagator matrix [1]_" Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor rtab : array, shape (N,3) real space points in which calculates the pdf References ---------- .. [1] Merlet S. et al., "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ r, theta, phi = cart2sphere(rtab[:, 0], rtab[:, 1], rtab[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) psi = np.zeros((r.shape[0], n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): psi[:, counter] = real_sh_descoteaux_from_index( m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(4 * np.pi ** 2 * zeta * r ** 2) *\ np.exp(-2 * np.pi ** 2 * zeta * r ** 2) *\ _kappa_pdf(zeta, n, l) *\ (4 * np.pi ** 2 * zeta * r ** 2) ** (l / 2) * \ (-1) ** (n - l / 2) counter += 1 return psi
def test_smooth_pinv(): hemi = hemi_icosahedron.subdivide(2) m, n = sph_harm_ind_list(4) B = real_sh_descoteaux_from_index(m, n, hemi.theta[:, None], hemi.phi[:, None]) L = np.zeros(len(m)) C = smooth_pinv(B, L) D = np.dot(npl.inv(np.dot(B.T, B)), B.T) assert_array_almost_equal(C, D) L = n * (n + 1) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D) L = np.arange(len(n)) * .05 C = smooth_pinv(B, L) L = np.diag(L) D = np.dot(npl.inv(np.dot(B.T, B) + L * L), B.T) assert_array_almost_equal(C, D)
def shore_matrix_odf(radial_order, zeta, sphere_vertices): r"""Compute the SHORE ODF matrix [1]_" Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor sphere_vertices : array, shape (N,3) vertices of the odf sphere References ---------- .. [1] Merlet S. et al., "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ r, theta, phi = cart2sphere(sphere_vertices[:, 0], sphere_vertices[:, 1], sphere_vertices[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) upsilon = np.zeros((len(sphere_vertices), n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): upsilon[:, counter] = (-1) ** (n - l / 2.0) * \ _kappa_odf(zeta, n, l) * \ hyp2f1(l - n, l / 2.0 + 1.5, l + 1.5, 2.0) * \ real_sh_descoteaux_from_index(m, l, theta, phi) counter += 1 return upsilon
def shore_matrix(radial_order, zeta, gtab, tau=1 / (4 * np.pi ** 2)): r"""Compute the SHORE matrix for modified Merlet's 3D-SHORE [1]_ ..math:: :nowrap: \begin{equation} \textbf{E}(q\textbf{u})=\sum_{l=0, even}^{N_{max}} \sum_{n=l}^{(N_{max}+l)/2} \sum_{m=-l}^l c_{nlm} \phi_{nlm}(q\textbf{u}) \end{equation} where $\phi_{nlm}$ is ..math:: :nowrap: \begin{equation} \phi_{nlm}^{SHORE}(q\textbf{u})=\Biggl[\dfrac{2(n-l)!} {\zeta^{3/2} \Gamma(n+3/2)} \Biggr]^{1/2} \Biggl(\dfrac{q^2}{\zeta}\Biggr)^{l/2} exp\Biggl(\dfrac{-q^2}{2\zeta}\Biggr) L^{l+1/2}_{n-l} \Biggl(\dfrac{q^2}{\zeta}\Biggr) Y_l^m(\textbf{u}). \end{equation} Parameters ---------- radial_order : unsigned int, an even integer that represent the order of the basis zeta : unsigned int, scale factor gtab : GradientTable, gradient directions and bvalues container class tau : float, diffusion time. By default the value that makes q=sqrt(b). References ---------- .. [1] Merlet S. et al., "Continuous diffusion signal, EAP and ODF estimation via Compressive Sensing in diffusion MRI", Medical Image Analysis, 2013. """ qvals = np.sqrt(gtab.bvals / (4 * np.pi ** 2 * tau)) qvals[gtab.b0s_mask] = 0 bvecs = gtab.bvecs qgradients = qvals[:, None] * bvecs r, theta, phi = cart2sphere(qgradients[:, 0], qgradients[:, 1], qgradients[:, 2]) theta[np.isnan(theta)] = 0 F = radial_order / 2 n_c = int(np.round(1 / 6.0 * (F + 1) * (F + 2) * (4 * F + 3))) M = np.zeros((r.shape[0], n_c)) counter = 0 for l in range(0, radial_order + 1, 2): for n in range(l, int((radial_order + l) / 2) + 1): for m in range(-l, l + 1): M[:, counter] = real_sh_descoteaux_from_index( m, l, theta, phi) * \ genlaguerre(n - l, l + 0.5)(r ** 2 / zeta) * \ np.exp(- r ** 2 / (2.0 * zeta)) * \ _kappa(zeta, n, l) * \ (r ** 2 / zeta) ** (l / 2) counter += 1 return M
def multi_shell_fiber_response(sh_order, bvals, wm_rf, gm_rf, csf_rf, sphere=None, tol=20): """Fiber response function estimation for multi-shell data. Parameters ---------- sh_order : int Maximum spherical harmonics order. bvals : ndarray Array containing the b-values. Must be unique b-values, like outputed by `dipy.core.gradients.unique_bvals_tolerance`. wm_rf : (4, len(bvals)) ndarray Response function of the WM tissue, for each bvals. gm_rf : (4, len(bvals)) ndarray Response function of the GM tissue, for each bvals. csf_rf : (4, len(bvals)) ndarray Response function of the CSF tissue, for each bvals. sphere : `dipy.core.Sphere` instance, optional Sphere where the signal will be evaluated. Returns ------- MultiShellResponse MultiShellResponse object. """ NUMPY_1_14_PLUS = LooseVersion(np.__version__) >= LooseVersion('1.14.0') rcond_value = None if NUMPY_1_14_PLUS else -1 bvals = np.array(bvals, copy=True) evecs = np.zeros((3, 3)) z = np.array([0, 0, 1.]) evecs[:, 0] = z evecs[:2, 1:] = np.eye(2) n = np.arange(0, sh_order + 1, 2) m = np.zeros_like(n) if sphere is None: sphere = default_sphere big_sphere = sphere.subdivide() theta, phi = big_sphere.theta, big_sphere.phi B = shm.real_sh_descoteaux_from_index(m, n, theta[:, None], phi[:, None]) A = shm.real_sh_descoteaux_from_index(0, 0, 0, 0) response = np.empty([len(bvals), len(n) + 2]) if bvals[0] < tol: gtab = GradientTable(big_sphere.vertices * 0) wm_response = single_tensor(gtab, wm_rf[0, 3], wm_rf[0, :3], evecs, snr=None) response[0, 2:] = np.linalg.lstsq(B, wm_response, rcond=rcond_value)[0] response[0, 1] = gm_rf[0, 3] / A response[0, 0] = csf_rf[0, 3] / A for i, bvalue in enumerate(bvals[1:]): gtab = GradientTable(big_sphere.vertices * bvalue) wm_response = single_tensor(gtab, wm_rf[i, 3], wm_rf[i, :3], evecs, snr=None) response[i + 1, 2:] = np.linalg.lstsq(B, wm_response, rcond=rcond_value)[0] response[i + 1, 1] = gm_rf[i, 3] * np.exp(-bvalue * gm_rf[i, 0]) / A response[i + 1, 0] = csf_rf[i, 3] * np.exp(-bvalue * csf_rf[i, 0]) / A S0 = [csf_rf[0, 3], gm_rf[0, 3], wm_rf[0, 3]] else: warnings.warn("""No b0 given. Proceeding either way.""", UserWarning) for i, bvalue in enumerate(bvals): gtab = GradientTable(big_sphere.vertices * bvalue) wm_response = single_tensor(gtab, wm_rf[i, 3], wm_rf[i, :3], evecs, snr=None) response[i, 2:] = np.linalg.lstsq(B, wm_response, rcond=rcond_value)[0] response[i, 1] = gm_rf[i, 3] * np.exp(-bvalue * gm_rf[i, 0]) / A response[i, 0] = csf_rf[i, 3] * np.exp(-bvalue * csf_rf[i, 0]) / A S0 = [csf_rf[0, 3], gm_rf[0, 3], wm_rf[0, 3]] return MultiShellResponse(response, sh_order, bvals, S0=S0)
def __init__(self, gtab, ratio, reg_sphere=None, sh_order=8, lambda_=1., tau=0.1): r""" Spherical Deconvolution Transform (SDT) [1]_. The SDT computes a fiber orientation distribution (FOD) as opposed to a diffusion ODF as the QballModel or the CsaOdfModel. This results in a sharper angular profile with better angular resolution. The Constrained SDTModel is similar to the Constrained CSDModel but mathematically it deconvolves the q-ball ODF as oppposed to the HARDI signal (see [1]_ for a comparison and a through discussion). A sharp fODF is obtained because a single fiber *response* function is injected as *a priori* knowledge. In the SDTModel, this response is a single fiber q-ball ODF as opposed to a single fiber signal function for the CSDModel. The response function will be used as deconvolution kernel. Parameters ---------- gtab : GradientTable ratio : float ratio of the smallest vs the largest eigenvalue of the single prolate tensor response function reg_sphere : Sphere sphere used to build the regularization B matrix sh_order : int maximal spherical harmonics order lambda_ : float weight given to the constrained-positivity regularization part of the deconvolution equation tau : float threshold (tau *mean(fODF)) controlling the amplitude below which the corresponding fODF is assumed to be zero. References ---------- .. [1] Descoteaux, M., et al. IEEE TMI 2009. Deterministic and Probabilistic Tractography Based on Complex Fibre Orientation Distributions. """ SphHarmModel.__init__(self, gtab) m, n = sph_harm_ind_list(sh_order) self.m, self.n = m, n self._where_b0s = lazy_index(gtab.b0s_mask) self._where_dwi = lazy_index(~gtab.b0s_mask) no_params = ((sh_order + 1) * (sh_order + 2)) / 2 if no_params > np.sum(~gtab.b0s_mask): msg = "Number of parameters required for the fit are more " msg += "than the actual data points" warnings.warn(msg, UserWarning) x, y, z = gtab.gradients[self._where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere self.B_dwi = real_sh_descoteaux_from_index(m, n, theta[:, None], phi[:, None]) # for the odf sphere if reg_sphere is None: self.sphere = get_sphere('symmetric362') else: self.sphere = reg_sphere r, theta, phi = cart2sphere(self.sphere.x, self.sphere.y, self.sphere.z) self.B_reg = real_sh_descoteaux_from_index(m, n, theta[:, None], phi[:, None]) self.R, self.P = forward_sdt_deconv_mat(ratio, n) # scale lambda_ to account for differences in the number of # SH coefficients and number of mapped directions self.lambda_ = (lambda_ * self.R.shape[0] * self.R[0, 0] / self.B_reg.shape[0]) self.tau = tau self.sh_order = sh_order
def __init__(self, gtab, response, reg_sphere=None, sh_order=8, lambda_=1, tau=0.1, convergence=50): r""" Constrained Spherical Deconvolution (CSD) [1]_. Spherical deconvolution computes a fiber orientation distribution (FOD), also called fiber ODF (fODF) [2]_, as opposed to a diffusion ODF as the QballModel or the CsaOdfModel. This results in a sharper angular profile with better angular resolution that is the best object to be used for later deterministic and probabilistic tractography [3]_. A sharp fODF is obtained because a single fiber *response* function is injected as *a priori* knowledge. The response function is often data-driven and is thus provided as input to the ConstrainedSphericalDeconvModel. It will be used as deconvolution kernel, as described in [1]_. Parameters ---------- gtab : GradientTable response : tuple or AxSymShResponse object A tuple with two elements. The first is the eigen-values as an (3,) ndarray and the second is the signal value for the response function without diffusion weighting (i.e. S0). This is to be able to generate a single fiber synthetic signal. The response function will be used as deconvolution kernel ([1]_). reg_sphere : Sphere (optional) sphere used to build the regularization B matrix. Default: 'symmetric362'. sh_order : int (optional) maximal spherical harmonics order. Default: 8 lambda_ : float (optional) weight given to the constrained-positivity regularization part of the deconvolution equation (see [1]_). Default: 1 tau : float (optional) threshold controlling the amplitude below which the corresponding fODF is assumed to be zero. Ideally, tau should be set to zero. However, to improve the stability of the algorithm, tau is set to tau*100 % of the mean fODF amplitude (here, 10% by default) (see [1]_). Default: 0.1 convergence : int Maximum number of iterations to allow the deconvolution to converge. References ---------- .. [1] Tournier, J.D., et al. NeuroImage 2007. Robust determination of the fibre orientation distribution in diffusion MRI: Non-negativity constrained super-resolved spherical deconvolution .. [2] Descoteaux, M., et al. IEEE TMI 2009. Deterministic and Probabilistic Tractography Based on Complex Fibre Orientation Distributions .. [3] Côté, M-A., et al. Medical Image Analysis 2013. Tractometer: Towards validation of tractography pipelines .. [4] Tournier, J.D, et al. Imaging Systems and Technology 2012. MRtrix: Diffusion Tractography in Crossing Fiber Regions """ # Initialize the parent class: SphHarmModel.__init__(self, gtab) m, n = sph_harm_ind_list(sh_order) self.m, self.n = m, n self._where_b0s = lazy_index(gtab.b0s_mask) self._where_dwi = lazy_index(~gtab.b0s_mask) no_params = ((sh_order + 1) * (sh_order + 2)) / 2 if no_params > np.sum(~gtab.b0s_mask): msg = "Number of parameters required for the fit are more " msg += "than the actual data points" warnings.warn(msg, UserWarning) x, y, z = gtab.gradients[self._where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere self.B_dwi = real_sh_descoteaux_from_index(m, n, theta[:, None], phi[:, None]) # for the sphere used in the regularization positivity constraint self.sphere = reg_sphere or small_sphere r, theta, phi = cart2sphere(self.sphere.x, self.sphere.y, self.sphere.z) self.B_reg = real_sh_descoteaux_from_index(m, n, theta[:, None], phi[:, None]) self.response = response if isinstance(response, AxSymShResponse): r_sh = response.dwi_response self.response_scaling = response.S0 n_response = response.n m_response = response.m else: self.S_r = estimate_response(gtab, self.response[0], self.response[1]) r_sh = np.linalg.lstsq(self.B_dwi, self.S_r[self._where_dwi], rcond=-1)[0] n_response = n m_response = m self.response_scaling = response[1] r_rh = sh_to_rh(r_sh, m_response, n_response) self.R = forward_sdeconv_mat(r_rh, n) # scale lambda_ to account for differences in the number of # SH coefficients and number of mapped directions # This is exactly what is done in [4]_ lambda_ = (lambda_ * self.R.shape[0] * r_rh[0] / (np.sqrt(self.B_reg.shape[0]) * np.sqrt(362.))) self.B_reg *= lambda_ self.sh_order = sh_order self.tau = tau self.convergence = convergence self._X = X = self.R.diagonal() * self.B_dwi self._P = np.dot(X.T, X)
def basis(self, sphere): """A basis that maps the response coefficients onto a sphere.""" theta = sphere.theta[:, None] phi = sphere.phi[:, None] return real_sh_descoteaux_from_index(self.m, self.n, theta, phi)
def recursive_response(gtab, data, mask=None, sh_order=8, peak_thr=0.01, init_fa=0.08, init_trace=0.0021, iter=8, convergence=0.001, parallel=True, nbr_processes=None, sphere=default_sphere): """ Recursive calibration of response function using peak threshold Parameters ---------- gtab : GradientTable data : ndarray diffusion data mask : ndarray, optional mask for recursive calibration, for example a white matter mask. It has shape `data.shape[0:3]` and dtype=bool. Default: use the entire data array. sh_order : int, optional maximal spherical harmonics order. Default: 8 peak_thr : float, optional peak threshold, how large the second peak can be relative to the first peak in order to call it a single fiber population [1]. Default: 0.01 init_fa : float, optional FA of the initial 'fat' response function (tensor). Default: 0.08 init_trace : float, optional trace of the initial 'fat' response function (tensor). Default: 0.0021 iter : int, optional maximum number of iterations for calibration. Default: 8. convergence : float, optional convergence criterion, maximum relative change of SH coefficients. Default: 0.001. parallel : bool, optional Whether to use parallelization in peak-finding during the calibration procedure. Default: True nbr_processes: int If `parallel` is True, the number of subprocesses to use (default multiprocessing.cpu_count()). sphere : Sphere, optional. The sphere used for peak finding. Default: default_sphere. Returns ------- response : ndarray response function in SH coefficients Notes ----- In CSD there is an important pre-processing step: the estimation of the fiber response function. Using an FA threshold is not a very robust method. It is dependent on the dataset (non-informed used subjectivity), and still depends on the diffusion tensor (FA and first eigenvector), which has low accuracy at high b-value. This function recursively calibrates the response function, for more information see [1]. References ---------- .. [1] Tax, C.M.W., et al. NeuroImage 2014. Recursive calibration of the fiber response function for spherical deconvolution of diffusion MRI data. """ S0 = 1. evals = fa_trace_to_lambdas(init_fa, init_trace) res_obj = (evals, S0) if mask is None: data = data.reshape(-1, data.shape[-1]) else: data = data[mask] n = np.arange(0, sh_order + 1, 2) where_dwi = lazy_index(~gtab.b0s_mask) response_p = np.ones(len(n)) for _ in range(iter): r_sh_all = np.zeros(len(n)) csd_model = ConstrainedSphericalDeconvModel(gtab, res_obj, sh_order=sh_order) csd_peaks = peaks_from_model(model=csd_model, data=data, sphere=sphere, relative_peak_threshold=peak_thr, min_separation_angle=25, parallel=parallel, nbr_processes=nbr_processes) dirs = csd_peaks.peak_dirs vals = csd_peaks.peak_values single_peak_mask = (vals[:, 1] / vals[:, 0]) < peak_thr data = data[single_peak_mask] dirs = dirs[single_peak_mask] for num_vox in range(data.shape[0]): rotmat = vec2vec_rotmat(dirs[num_vox, 0], np.array([0, 0, 1])) rot_gradients = np.dot(rotmat, gtab.gradients.T).T x, y, z = rot_gradients[where_dwi].T r, theta, phi = cart2sphere(x, y, z) # for the gradient sphere B_dwi = real_sh_descoteaux_from_index(0, n, theta[:, None], phi[:, None]) r_sh_all += np.linalg.lstsq(B_dwi, data[num_vox, where_dwi], rcond=-1)[0] response = r_sh_all / data.shape[0] res_obj = AxSymShResponse(data[:, gtab.b0s_mask].mean(), response) change = abs((response_p - response) / response_p) if all(change < convergence): break response_p = response return res_obj