def fit(self): """ Predict the signal based on the kernel model fit """ if self.verbose: print("Predicting signal from SparseKernelModel") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] out_flat = np.zeros(self._flat_signal.shape) flat_params = self.model_params[self.mask] # We will use a cached fit object generated in the first iteration _fit_obj = None # And the vertices are the b vectors: _verts = self.bvecs[:, self.b_idx].T for vox in xrange(out_flat.shape[0]): this_fit = self.kernel_model.SparseKernelFit(flat_params[vox][1:], flat_params[vox][0], model=self._km) this_relative = this_fit.predict(cache=_fit_obj, vertices=_verts) _fit_obj = this_fit # From now on, we will use this cached object _verts = None # And set the verts input to None, so that it is # ignored in future iterations out_flat[vox] = this_relative * self._flat_S0[vox] if self.verbose: prog_bar.animate(vox, f_name=f_name) out = ozu.nans(self.signal.shape) out[self.mask] = out_flat return out
def odf(self): """ The orientation distribution function estimated from the SparseKernel model """ _verts = self.odf_verts[ 0] # These are the vertices on which we estimate # the ODF if self.verbose: prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] out_flat = np.zeros((self._flat_signal.shape[0], _verts.shape[0])) flat_params = self.model_params[self.mask] # We are going to use cached computations in the fit object: _fit_obj = None # Initially we don't have a cached fit object for vox in xrange(out_flat.shape[0]): this_fit = self.kernel_model.SparseKernelFit(flat_params[vox][1:], flat_params[vox][0], model=self._km) out_flat[vox] = this_fit.odf(cache=_fit_obj, vertices=_verts) _fit_obj = this_fit # From now on, we will use this cached object _verts = None # And we need to ignore the vertices, so that the # cached fit object can use the cached computation. if self.verbose: prog_bar.animate(vox, f_name=f_name) out = ozu.nans(self.signal.shape[:3] + (out_flat.shape[-1], )) out[self.mask] = out_flat return out
def fit(self): """ This is the signal estimated from the odf. """ if self.verbose: print("Predicting signal from SphericalHarmonicsModel") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # Reshape the odf to be one voxel per row: flat_odf = self.odf[self.mask] pred_sig = np.empty(flat_odf.shape) for vox in xrange(pred_sig.shape[0]): # Predict based on the convolution: this_pred_sig = self.response_function.convolve_odf( flat_odf[vox], self._flat_S0[vox]) # We might have a scaling and an offset in addition, so let's fit # those in each voxel based on the signal: a, b = np.polyfit(this_pred_sig, self._flat_signal[vox], 1) pred_sig[vox] = a * this_pred_sig + b if self.verbose: prog_bar.animate(vox, f_name=f_name) # Pack it back into a volume shaped thing: out = ozu.nans(self.signal.shape) out[self.mask] = pred_sig return out
def predict_all(self): """ Calculate the predicted signal for all the possible OLS solutions """ # Get the bvec weights (we don't know how many...) and the # isotropic weights (which are always last): b_w = self.ols[:, :-1, :].copy().squeeze() i_w = self.ols[:, -1, :].copy().squeeze() # nan out the places where weights are negative: #b_w[b_w<0] = np.nan #i_w[i_w<0] = np.nan # A predicted signal for each voxel, for each rot_idx, for each # direction: flat_out = np.empty((self._flat_signal.shape[0], len(self.rot_idx), self._flat_signal.shape[-1])) if self.verbose: print("Predicting all signals for MultiCanonicalTensorModel:") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] for vox in xrange(flat_out.shape[0]): for idx, rot_idx in enumerate(self.rot_idx): # The constant regressor gets added in first: this_relative = i_w[idx, vox] * self.regressors[0][0] # And we add the different canonicals on top of that: this_relative += ( np.dot( b_w[idx, :, vox], # The tensor regressors are different in cases where we # are fitting to relative/attenuation signal, so grab that # from the regressors attr: np.array([self.regressors[1][x] for x in rot_idx]))) if self.mode == 'relative_signal' or self.mode == 'normalize': flat_out[vox, idx] = this_relative * self._flat_S0[vox] elif self.mode == 'signal_attenuation': flat_out[vox, idx] = (1 - this_relative) * self._flat_S0[vox] if self.verbose: prog_bar.animate(vox, f_name=f_name) out = ozu.nans(self.signal.shape[:3] + (len(self.rot_idx), ) + (self.signal.shape[-1], )) out[self.mask] = flat_out return out
def model_params(self): """ Find the model parameters using least-squares optimization. """ if self.model_form == 'constrained': n_params = 3 elif self.model_form == 'flexible' or self.model_form == 'ball_and_stick': n_params = 4 else: e_s = "%s is not a recognized model form" % self.model_form raise ValueError(e_s) params = np.empty((self.fit_signal.shape[0], n_params)) if self.verbose: print('Fitting CanonicalTensorModelOpt:') prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # Initialize the starting conditions for the first voxel if self.model_form == 'constrained': this_params = 0, 0, np.mean(self.fit_signal[0]) elif (self.model_form == 'flexible' or self.model_form == 'ball_and_stick'): this_params = (0, 0, np.mean(self.fit_signal[0]), np.mean(self.fit_signal[0])) for vox in xrange(self.fit_signal.shape[0]): # From the second voxel and onwards, we use the end point of the # last voxel as the starting point for this voxel: start_params = this_params # Do the least-squares fitting (setting tolerance to a rather # lenient value?): this_params, status = opt.leastsq(self._err_func, start_params, args=(self.fit_signal[vox]), ftol=10e-5) params[vox] = this_params if self.verbose: prog_bar.animate(vox, f_name=f_name) out_params = ozu.nans(self.signal.shape[:3] + (n_params, )) out_params[self.mask] = np.array(params).squeeze() return out_params
def model_params(self): """ Fit the parameters of the kernel model """ # The file already exists: if os.path.isfile(self.params_file): if self.verbose: print("Loading params from file: %s" % self.params_file) # Get the cached values and be done with it: return ni.load(self.params_file).get_data() else: if self.verbose: print("Fitting params for SparseKernelModel") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # 1 parameter for each basis function + 1 for the intercept: out_flat = np.empty( (self._flat_signal.shape[0], self.quad_points + 1)) for vox in xrange(out_flat.shape[0]): this_fit = self._km.fit(self._flat_relative_signal[vox]) beta = this_fit.beta intercept = this_fit.intercept # Fit the model, get the params: out_flat[vox] = np.hstack([intercept, beta]) if self.verbose: prog_bar.animate(vox, f_name=f_name) out_params = ozu.nans(self.signal.shape[:3] + (self.quad_points + 1, )) out_params[self.mask] = out_flat if self.params_file != 'temp': # Save the params for future use: params_ni = ni.Nifti1Image(out_params, self.affine) if self.verbose: print("Saving params to file: %s" % self.params_file) params_ni.to_filename(self.params_file) # And return the params for current use: return out_params
def voxel2fiber(self): """ The first list in the tuple answers the question: Given a voxel (from the unique indices in this model), which fibers pass through it? The second answers the question: Given a voxel, for each fiber, which nodes are in that voxel? """ # Preallocate for speed: # Make a voxels by fibers grid. If the fiber is in the voxel, the value # there will be 1, otherwise 0: v2f = np.zeros((len(self.fg_idx_unique.T), len(self.FG.fibers))) # This is a grid of size (fibers, maximal length of a fiber), so that # we can capture put in the voxel number in each fiber/node combination: v2fn = ozu.nans((len(self.FG.fibers), np.max([f.coords.shape[-1] for f in self.FG]))) if self.verbose: prog_bar = ozu.ProgressBar(self.FG.n_fibers) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # In each fiber: for f_idx, f in enumerate(self.FG.fibers): # In each voxel present in there: for vv in f.coords.astype(int).T: # What serial number is this voxel in the unique fiber indices: voxel_id = np.where((vv[0] == self.fg_idx_unique[0]) * (vv[1] == self.fg_idx_unique[1]) * (vv[2] == self.fg_idx_unique[2]))[0] # Add that combination to the grid: v2f[voxel_id, f_idx] += 1 # All the nodes going through this voxel get its number: v2fn[f_idx][np.where( (f.coords.astype(int)[0] == vv[0]) * (f.coords.astype(int)[1] == vv[1]) * (f.coords.astype(int)[2] == vv[2]))] = voxel_id if self.verbose: prog_bar.animate(f_idx, f_name=f_name) return v2f, v2fn
def calibrate(self): """" This is the function to perform the calibration optimization on. When this is done, self.AD and self.RD will be set and parameter estimation can proceed as in the super-class """ out = np.empty( (self.calibration_signal.shape[0], len(self.start_params))) if self.verbose: print('Calibrating for AD/RD') prog_bar = ozu.ProgressBar(self.calibration_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] for vox in xrange(self.calibration_signal.shape[0]): # Perform the fitting itself: #out[vox], ier = leastsqbound(self._err_func, # self.start_params, # bounds = bounds, # args=(self.calibration_signal[vox]), # **optim_kwds) out[vox], ier = opt.leastsq(self._err_func, self.start_params, args=(self.calibration_signal[vox])) if self.verbose: prog_bar.animate(vox, f_name=f_name) # Set the object's AD/RD according to the calibration: self.ad = np.median(out[:, -2]) self.rd = np.median(out[:, -1]) # The isotropic component diffusivity is set to be the same as the # axial diffusivity in the fiber component: self.iso_diffusivity = self.ad return out
def fiber_signal(self): """ The relative signal predicted along each fiber. """ if self.verbose: prog_bar = ozu.ProgressBar(self.FG.n_fibers) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] sig = [] for f_idx, f in enumerate(self.FG): sig.append( f.predicted_signal(self.bvecs[:, self.b_idx], self.bvals[self.b_idx], self.axial_diffusivity, self.radial_diffusivity)) if self.verbose: prog_bar.animate(f_idx, f_name=f_name) return sig
def fit(self): """ Predict the signal attenuation from the fit of the MultiCanonicalTensorModel """ if self.verbose: print("Predicting signal from MultiCanonicalTensorModel") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] out_flat = np.empty(self._flat_signal.shape) flat_params = self.model_params[self.mask] for vox in xrange(out_flat.shape[0]): # If there's a nan in there, just ignore this voxel and set it to # all nans: if ~np.any(np.isnan(flat_params[vox, 1])): b_w = flat_params[vox, 1:1 + self.n_canonicals] i_w = flat_params[vox, -1] # This gets saved as a float, but we can safely assume it's # going to be an integer: rot_idx = self.rot_idx[int(flat_params[vox, 0])] out_flat[vox] = ( np.dot(b_w, np.array([self.rotations[i] for i in rot_idx])) + self.regressors[0][0] * i_w) * self._flat_S0[vox] else: out_flat[vox] = np.nan # This gets broadcast to the right # length on assigment? if self.verbose: prog_bar.animate(vox, f_name=f_name) out = ozu.nans(self.signal.shape) out[self.mask] = out_flat return out
def matrix(self): """ The matrix of fiber-contributions to the DWI signal. """ # Assign some local variables, for shorthand: vox_coords = self.fg_idx_unique.T n_vox = self.fg_idx_unique.shape[-1] n_bvecs = self.b_idx.shape[0] v2f, v2fn = self.voxel2fiber # How many fibers in each voxel (this will determine how many # components are in the fiber part of the matrix): n_unique_f = np.sum(v2f) # Preallocate these, which will be used to generate the two sparse # matrices: # This one will hold the fiber-predicted signal f_matrix_sig = np.zeros(n_unique_f * n_bvecs) f_matrix_row = np.zeros(n_unique_f * n_bvecs) f_matrix_col = np.zeros(n_unique_f * n_bvecs) # And this will hold weights to soak up the isotropic component in each # voxel: i_matrix_sig = np.zeros(n_vox * n_bvecs) i_matrix_row = np.zeros(n_vox * n_bvecs) i_matrix_col = np.zeros(n_vox * n_bvecs) keep_ct1 = 0 keep_ct2 = 0 if self.verbose: prog_bar = ozu.ProgressBar(len(vox_coords)) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # In each voxel: for v_idx, vox in enumerate(vox_coords): # For each fiber: for f_idx in np.where(v2f[v_idx])[0]: # Sum the signal from each node of the fiber in that voxel: pred_sig = np.zeros(n_bvecs) for n_idx in np.where(v2fn[f_idx] == v_idx)[0]: relative_signal = self.fiber_signal[f_idx][n_idx] if self.mode == 'relative_signal': # Predict the signal and demean it, so that the isotropic # part can carry that: pred_sig += (relative_signal - np.mean( self.relative_signal[vox[0], vox[1], vox[2]])) elif self.mode == 'signal_attenuation': pred_sig += ((1 - relative_signal) - np.mean( 1 - self.relative_signal[vox[0], vox[1], vox[2]])) # For each fiber-voxel combination, we now store the row/column # indices and the signal in the pre-allocated linear arrays f_matrix_row[keep_ct1:keep_ct1+n_bvecs] =\ np.arange(n_bvecs) + v_idx * n_bvecs f_matrix_col[keep_ct1:keep_ct1 + n_bvecs] = np.ones(n_bvecs) * f_idx f_matrix_sig[keep_ct1:keep_ct1 + n_bvecs] = pred_sig keep_ct1 += n_bvecs # Put in the isotropic part in the other matrix: i_matrix_row[keep_ct2:keep_ct2+n_bvecs]=\ np.arange(v_idx*n_bvecs, (v_idx + 1)*n_bvecs) i_matrix_col[keep_ct2:keep_ct2 + n_bvecs] = v_idx * np.ones(n_bvecs) i_matrix_sig[keep_ct2:keep_ct2 + n_bvecs] = 1 keep_ct2 += n_bvecs if self.verbose: prog_bar.animate(v_idx, f_name=f_name) # Allocate the sparse matrices, using the more memory-efficient 'csr' # format: fiber_matrix = sparse.coo_matrix( (f_matrix_sig, [f_matrix_row, f_matrix_col])).tocsr() iso_matrix = sparse.coo_matrix( (i_matrix_sig, [i_matrix_row, i_matrix_col])).tocsr() if self.verbose: print("Generated model matrices") return (fiber_matrix, iso_matrix)
def model_params(self): """ The model parameters. Similar to the CanonicalTensorModel, if a fit has ocurred, the data is cached on disk as a nifti file If a fit hasn't occured yet, calling this will trigger a model fit and derive the parameters. In that case, the steps are as follows: 1. Perform OLS fitting on all voxels in the mask, with each of the $\vec{b}$ combinations, choosing only sets for which all weights are non-negative. 2. Find the PDD combination that most readily explains the data (highest correlation coefficient between the data and the predicted signal) That will be the combination used to derive the fit for that voxel. """ # The file already exists: if os.path.isfile(self.params_file): if self.verbose: print("Loading params from file: %s" % self.params_file) # Get the cached values and be done with it: return ni.load(self.params_file).get_data() else: # Looks like we might need to do some fitting... # Get the bvec weights (we don't know how many...) and the # isotropic weights (which are always last): b_w = self.ols[:, :-1, :].copy().squeeze() i_w = self.ols[:, -1, :].copy().squeeze() # nan out the places where weights are negative: b_w[b_w < 0] = np.nan i_w[i_w < 0] = np.nan # Weight for each canonical tensor, plus a place for the index into # rot_idx and one more slot for the isotropic weight (at the end) params = np.empty( (self._flat_signal.shape[0], self.n_canonicals + 2)) if self.verbose: print("Fitting MultiCanonicalTensorModel:") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # Find the best OLS solution in each voxel: for vox in xrange(self._flat_signal.shape[0]): # We do this in each voxel (instead of all at once, which is # possible...) to not blow up the memory: vox_fits = np.empty((len(self.rot_idx), len(self.b_idx))) for idx, rot_idx in enumerate(self.rot_idx): # The constant regressor gets added in first: this_relative = i_w[idx, vox] * self.regressors[0][0] # And we add the different canonicals on top of that: this_relative += ( np.dot( b_w[idx, :, vox], # The tensor regressors are different in cases where we # are fitting to relative/attenuation signal, so grab that # from the regressors attr: np.array([self.regressors[1][x] for x in rot_idx]))) if self.mode == 'relative_signal' or self.mode == 'normalize': vox_fits[idx] = this_relative * self._flat_S0[vox] elif self.mode == 'signal_attenuation': vox_fits[idx] = (1 - this_relative) * self._flat_S0[vox] # Find the predicted signal that best matches the original # signal attenuation. That will choose the direction for the # tensor we use: corrs = ozu.coeff_of_determination(self._flat_signal[vox], vox_fits) idx = np.where(corrs == np.nanmax(corrs))[0] # Sometimes there is no good solution: if len(idx): # In case more than one fits the bill, just choose the # first one: if len(idx) > 1: idx = idx[0] params[vox, :] = np.hstack([ idx, np.array([x for x in b_w[idx, :, vox]]).squeeze(), i_w[idx, vox] ]) else: # In which case we set it to all nans: params[vox, :] = np.hstack( [np.nan, self.n_canonicals * (np.nan, ), np.nan]) if self.verbose: prog_bar.animate(vox, f_name=f_name) # Save the params for future use: out_params = ozu.nans(self.signal.shape[:3] + (params.shape[-1], )) out_params[self.mask] = np.array(params).squeeze() params_ni = ni.Nifti1Image(out_params, self.affine) if self.params_file != 'temp': if self.verbose: print("Saving params to file: %s" % self.params_file) params_ni.to_filename(self.params_file) # And return the params for current use: return out_params
def model_params(self): """ The model parameters. Similar to the TensorModel, if a fit has ocurred, the data is cached on disk as a nifti file If a fit hasn't occured yet, calling this will trigger a model fit and derive the parameters. In that case, the steps are as follows: 1. Perform OLS fitting on all voxels in the mask, with each of the $\vec{b}$. Choose only the non-negative weights. 2. Find the PDD that most readily explains the data (highest correlation coefficient between the data and the predicted signal) and use that one to derive the fit for that voxel """ # The file already exists: if os.path.isfile(self.params_file): if self.verbose: print("Loading params from file: %s" % self.params_file) # Get the cached values and be done with it: return ni.load(self.params_file).get_data() else: # Looks like we might need to do some fitting... # Get the bvec weights and the isotropic weights b_w = self.ols[:, 0, :].copy().squeeze() i_w = self.ols[:, 1, :].copy().squeeze() # nan out the places where weights are negative: b_w[b_w < 0] = np.nan i_w[i_w < 0] = np.nan params = np.empty((self._flat_signal.shape[0], 3)) if self.verbose: print("Fitting CanonicalTensorModel:") prog_bar = ozu.ProgressBar(self._flat_signal.shape[0]) this_class = str(self.__class__).split("'")[-2].split('.')[-1] f_name = this_class + '.' + inspect.stack()[0][3] # Find the best OLS solution in each voxel: for vox in xrange(self._flat_signal.shape[0]): # We do this in each voxel (instead of all at once, which is # possible...) to not blow up the memory: vox_fits = np.empty(self.rotations.shape) for rot_i, rot in enumerate(self.rotations): if self.mode == 'log': this_sig = ( np.exp(b_w[rot_i, vox] * rot + self.regressors[0][0] * i_w[rot_i, vox]) * self._flat_S0[vox]) else: this_relative = ( b_w[rot_i, vox] * rot + self.regressors[0][0] * i_w[rot_i, vox]) if self.mode == 'signal_attenuation': this_relative = 1 - this_relative this_sig = this_relative * self._flat_S0[vox] vox_fits[rot_i] = this_sig # Find the predicted signal that best matches the original # relative signal. That will choose the direction for the # tensor we use: corrs = ozu.coeff_of_determination(self._flat_signal[vox], vox_fits) idx = np.where(corrs == np.nanmax(corrs))[0] # Sometimes there is no good solution (maybe we need to fit # just an isotropic to all of these?): if len(idx): # In case more than one fits the bill, just choose the # first one: if len(idx) > 1: idx = idx[0] params[vox, :] = np.array( [idx, b_w[idx, vox], i_w[idx, vox]]).squeeze() else: params[vox, :] = np.array([np.nan, np.nan, np.nan]) if self.verbose: prog_bar.animate(vox, f_name=f_name) # Save the params for future use: out_params = ozu.nans(self.signal.shape[:3] + (3, )) out_params[self.mask] = np.array(params).squeeze() params_ni = ni.Nifti1Image(out_params, self.affine) if self.params_file != 'temp': if self.verbose: print("Saving params to file: %s" % self.params_file) params_ni.to_filename(self.params_file) # And return the params for current use: return out_params
def isotropic_params(data, bvals, bvecs, mask, func, factor=1000, initial="preset", bounds="preset", params_file='temp', signal="relative_signal"): """ Finds the parameters of the given function to the given data that minimizes the sum squared errors. Parameters ---------- data: 4 dimensional array Diffusion MRI data bvals: 1 dimensional array All b values mask: 3 dimensional array Brain mask of the data func: str or callable String indicating the mean model function to perform kfold cross-validation on. initial: tuple Initial values for the parameters. factor: int Integer indicating the scaling factor for the b values bounds: list List containing tuples indicating the bounds for each parameter in the mean model function. Returns ------- param_out: 2 dimensional array Parameters that minimize the residuals fit_out: 2 dimensional array Model fitted means ss_err: 2 dimensional array Sum squared error between the model fitted means and the actual means """ if isinstance(func, str): # Grab the function handle for the desired mean model func = globals()[func] # Get the initial values for the desired mean model if (bounds == "preset") | (initial == "preset"): all_params = initial_params(data, bvecs, bvals, func, mask=mask, params_file=params_file) if bounds == "preset": bounds = all_params[0] if initial == "preset": func_initial = all_params[1] else: this_initial = initial # Separate b values and grab their indices bval_list, b_inds, unique_b, rounded_bvals = ozu.separate_bvals(bvals) all_b_idx, b0_inds = _diffusion_inds(bvals, b_inds, rounded_bvals) # Divide the b values by a scaling factor first. b = bvals[all_b_idx] / factor flat_data = data[np.where(mask)] # Get the number of inputs to the mean diffusivity function param_num = len(inspect.getargspec(func)[0]) # Pre-allocate the outputs: param_out = np.zeros((int(np.sum(mask)), param_num - 1)) cod = ozu.nans(np.sum(mask)) fit_out = ozu.nans(cod.shape + (len(all_b_idx), )) prog_bar = ozu.ProgressBar(flat_data.shape[0]) for vox in np.arange(np.sum(mask)).astype(int): prog_bar.animate(vox) s0 = np.mean(flat_data[vox, b0_inds], -1) if initial == "preset": this_initial = func_initial[vox] input_signal = flat_data[vox, all_b_idx] / s0 if signal == "log": input_signal = np.log(input_signal) if bounds == None: params, _ = opt.leastsq(err_func, this_initial, args=(b, input_signal, func)) else: lsq_b_out = lsq.leastsqbound(err_func, this_initial, args=(b, input_signal, func), bounds=bounds) params = lsq_b_out[0] param_out[vox] = np.squeeze(params) fit_out[vox] = func(b, *params) cod[vox] = ozu.coeff_of_determination(input_signal, fit_out[vox]) return param_out, fit_out, cod