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 1 commit
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
100 changes: 89 additions & 11 deletions dmipy/core/modeling_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -2205,11 +2205,15 @@ class MultiCompartmentAMICOModel(MultiCompartmentModelProperties):
the models to combine into the MultiCompartmentModel.
S0_tissue_responses: list of N values,
the S0 response of the tissue modelled by each compartment.
Nt: int
Number of equidistant sampling points over each
parameter range used to create atoms of observation matrix M
parameter_links : list of iterables (model, parameter name, link function,
argument list),
deprecated, for testing only.
"""
def __init__(self, models, S0_tissue_responses=None, parameter_links=None):
def __init__(self, models, S0_tissue_responses=None, Nt=10,
parameter_links=None):
self.models = models
self.N_models = len(models)
if S0_tissue_responses is not None:
Expand All @@ -2219,6 +2223,7 @@ def __init__(self, models, S0_tissue_responses=None, parameter_links=None):
raise ValueError(
msg.format(len(S0_tissue_responses), self.N_models))
self.S0_tissue_responses = S0_tissue_responses
self.Nt = Nt
self.parameter_links = parameter_links
if parameter_links is None:
self.parameter_links = []
Expand All @@ -2228,6 +2233,12 @@ def __init__(self, models, S0_tissue_responses=None, parameter_links=None):
self._prepare_parameter_links()
self._prepare_model_properties()
self._check_for_double_model_class_instances()

# QUESTION: I think we should create multi-compartment model
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

# somewhere in __init__ to avoid creating it every time we create
# forward model matrix
self.mc_model = MultiCompartmentModel(models=self.models)

self._prepare_parameters_to_optimize()
self._check_for_NMR_and_other_models()
self.x0_parameters = {}
Expand Down Expand Up @@ -2271,16 +2282,83 @@ def amico_idx(self):
matrix."""
return self._amico_idx

def forward_model_matrix(self, *args, **kwargs):
# TODO: move the creation of the forward model matrix from the optimizer
# to here. At the same time, instantiate the parameter grid and
# indices.
# - Create the forward model matrix that will be returned
# - Instantiate the self._amico_grid and self._amico_idx attributes
# - Save the "freezed" parameters vector as a hidden attribute and
# check if it has changed since the last call.
# Attribute: self._freezed_parameters_vector
raise NotImplementedError
def forward_model_matrix(self, acquisition_scheme, model_dirs, **kwargs):
"""Creates forward model matrix, including parameter tessellation grid
and corresponding indices.
"""
"""
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Double docstring. Just needs to be merged.

Copy link
Collaborator

Choose a reason for hiding this comment

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

what do you mean? you just want to merge this in so you can continue on the other branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, actually that the two docstrings should be merged in a single docstring.

Arguments:
acquisition_scheme: instance containing acquisition protocol
model_dirs: list containing direction of all models in
multi-compartment model
Returns:
observation matrix M
"""

dir_params = [p for p in self.mc_model.parameter_names
if p.endswith('mu')]
if len(dir_params) != len(model_dirs):
raise ValueError("Length of model_dirs should correspond "
"to the number of directional parameters!")

if not self._freezed_parameters_vector:
self._amico_grid, self._amico_idx = {}, {}

grid_params = \
[p for p in self.mc_model.parameter_names
if not p.endswith('mu') and
not p.startswith('partial_volume')]

# Compute length of the vector x0
x0_len = 0
for m_idx in range(self.N_models):
m_atoms = 1
for p in self.models[m_idx].parameter_names:
if self.mc_model.model_names[m_idx] + p in grid_params:
m_atoms *= self.Nt
x0_len += m_atoms

for m_idx in range(self.N_models):
model = self.models[m_idx]
model_name = self.mc_model.model_names[m_idx]

param_sampling, grid_params_names = [], []
m_atoms = 1
for p in model.parameter_names:
if model_name + p not in grid_params:
continue
grid_params_names.append(model_name + p)
p_range = self.mc_model.parameter_ranges[model_name + p]
self._amico_grid[model_name + p] = \
np.full(x0_len, np.mean(p_range))
param_sampling.append(np.linspace(p_range[0], p_range[1],
self.Nt, endpoint=True))
m_atoms *= self.Nt

self._amico_idx[model_name] =\
sum([len(self._amico_idx[k])
for k in self._amico_idx]) + \
np.arange(m_atoms)
params_mesh = np.meshgrid(*param_sampling)
for p_idx, p in enumerate(grid_params_names):
self.grids[p][self._amico_idx[model_name]] =\
np.ravel(params_mesh[p_idx])

