Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: add skeleton for amico implementation #89

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3b8210a
FEAT: add skeleton for amico implementation
matteofrigo Apr 30, 2020
1a72f0d
add dmipy.utils.build_sphere.get_sphere function
matteofrigo Apr 30, 2020
f12ce31
remove AMICO-SM and add change definition of parameters in AMICO
matteofrigo May 10, 2020
3fa0f81
add details to AMICO skeleton
matteofrigo May 17, 2020
090b123
Merge branch 'master' of github.com:AthenaEPI/dmipy into amico
matteofrigo May 24, 2020
66f67e6
Merge branch 'master' of github.com:AthenaEPI/dmipy into amico
matteofrigo Jun 3, 2020
6a101b2
implemented predict and check orientations for AMICO
matteofrigo Jun 3, 2020
4ef1dbe
add comments to prepare amico interface
matteofrigo Jun 4, 2020
0278fa1
add specifications about interfacing amico solver
matteofrigo Jun 4, 2020
b398592
Move creation of forward model matrix
Sara04 Jun 8, 2020
873fb4e
Remove questions; fix typos
Sara04 Jun 9, 2020
4a0d277
Merge branch 'master' of github.com:AthenaEPI/dmipy into amico
matteofrigo Jun 26, 2020
02ad74e
setup AmicoCvxpyOptimizer in MultiCompartmentAMICOModel
matteofrigo Aug 18, 2020
85994d9
fix regularization parameter bug in AMICO framework
matteofrigo Aug 18, 2020
55b76fe
fix set_X_parameter in MultiCompartmentAMICOModel
matteofrigo Aug 19, 2020
9958209
fix parameter indices in FittedMultiCompartmentAMICOModel
matteofrigo Aug 19, 2020
626444f
refactor distribution variable in AmicoCvxpyOptimizer
matteofrigo Aug 19, 2020
d1b7100
Fix fitted parameters conversion from array to dictionary
Sara04 Aug 20, 2020
4e1a38d
remove forward model matrix from FittedMultiCompartmentAMICOModel
matteofrigo Aug 20, 2020
08d74e1
add directions to input of forward_model_matrix in AMICO models
matteofrigo Aug 21, 2020
735b2a1
Check if non-directional fixed parameters are unique for all voxels; …
Sara04 Aug 21, 2020
99168c0
normalize dictionary in AmicoCvxpyOptimizer
matteofrigo Aug 25, 2020
fe3f35c
fix unexpected behaviour of set_fixed_parameter of AMICO models
matteofrigo Aug 25, 2020
6d65658
change dictionary normalization of AmicoCvxpyOptimizer from l1 to l2
matteofrigo Aug 25, 2020
51aa0ab
Check if partial volume parameters are set, check if there are parame…
Sara04 Aug 27, 2020
692bb15
Add scaling of parameters
Sara04 Aug 27, 2020
3cee2c8
Add possibility to estimate circular parameters (as in Bingham distri…
Sara04 Aug 27, 2020
307958c
Fix counting of total number of atoms; increase max number of atoms
Sara04 Aug 27, 2020
0e1f5bf
Add tests for forward model matrix for ball and stick model and NODDI…
Sara04 Aug 27, 2020
70d56cf
fix initial condition error test amico
matteofrigo Sep 8, 2020
e7a506b
FIX: forbid setting tortuosity from MultiCompartmentAMICOModel
matteofrigo Oct 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions dmipy/core/fitted_modeling_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,3 +897,246 @@ def mean_squared_error(self, data):
mse = np.mean((data_ - y_hat) ** 2, axis=-1)
mse[~self.mask] = 0
return mse

class FittedMultiCompartmentAMICOModel:
"""
The FittedMultiCompartmentModel instance contains information about the
original MultiCompartmentModel, the estimated S0 values, the fitting mask
and the fitted model parameters.

Parameters
----------
model : MultiCompartmentAMICOModel instance,
A dmipy MultiCompartmentAMICOModel.
S0 : array of size (Ndata,) or (N_data, N_DWIs),
Array containing the estimated S0 values of the data. If data is 4D,
then S0 is 3D if there is only one TE, and the same 4D size of the data
if there are multiple TEs.
mask : array of size (N_data,),
boolean mask of voxels that were fitted.
fitted_parameters_vector : array of size (N_data, Nparameters),
fitted model parameters array.
"""

def __init__(self, model, S0, mask, fitted_parameters_vector,
fitted_multi_tissue_fractions_vector=None):
self.model = model
self.S0 = S0
self.mask = mask
self.fitted_parameters_vector = fitted_parameters_vector
self.fitted_multi_tissue_fractions_vector = (
fitted_multi_tissue_fractions_vector)

@property
def fitted_distribution(self):
"""Returns the distribution of each parameter."""
# TODO: we have to find a way to return the distribution of each
# parameter obtained from the optimization
raise NotImplementedError

@property
def fitted_parameters(self):
"Returns the fitted parameters as a dictionary."
rutgerfick marked this conversation as resolved.
Show resolved Hide resolved
fitted = {}
for pname in self.model.parameter_names:
prange = self.model.parameter_ranges[pname]
idx = self.model.parameter_indices[pname]
px = self.fitted_parameters_vector[idx]
value = np.zeros_like(prange[0])
for v, x in zip(prange, px): # TODO: can be optimized by using dot
value += v * x
value /= np.sum(px, axis=-1)
# By summing over the last axis, we assume the x have been saved
# in a compatible format, namely with one vector per voxel in the
# last direction.
fitted[pname] = value
return fitted

@property
def fitted_multi_tissue_fractions(self):
"Returns the fitted multi tissue fractions as a dictionary."
return self._return_fitted_multi_tissue_fractions()

@property
def fitted_multi_tissue_fractions_normalized(self):
"Returns the normalized fitted multi tissue fractions as a dictionary"
return self._return_fitted_multi_tissue_fractions(normalized=True)

def _return_fitted_multi_tissue_fractions(self, normalized=False):
"""
Returns the multi-tissue estimated volume fractions.

Parameters
----------
normalized: boolean,
whether or not to normalize returned multi-tissue volume fractions.
NOTE: This does not mean the unity constraint was enforced during
the estimation of the fractions - it is just a normalization after
the fact.

Returns
-------
mt_partial_volumes: dict,
contains the multi-tissue volume fractions by name.
NOTE: if the MC-model only consisted of 1 model, then the name will
be 'partial_volume_0', but will not have a counterpart in
self.fitted_parameters.
"""
mt_fract_vec = self.fitted_multi_tissue_fractions_vector.copy()
if normalized:
mt_fract_sum = np.sum(mt_fract_vec, axis=-1)
mt_fract_mask = mt_fract_sum > 0
mt_fract_vec[mt_fract_mask] = (
mt_fract_vec[mt_fract_mask] /
mt_fract_sum[mt_fract_mask][..., None])
if self.model.N_models > 1:
fract_names = self.model.partial_volume_names
else:
fract_names = ['partial_volume_0']
mt_partial_volumes = {}
for i, partial_volume_name in enumerate(fract_names):
mt_partial_volumes[partial_volume_name] = mt_fract_vec[..., i]
return mt_partial_volumes

@property
def fitted_and_linked_parameters(self):
"Returns the fitted and linked parameters as a dictionary."
fitted_parameters = self.model.parameter_vector_to_parameters(
self.fitted_parameters_vector)
return self.model.add_linked_parameters_to_parameters(
fitted_parameters)

def fod(self, vertices, visual_odi_lower_bound=0.):
rutgerfick marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns the Fiber Orientation Distribution if it is available.

Parameters
----------
vertices : array of size (Nvertices, 3),
Array of cartesian unit vectors at which to sample the FOD.
visual_odi_lower_bound : float,
gives a lower bound to the Orientation Distribution Index (ODI) of
FODs of Watson and Bingham distributions. This can be useful to
visualize FOD fields where some FODs are extremely sharp.

Returns
-------
fods : array of size (Ndata, Nvertices),
the FODs of the fitted model, scaled by volume fraction.
"""
raise NotImplementedError

def fod_sh(self, sh_order=8, basis_type=None):
"""
Returns the spherical harmonics coefficients of the Fiber Orientation
Distribution (FOD) if it is available. Uses are 724 spherical
tessellation to do the spherical harmonics transform.

Parameters
----------
sh_order : integer,
the maximum spherical harmonics order of the coefficient expansion.
basis_type : string,
type of spherical harmonics basis to use for the expansion, see
sh_to_sf_matrix for more info.

Returns
-------
fods_sh : array of size (Ndata, Ncoefficients),
rutgerfick marked this conversation as resolved.
Show resolved Hide resolved
spherical harmonics coefficients of the FODs, scaled by volume
fraction.
"""
if not self.model.fod_available:
msg = ('FODs not available for current model.')
raise ValueError(msg)
sphere = get_sphere(name='repulsion724')
vertices = sphere.vertices
_, inv_sh_matrix = sh_to_sf_matrix(
sphere, sh_order, basis_type=basis_type, return_inv=True)
fods_sf = self.fod(vertices)

dataset_shape = self.fitted_parameters_vector.shape[:-1]
number_coef_used = int((sh_order + 2) * (sh_order + 1) // 2)
fods_sh = np.zeros(np.r_[dataset_shape, number_coef_used])
mask_pos = np.where(self.mask)
for pos in zip(*mask_pos):
fods_sh[pos] = np.dot(inv_sh_matrix.T, fods_sf[pos])
return fods_sh

def peaks_spherical(self):
"Returns the peak angles of the model."
rutgerfick marked this conversation as resolved.
Show resolved Hide resolved
mu_params = []
for name, card in self.model.parameter_cardinality.items():
if name[-2:] == 'mu' and card == 2:
mu_params.append(self.fitted_parameters[name])
if len(mu_params) == 0:
msg = ('peaks not available for current model.')
raise ValueError(msg)
if len(mu_params) == 1:
return np.expand_dims(mu_params[0], axis=-2)
return np.concatenate([mu[..., None] for mu in mu_params], axis=-1)

def peaks_cartesian(self):
"Returns the cartesian peak unit vectors of the model."
rutgerfick marked this conversation as resolved.
Show resolved Hide resolved
peaks_spherical = self.peaks_spherical()
peaks_cartesian = unitsphere2cart_Nd(peaks_spherical)
return peaks_cartesian

def predict(self, acquisition_scheme=None, S0=None, mask=None):
rutgerfick marked this conversation as resolved.
Show resolved Hide resolved
"""
simulates the dMRI signal of the fitted MultiCompartmentModel for the
estimated model parameters. If no acquisition_scheme is given, then
the same acquisition_scheme that was used for the fitting is used. If
no S0 is given then it is assumed to be the estimated one. If no mask
is given then all voxels are assumed to have been fitted.

Parameters
----------
acquisition_scheme : DmipyAcquisitionScheme instance,
An acquisition scheme that has been instantiated using dMipy.
S0 : None or float,
Signal intensity without diffusion sensitization. If None, uses
estimated SO from fitting process. If float, uses that value.
mask : (N-1)-dimensional integer/boolean array of size (N_x, N_y, ...),
mask of voxels to simulate data at.

Returns
-------
predicted_signal : array of size (Ndata, N_DWIS),
predicted DWIs for the given model parameters and acquisition
scheme.
"""
if acquisition_scheme is None:
# TODO: for each voxel, multiply the forward model matrix by the
# corresponding x.
pass
else:
# TODO: recompute the forward model matrix on the given set of
# orientations. Also, the coefficients for each orientation must
# be resampled.
pass
raise NotImplementedError
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the predict function should be nonimplemented in this first MR since it addresses basic functionality of the fitted model.

Since in the first version we will not even have the lookup table, we will have to calculate the forward model matrix no matter if acquisition_scheme=None or something else.

The code you wrote here appropriate for the follow-up MR with look-up table.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's define if the LUT is part of the first MR or not ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agreed on no LUT for the moment.

Anyway, we need to think this properly. If we have the parameters fitted on a predefined set of orientations, how do we "register" this to the new acquisition scheme? Do we want to use a spherical harmonics representation and resample it accordingly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still, it would be tricky for different shells

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the function should look something like

if acquisition_scheme is None:
    acquisition_scheme = self.scheme
E_shape = self.model.mask.shape + (acquisition_scheme.N_dwi,)
E = np.zeros(E_shape)
x, y, z = np.where(self.mask)
for x_, y_, z_ in zip(x, y, z):
    parameter_array = self.fitted_parameter_vectors[x_, y_, z_]
    M = self.forward_model(xxxxx)  # same way as we instantiated it before the fitting
    E[x_, y_, z_] = M.dot(parameter_array)
return E


def R2_coefficient_of_determination(self, data):
"Calculates the R-squared of the model fit."
data_ = data / self.S0[..., None]

y_hat = self.predict(S0=1.)
y_bar = np.mean(data_, axis=-1)
SStot = np.sum((data_ - y_bar[..., None]) ** 2, axis=-1)
SSres = np.sum((data_ - y_hat) ** 2, axis=-1)
R2 = 1 - SSres / SStot
R2[~self.mask] = 0
return R2

def mean_squared_error(self, data):
"Calculates the mean squared error of the model fit."
if self.model.scheme.TE is None:
data_ = data / self.S0[..., None]
else:
data_ = data / self.S0

y_hat = self.predict(S0=1.)
mse = np.mean((data_ - y_hat) ** 2, axis=-1)
mse[~self.mask] = 0
return mse
Loading