self._amico_grid['partial_volume_' + str(m_idx)] = \
np.zeros(x0_len)
self._amico_grid['partial_volume_' +
str(m_idx)][self._amico_idx[model_name]] = 1.
self._freezed_parameters_vector = True

for d_idx, dp in enumerate(dir_params):
self._amico_grids[dp] = model_dirs[d_idx]

# QUESTION:
# Should we return simulated signals with b=0 values
# or only diffusion weighted, I think b=0 impacts a lot
# results when solving nnls
Copy link
Collaborator

Choose a reason for hiding this comment

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

why should it matter to nnls?

Copy link
Contributor

Choose a reason for hiding this comment

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

Because b=0 have higher values than diffusion weighted values (and less affected by noise), so it will force that sum of b=0 values of atoms is equal to 1 and in this way will prevent that some atoms are fitted to noise. Also, there is a difference if we use for example 18 b=0 values (as in HCP dataset) or just single b=0 value that is equal to the mean of all b=0 values (that would be 1 after normalization), because the higher the number of b=0 values, nnls will give more importance to fitting b=0 parts of the atoms.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So you vote for keeping the b=0 measurements then, if I understand correctly?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with this also.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but maybe it is something that we can investigate

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 we can leave the default as being just sample anything that is in the acquisition scheme.
we can test the effect of having more B0s by just making a scheme with more or less b0s in it.

return self.mc_model.simulate_signal(acquisition_scheme,
self._amico_grid).T

def fit(self, acquisition_scheme, data,
mask=None,
Expand Down
59 changes: 2 additions & 57 deletions dmipy/optimizers/amico_cvxpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,73 +47,18 @@ class AmicoCvxpyOptimizer:
Learning Research 17.1 (2016): 2909-2913.
"""

def __init__(self, acquisition_scheme, model, x0_vector=None,
lambda_1=None, lambda_2=None, Nt=10):
def __init__(self, acquisition_scheme, model,
lambda_1=None, lambda_2=None):
self.model = model
self.acquisition_scheme = acquisition_scheme

if len(lambda_1) != self.model.N_models or \
len(lambda_2) != self.model.N_models:
raise ValueError("Number of regularization weights should"
"correspond to the number of compartments!")

self.lambda_1 = lambda_1
self.lambda_2 = lambda_2

self.Nt = Nt

# Make a list of parameters that are not fixed and that require
# tessellation of parameter ranges
dir_params = [p for p in self.model.parameter_names
if p.endswith('mu')]
grid_params =\
[p for p in self.model.parameter_names
if not p.endswith('mu') and not p.startswith('partial_volume')]

# Compute length of the vector x0
x0_len = 0
for m_idx in range(self.model.N_models):
m_atoms = 1
for p in self.model.models[m_idx].parameter_names:
if self.model.model_names[m_idx] + p in grid_params:
m_atoms *= Nt
x0_len += m_atoms

# Creating parameter tessellation grids and corresponding indices
# TODO: move the matrix/grid/indices definition to the
# modeling_framework module.
self.grids, self.idx = {}, {}
for m_idx in range(self.model.N_models):
model = self.model.models[m_idx]
model_name = self.model.model_names[m_idx]

param_sampling, grid_params_names = [], []
m_atoms = 1
for p in model.parameter_names:
if model_name + p not in grid_params:
continue
grid_params_names.append(model_name + p)
p_range = self.model.parameter_ranges[model_name + p]
self.grids[model_name + p] = np.full(x0_len, np.mean(p_range))
param_sampling.append(np.linspace(p_range[0], p_range[1],
self.Nt, endpoint=True))
m_atoms *= self.Nt

self.idx[model_name] =\
sum([len(self.idx[k]) for k in self.idx]) + np.arange(m_atoms)
params_mesh = np.meshgrid(*param_sampling)
for p_idx, p in enumerate(grid_params_names):
self.grids[p][self.idx[model_name]] =\
np.ravel(params_mesh[p_idx])

self.grids['partial_volume_' + str(m_idx)] = np.zeros(x0_len)
self.grids['partial_volume_' +
str(m_idx)][self.idx[model_name]] = 1.

self.grids[dir_params[0]] = [0, 0]
self.M = self.model.simulate_signal(acquisition_scheme, self.grids)
self.M = self.M[:, ~acquisition_scheme.b0_mask].T

def __call__(self, data):
"""
The fitting function of AMICO optimizer.
Expand Down