Exemplo n.º 1
0
    def _process(self, roi_indices, next_indices=None):
        build_model = self._model.build(roi_indices)
        codec = self._model.get_parameter_codec()

        x0 = codec.encode(build_model.get_initial_parameters(),
                          build_model.get_kernel_data())

        cl_runtime_info = CLRuntimeInfo()

        self._logger.info('Starting minimization')
        self._logger.info('Using MOT version {}'.format(mot.__version__))
        self._logger.info(
            'We will use a {} precision float type for the calculations.'.
            format('double' if cl_runtime_info.double_precision else 'single'))
        for env in cl_runtime_info.get_cl_environments():
            self._logger.info('Using device \'{}\'.'.format(str(env)))
        self._logger.info('Using compile flags: {}'.format(
            cl_runtime_info.get_compile_flags()))

        if self._optimizer_options:
            self._logger.info('We will use the optimizer {} '
                              'with optimizer settings {}'.format(
                                  self._method, self._optimizer_options))
        else:
            self._logger.info(
                'We will use the optimizer {} with default settings.'.format(
                    self._method))

        objective_func = wrap_objective_function(
            build_model.get_objective_function(), codec.get_decode_function(),
            x0.shape[1])

        results = minimize(objective_func,
                           x0,
                           method=self._method,
                           nmr_observations=build_model.get_nmr_observations(),
                           cl_runtime_info=cl_runtime_info,
                           data=build_model.get_kernel_data(),
                           options=self._optimizer_options)

        self._logger.info('Finished optimization')
        self._logger.info('Starting post-processing')

        x_final = codec.decode(results['x'], build_model.get_kernel_data())

        results = build_model.get_post_optimization_output(
            x_final, results['status'])
        results.update({
            self._used_mask_name:
            np.ones(roi_indices.shape[0], dtype=np.bool)
        })

        self._logger.info('Finished post-processing')

        self._write_output_recursive(results, roi_indices)
Exemplo n.º 2
0
    def _calculate_cl(self, test_params, double_precision=False):
        func = mdt.lib.components.get_component('library_functions', 'VanGelderenCylinder')()

        names = ['Delta', 'delta', 'd', 'R', 'G']
        input_data = dict(zip(names, [Array(test_params[..., ind], as_scalar=True)
                                      for ind in range(test_params.shape[1])]))

        return func.evaluate(input_data, test_params.shape[0],
                             cl_runtime_info=CLRuntimeInfo(double_precision=double_precision))
Exemplo n.º 3
0
Arquivo: base.py Projeto: 42n4/MOT
    def __init__(self,
                 ll_func,
                 log_prior_func,
                 x0,
                 data=None,
                 cl_runtime_info=None,
                 **kwargs):
        """Abstract base class for sample routines.

        Sampling routines implementing this interface should be stateful objects that, for the given likelihood
        and prior, keep track of the sample state over multiple calls to :meth:`sample`.

        Args:
            ll_func (mot.lib.cl_function.CLFunction): The log-likelihood function. A CL function with the signature:

                .. code-block:: c

                        double <func_name>(local const mot_float_type* const x, void* data);

            log_prior_func (mot.lib.cl_function.CLFunction): The log-prior function. A CL function with the signature:

                .. code-block:: c

                    mot_float_type <func_name>(local const mot_float_type* const x, void* data);

            x0 (ndarray): the starting positions for the sampler. Should be a two dimensional matrix
                with for every modeling instance (first dimension) and every parameter (second dimension) a value.
            data (mot.lib.kernel_data.KernelData): the user provided data for the ``void* data`` pointer.
        """
        self._cl_runtime_info = cl_runtime_info or CLRuntimeInfo()
        self._logger = logging.getLogger(__name__)
        self._ll_func = ll_func
        self._log_prior_func = log_prior_func
        self._data = data
        self._x0 = x0
        if len(x0.shape) < 2:
            self._x0 = self._x0[..., None]
        self._nmr_problems = self._x0.shape[0]
        self._nmr_params = self._x0.shape[1]
        self._sampling_index = 0

        float_type = self._cl_runtime_info.mot_float_dtype
        self._current_chain_position = np.require(np.copy(self._x0),
                                                  requirements='CAOW',
                                                  dtype=float_type)
        self._current_log_likelihood = np.zeros(self._nmr_problems,
                                                dtype=float_type)
        self._current_log_prior = np.zeros(self._nmr_problems,
                                           dtype=float_type)
        self._rng_state = np.random.uniform(low=np.iinfo(np.uint32).min,
                                            high=np.iinfo(np.uint32).max + 1,
                                            size=(self._nmr_problems,
                                                  6)).astype(np.uint32)

        self._initialize_likelihood_prior(self._current_chain_position,
                                          self._current_log_likelihood,
                                          self._current_log_prior)
Exemplo n.º 4
0
def minimize(func, x0, data=None, method=None, nmr_observations=None, cl_runtime_info=None, options=None):
    """Minimization of scalar function of one or more variables.

    Args:
        func (mot.lib.cl_function.CLFunction): A CL function with the signature:

            .. code-block:: c

                double <func_name>(local const mot_float_type* const x,
                                   void* data,
                                   local mot_float_type* objective_list);

            The objective list needs to be filled when the provided pointer is not null. It should contain
            the function values for each observation. This list is used by non-linear least-squares routines,
            and will be squared by the least-square optimizer. This is only used by the ``Levenberg-Marquardt`` routine.

        x0 (ndarray): Initial guess. Array of real elements of size (n, p), for 'n' problems and 'p'
            independent variables.
        data (mot.lib.kernel_data.KernelData): the kernel data we will load. This is returned to the likelihood function
            as the ``void* data`` pointer.
        method (str): Type of solver.  Should be one of:
            - 'Levenberg-Marquardt'
            - 'Nelder-Mead'
            - 'Powell'
            - 'Subplex'

            If not given, defaults to 'Powell'.

        nmr_observations (int): the number of observations returned by the optimization function.
            This is only needed for the ``Levenberg-Marquardt`` method.
        cl_runtime_info (mot.configuration.CLRuntimeInfo): the CL runtime information
        options (dict): A dictionary of solver options. All methods accept the following generic options:
                patience (int): Maximum number of iterations to perform.

    Returns:
        mot.optimize.base.OptimizeResults:
            The optimization result represented as a ``OptimizeResult`` object.
            Important attributes are: ``x`` the solution array.
    """
    if not method:
        method = 'Powell'

    cl_runtime_info = cl_runtime_info or CLRuntimeInfo()

    if len(x0.shape) < 2:
        x0 = x0[..., None]

    if method == 'Powell':
        return _minimize_powell(func, x0, cl_runtime_info, data, options)
    elif method == 'Nelder-Mead':
        return _minimize_nmsimplex(func, x0, cl_runtime_info, data, options)
    elif method == 'Levenberg-Marquardt':
        return _minimize_levenberg_marquardt(func, x0, nmr_observations, cl_runtime_info, data, options)
    elif method == 'Subplex':
        return _minimize_subplex(func, x0, cl_runtime_info, data, options)
    raise ValueError('Could not find the specified method "{}".'.format(method))
Exemplo n.º 5
0
    def __init__(self,
                 optimization_method,
                 input_data,
                 optimization_results,
                 nmr_samples,
                 model,
                 mask,
                 nifti_header,
                 output_dir,
                 tmp_storage_dir,
                 recalculate,
                 keep_samples=True,
                 optimizer_options=None):
        """The processing worker for model sample.

        Args:
            optimization_method: the optimization routine to use
            optimization_results (dict): the starting point for the bootstrapping method
            nmr_samples (int): the number of samples we would like to return.
        """
        super().__init__(mask, nifti_header, output_dir, tmp_storage_dir,
                         recalculate)
        self._logger = logging.getLogger(__name__)
        self._optimization_method = optimization_method
        self._input_data = input_data.get_subset(
            volumes_to_keep=model.get_used_volumes(input_data))
        self._roi_input_data = ROIMRIInputData.from_input_data(
            self._input_data)
        self._optimization_results = optimization_results
        self._nmr_samples = nmr_samples
        self._model = model
        self._write_volumes_gzipped = gzip_sampling_results()
        self._keep_samples = keep_samples
        self._logger = logging.getLogger(__name__)
        self._optimizer_options = optimizer_options
        self._sample_storage = None

        self._model.set_input_data(input_data)

        self._x_opt_array = self._model.param_dict_to_array(
            DeferredActionDict(
                lambda _, v: create_roi(v, self._input_data.mask),
                self._optimization_results))

        self._cl_runtime_info = CLRuntimeInfo()
        self._codec = self._model.get_mle_codec()
        self._lower_bounds, self._upper_bounds = self._codec.encode_bounds(
            self._model.get_lower_bounds(), self._model.get_upper_bounds())
        self._wrapper = ParameterDecodingWrapper(
            self._model.get_nmr_parameters(),
            self._codec.get_decode_function())
        self._objective_func = self._wrapper.wrap_objective_function(
            self._model.get_objective_function())
        self._constraints_func = self._wrapper.wrap_constraints_function(
            self._model.get_constraints_function())
Exemplo n.º 6
0
    def _calculate_cl(self, test_params, double_precision=False):
        ssfp = mdt.lib.components.get_component('library_functions', 'SSFP')()

        names = ['d', 'delta', 'G', 'TR', 'flip_angle', 'b1', 'T1', 'T2']
        input_data = dict(
            zip(names, [
                Array(test_params[..., ind], as_scalar=True)
                for ind in range(test_params.shape[1])
            ]))

        return ssfp.evaluate(
            input_data,
            test_params.shape[0],
            cl_runtime_info=CLRuntimeInfo(double_precision=double_precision))
Exemplo n.º 7
0
def apply_cl_function(cl_function,
                      kernel_data,
                      nmr_instances,
                      use_local_reduction=False,
                      cl_runtime_info=None):
    """Run the given function/procedure on the given set of data.

    This class will wrap the given CL function in a kernel call and execute that that for every data instance using
    the provided kernel data. This class will respect the read write setting of the kernel data elements such that
    output can be written back to the according kernel data elements.

    Args:
        cl_function (mot.lib.cl_function.CLFunction): the function to
            run on the datasets. Either a name function tuple or an actual CLFunction object.
        kernel_data (dict[str: mot.lib.kernel_data.KernelData]): the data to use as input to the function.
        nmr_instances (int): the number of parallel threads to run (used as ``global_size``)
        use_local_reduction (boolean): set this to True if you want to use local memory reduction in
             your CL procedure. If this is set to True we will multiply the global size (given by the nmr_instances)
             by the work group sizes.
        cl_runtime_info (mot.configuration.CLRuntimeInfo): the runtime information
    """
    cl_runtime_info = cl_runtime_info or CLRuntimeInfo()

    for param in cl_function.get_parameters():
        if param.name not in kernel_data:
            names = [param.name for param in cl_function.get_parameters()]
            missing_names = [name for name in names if name not in kernel_data]
            raise ValueError(
                'Some parameters are missing an input value, '
                'required parameters are: {}, missing inputs are: {}'.format(
                    names, missing_names))

    if cl_function.get_return_type() != 'void':
        kernel_data['_results'] = Zeros((nmr_instances, ),
                                        cl_function.get_return_type())

    workers = []
    for cl_environment in cl_runtime_info.get_cl_environments():
        workers.append(
            _ProcedureWorker(cl_environment,
                             cl_runtime_info.get_compile_flags(), cl_function,
                             kernel_data, cl_runtime_info.double_precision,
                             use_local_reduction))

    cl_runtime_info.load_balancer.process(workers, nmr_instances)

    if cl_function.get_return_type() != 'void':
        return kernel_data['_results'].get_data()
Exemplo n.º 8
0
    def __init__(self,
                 method,
                 model,
                 mask,
                 nifti_header,
                 output_dir,
                 tmp_storage_dir,
                 recalculate,
                 optimizer_options=None):
        """The processing worker for model fitting.

        Use this if you want to use the model processing strategy to do model fitting.

        Args:
            method: the optimization routine to use
        """
        super().__init__(mask, nifti_header, output_dir, tmp_storage_dir,
                         recalculate)
        self._model = model
        self._method = method
        self._optimizer_options = optimizer_options
        self._write_volumes_gzipped = gzip_optimization_results()
        self._subdirs = set()
        self._logger = logging.getLogger(__name__)

        self._cl_runtime_info = CLRuntimeInfo()
        self._codec = self._model.get_mle_codec()
        self._initial_params = self._model.get_initial_parameters()
        self._wrapper = ParameterDecodingWrapper(
            self._model.get_nmr_parameters(),
            self._codec.get_decode_function())
        self._kernel_data = self._model.get_kernel_data()
        self._lower_bounds, self._upper_bounds = self._codec.encode_bounds(
            self._model.get_lower_bounds(), self._model.get_upper_bounds())
        self._objective_func = self._wrapper.wrap_objective_function(
            self._model.get_objective_function())
        self._constraints_func = self._wrapper.wrap_constraints_function(
            self._model.get_constraints_function())
Exemplo n.º 9
0
    def __init__(self, channels, x0=None, cl_device_ind=None, **kwargs):
        """Reconstruct the input using the STARC method.

        Args:
            channels (list): the list of input nifti files, one for each channel element. Every nifti file
                    should be a 4d matrix with on the 4th dimension all the time series. The length of this list
                    should equal the number of input channels.
            x0 (ndarray or str): optional, the set of weights to use as a starting point for the fitting routine.
            cl_device_ind (int or list of int): the list of indices into :func:`mct.utils.get_cl_devices` that you want
                to use for the OpenCL based optimization.
        """
        super().__init__(channels, **kwargs)

        cl_environments = None
        if cl_device_ind is not None:
            if not isinstance(cl_device_ind, (tuple, list)):
                cl_device_ind = [cl_device_ind]
            cl_environments = [get_cl_devices()[ind] for ind in cl_device_ind]

        self.cl_runtime_info = CLRuntimeInfo(cl_environments=cl_environments)
        self._x0 = x0
        if isinstance(self._x0, str):
            self._x0 = mdt.load_nifti(x0).get_data()
Exemplo n.º 10
0
def minimize(func, x0, data=None, method=None, lower_bounds=None, upper_bounds=None, constraints_func=None,
             nmr_observations=None, cl_runtime_info=None, options=None, use_local_reduction=True):
    """Minimization of one or more variables.

    For an easy wrapper of function maximization, see :func:`maximize`.

    All boundary conditions are enforced using the penalty method. That is, we optimize the objective function:

    .. math::

        F(x) = f(x) \mu \sum \max(0, g_i(x))^2

    where :math:`F(x)` is the new objective function, :math:`f(x)` is the old objective function, :math:`g_i` are
    the boundary functions defined as :math:`g_i(x) \leq 0` and :math:`\mu` is the penalty weight.

    The penalty weight is by default :math:`\mu = 1e20` and can be set
    using the ``options`` dictionary as ``penalty_weight``.

    Args:
        func (mot.lib.cl_function.CLFunction): A CL function with the signature:

            .. code-block:: c

                double <func_name>(local const mot_float_type* const x,
                                   void* data,
                                   local mot_float_type* objective_list);

            The objective list needs to be filled when the provided pointer is not null. It should contain
            the function values for each observation. This list is used by non-linear least-squares routines,
            and will be squared by the least-square optimizer. This is only used by the ``Levenberg-Marquardt`` routine.

        x0 (ndarray): Initial guess. Array of real elements of size (n, p), for 'n' problems and 'p'
            independent variables.
        data (mot.lib.kernel_data.KernelData): the kernel data we will load. This is returned to the likelihood function
            as the ``void* data`` pointer.
        method (str): Type of solver.  Should be one of:
            - 'Levenberg-Marquardt'
            - 'Nelder-Mead'
            - 'Powell'
            - 'Subplex'

            If not given, defaults to 'Powell'.
        lower_bounds (tuple): per parameter a lower bound, if given, the optimizer ensures ``a <= x`` with
            a the lower bound and x the parameter. If not given, -infinity is assumed for all parameters.
            Each tuple element can either be a scalar or a vector. If a vector is given the first dimension length
            should match that of the parameters.
        upper_bounds (tuple): per parameter an upper bound, if given, the optimizer ensures ``x >= b`` with
            b the upper bound and x the parameter. If not given, +infinity is assumed for all parameters.
            Each tuple element can either be a scalar or a vector. If a vector is given the first dimension length
            should match that of the parameters.
        constraints_func (mot.optimize.base.ConstraintFunction): function to compute (inequality) constraints.
            Should hold a CL function with the signature:

            .. code-block:: c

                void <func_name>(local const mot_float_type* const x,
                                 void* data,
                                 local mot_float_type* constraints);

            Where ``constraints_values`` is filled as:

            .. code-block:: c

                constraints[i] = g_i(x)

            That is, for each constraint function :math:`g_i`, formulated as :math:`g_i(x) <= 0`, we should return
            the function value of :math:`g_i`.

        nmr_observations (int): the number of observations returned by the optimization function.
            This is only needed for the ``Levenberg-Marquardt`` method.
        cl_runtime_info (mot.configuration.CLRuntimeInfo): the CL runtime information
        options (dict): A dictionary of solver options. All methods accept the following generic options:
            - patience (int): Maximum number of iterations to perform.
            - penalty_weight (float): the weight of the penalty term for the boundary conditions
        use_local_reduction (boolean): set this to False if you do not want to use local memory reduction in
             the CL kernel. By default this is True and we use local reduction in the optimization routine and
             model function.

    Returns:
        mot.optimize.base.OptimizeResults:
            The optimization result represented as a ``OptimizeResult`` object.
            Important attributes are: ``x`` the solution array.
    """
    if not method:
        method = 'Powell'

    data = data or {}

    cl_runtime_info = cl_runtime_info or CLRuntimeInfo()

    if len(x0.shape) < 2:
        x0 = x0[..., None]

    lower_bounds = _bounds_to_array(lower_bounds or np.ones(x0.shape[1]) * -np.inf)
    upper_bounds = _bounds_to_array(upper_bounds or np.ones(x0.shape[1]) * np.inf)

    if method == 'Powell':
        return _minimize_powell(func, x0, cl_runtime_info, lower_bounds, upper_bounds,
                                use_local_reduction,
                                constraints_func=constraints_func, data=data, options=options)
    elif method == 'Nelder-Mead':
        return _minimize_nmsimplex(func, x0, cl_runtime_info, lower_bounds, upper_bounds,
                                   use_local_reduction,
                                   constraints_func=constraints_func, data=data, options=options)
    elif method == 'Levenberg-Marquardt':
        return _minimize_levenberg_marquardt(func, x0, nmr_observations, cl_runtime_info, lower_bounds, upper_bounds,
                                             use_local_reduction,
                                             constraints_func=constraints_func, data=data, options=options)
    elif method == 'Subplex':
        return _minimize_subplex(func, x0, cl_runtime_info, lower_bounds, upper_bounds,
                                 use_local_reduction,
                                 constraints_func=constraints_func, data=data, options=options)
    raise ValueError('Could not find the specified method "{}".'.format(method))
Exemplo n.º 11
0
    def __init__(self,
                 model,
                 input_data,
                 output_folder,
                 method=None,
                 optimizer_options=None,
                 recalculate=False,
                 only_recalculate_last=False,
                 cl_device_ind=None,
                 double_precision=False,
                 tmp_results_dir=True,
                 initialization_data=None,
                 post_processing=None):
        """Setup model fitting for the given input model and data.

        To actually fit the model call run().

        Args:
            model (str or :class:`~mdt.models.composite.DMRICompositeModel` or :class:`~mdt.models.cascade.DMRICascadeModelInterface`):
                    the model we want to optimize.
            input_data (:class:`~mdt.utils.MRIInputData`): the input data object containing
                all the info needed for the model fitting.
            output_folder (string): The full path to the folder where to place the output
            method (str): The optimization method to use, one of:
                - 'Levenberg-Marquardt'
                - 'Nelder-Mead'
                - 'Powell'
                - 'Subplex'

                If not given, defaults to 'Powell'.
            optimizer_options (dict): extra options passed to the optimization routines.
            recalculate (boolean): If we want to recalculate the results if they are already present.
            only_recalculate_last (boolean): If we want to recalculate all the models.
                This is only of importance when dealing with CascadeModels. If set to true we only recalculate
                the last element in the chain (if recalculate is set to True, that is). If set to false,
                we recalculate everything. This only holds for the first level of the cascade.
            cl_device_ind (int or list): the index of the CL device to use. The index is from the list from the function
                get_cl_devices(). This can also be a list of device indices.
            double_precision (boolean): if we would like to do the calculations in double precision
            tmp_results_dir (str, True or None): The temporary dir for the calculations. Set to a string to use
                that path directly, set to True to use the config value, set to None to disable.
            initialization_data (dict or :class:`~mdt.utils.InitializationData`): extra initialization data to use
                during model fitting. If we are optimizing a cascade model this data only applies to the last model in
                the cascade.
            post_processing (dict): a dictionary with flags for post-processing options to enable or disable.
                For valid elements, please see the configuration file settings for ``optimization``
                under ``post_processing``. Valid input for this parameter is for example: {'covariance': False}
                to disable automatic calculation of the covariance from the Hessian.

        """
        if isinstance(model, str):
            model = get_model(model)()

        if post_processing:
            model.update_active_post_processing('optimization',
                                                post_processing)

        self._model = model
        self._input_data = input_data
        self._output_folder = output_folder
        self._method = method
        self._optimizer_options = optimizer_options
        self._recalculate = recalculate
        self._only_recalculate_last = only_recalculate_last
        self._logger = logging.getLogger(__name__)

        self._model_names_list = []
        self._tmp_results_dir = get_temporary_results_dir(tmp_results_dir)

        if initialization_data is not None and not isinstance(
                initialization_data, InitializationData):
            self._initialization_data = SimpleInitializationData(
                **initialization_data)
        else:
            self._initialization_data = initialization_data

        if cl_device_ind is not None:
            self._cl_runtime_info = CLRuntimeInfo(
                cl_environments=get_cl_devices(cl_device_ind),
                double_precision=double_precision)
        else:
            self._cl_runtime_info = CLRuntimeInfo(
                double_precision=double_precision)

        if not model.is_input_data_sufficient(self._input_data):
            raise InsufficientProtocolError(
                'The provided protocol is insufficient for this model. '
                'The reported errors where: {}'.format(
                    self._model.get_input_data_problems(self._input_data)))
Exemplo n.º 12
0
def fit_model(model,
              input_data,
              output_folder,
              method=None,
              recalculate=False,
              cl_device_ind=None,
              double_precision=False,
              tmp_results_dir=True,
              initialization_data=None,
              use_cascaded_inits=True,
              post_processing=None,
              optimizer_options=None):
    """Run the optimizer on the given model.

    Args:
        model (str or :class:`~mdt.models.base.EstimableModel`):
            The name of a composite model or an implementation of a composite model.
        input_data (:class:`~mdt.utils.MRIInputData`): the input data object containing all
            the info needed for the model fitting.
        output_folder (string): The path to the folder where to place the output, we will make a subdir with the
            model name in it.
        method (str): The optimization method to use, one of:
            - 'Levenberg-Marquardt'
            - 'Nelder-Mead'
            - 'Powell'
            - 'Subplex'

            If not given, defaults to 'Powell'.

        recalculate (boolean): If we want to recalculate the results if they are already present.
        cl_device_ind (int or list): the index of the CL device to use. The index is from the list from the function
            utils.get_cl_devices(). This can also be a list of device indices.
        double_precision (boolean): if we would like to do the calculations in double precision
        tmp_results_dir (str, True or None): The temporary dir for the calculations. Set to a string to use
            that path directly, set to True to use the config value, set to None to disable.
        initialization_data (dict): provides (extra) initialization data to
            use during model fitting. This dictionary can contain the following elements:

            * ``inits``: dictionary with per parameter an initialization point
            * ``fixes``: dictionary with per parameter a fixed point, this will remove that parameter from the fitting
            * ``lower_bounds``: dictionary with per parameter a lower bound
            * ``upper_bounds``: dictionary with per parameter a upper bound
            * ``unfix``: a list of parameters to unfix

            For example::

                initialization_data = {
                    'fixes': {'Stick0.theta: np.array(...), ...},
                    'inits': {...}
                }

        use_cascaded_inits (boolean): if set, we initialize the model parameters using :func:`get_optimization_inits`.
            You can also overrule the default initializations using the ``initialization_data`` attribute.
        post_processing (dict): a dictionary with flags for post-processing options to enable or disable.
            For valid elements, please see the configuration file settings for ``optimization``
            under ``post_processing``. Valid input for this parameter is for example: {'covariance': False}
            to disable automatic calculation of the covariance from the Hessian.
        optimizer_options (dict): extra options passed to the optimization routines.

    Returns:
        dict: The result maps for the given composite model or the last model in the cascade.
            This returns the results as 3d/4d volumes for every output map.
    """
    logger = logging.getLogger(__name__)

    if not check_user_components():
        init_user_settings(pass_if_exists=True)

    if cl_device_ind is not None:
        if not isinstance(cl_device_ind, collections.Iterable):
            cl_device_ind = [cl_device_ind]
        cl_runtime_info = CLRuntimeInfo(
            cl_environments=get_cl_devices(cl_device_ind),
            double_precision=double_precision)
    else:
        cl_runtime_info = CLRuntimeInfo(double_precision=double_precision)

    if isinstance(model, str):
        model_name = model
        model_instance = get_model(model)()
    else:
        model_name = model.name
        model_instance = model

    if not model_instance.is_input_data_sufficient(input_data):
        raise InsufficientProtocolError(
            'The provided protocol is insufficient for this model. '
            'The reported errors where: {}'.format(
                model_instance.get_input_data_problems(input_data)))

    if post_processing:
        model_instance.update_active_post_processing('optimization',
                                                     post_processing)

    if use_cascaded_inits:
        if initialization_data is None:
            initialization_data = {}
        initialization_data['inits'] = initialization_data.get('inits', {})
        inits = get_optimization_inits(model_name,
                                       input_data,
                                       output_folder,
                                       cl_device_ind=cl_device_ind)
        inits.update(initialization_data['inits'])
        initialization_data['inits'] = inits

        initialization_data = SimpleInitializationData(**initialization_data)
        initialization_data.apply_to_model(model_instance, input_data)

        logger.info('Preparing {0} with the cascaded initializations.'.format(
            model_name))

    if method is None:
        method, optimizer_options = get_optimizer_for_model(model_name)

    with mot.configuration.config_context(CLRuntimeAction(cl_runtime_info)):
        fit_composite_model(model_instance,
                            input_data,
                            output_folder,
                            method,
                            get_temporary_results_dir(tmp_results_dir),
                            recalculate=recalculate,
                            optimizer_options=optimizer_options)

    return get_all_nifti_data(os.path.join(output_folder, model_name))
Exemplo n.º 13
0
def numerical_hessian(objective_func,
                      parameters,
                      lower_bounds=None,
                      upper_bounds=None,
                      step_ratio=2,
                      nmr_steps=15,
                      data=None,
                      max_step_sizes=None,
                      scaling_factors=None,
                      step_offset=None,
                      parameter_transform_func=None,
                      cl_runtime_info=None):
    """Calculate and return the Hessian of the given function at the given parameters.

    This calculates the Hessian using central difference (using a 2nd order Taylor expansion) with a Richardson
    extrapolation over the proposed sequence of steps.

    The Hessian is evaluated at the steps:

    .. math::
        \quad  ((f(x + d_j e_j + d_k e_k) - f(x + d_j e_j - d_k e_k)) -
                (f(x - d_j e_j + d_k e_k) - f(x - d_j e_j - d_k e_k)) /
                (4 d_j d_k)

    where :math:`e_j` is a vector where element :math:`j` is one and the rest are zero
    and :math:`d_j` is a scalar spacing :math:`steps_j`.

    Steps are generated according to a exponentially diminishing ratio defined as:

        steps = max_step * step_ratio**-(i+offset), i=0, 1,.., nmr_steps-1.

    Where the max step can be provided. For example, a maximum step of 2 with a step ratio of 2 and with
    4 steps gives: [2.0, 1.0, 0.5, 0.25]. If offset would be 2, we would instead get: [0.5, 0.25, 0.125, 0.0625].

    If number of steps is 1, we use not Richardson extrapolation and return the results of the first step. If the
    number of steps is 2 we use a first order Richardson extrapolation step. For all higher number of steps
    we use a second order Richardson extrapolation.

    The derivative calculation method uses an adaptive step size to determine the step with the best trade-off between
    numerical errors and localization of the derivative.

    Args:
        objective_func (mot.lib.cl_function.CLFunction): The function we want to differentiate.
            A CL function with the signature:

            .. code-block:: c

                double <func_name>(local const mot_float_type* const x,
                                   void* data,
                                   local mot_float_type* objective_list);

            The objective function has the same signature as the minimization function in MOT. For the numerical
            hessian, the ``objective_list`` parameter is ignored.

        parameters (ndarray): The parameters at which to evaluate the gradient. A (d, p) matrix with d problems,
            and p parameters
        lower_bounds (list or None): a list of length (p,) for p parameters with the lower bounds.
            Each element of the list can be a scalar, a vector (of the same length as the number of problem instances),
            or None. For infinity use np.inf, for boundless use None.
        upper_bounds (list or None): a list of length (p,) for p parameters with the upper bounds.
            Each element of the list can be a scalar, a vector (of the same length as the number of problem instances),
            or None. For infinity use np.inf, for boundless use None.
        step_ratio (float): the ratio at which the steps diminish.
        nmr_steps (int): the number of steps we will generate. We will calculate the derivative for each of these
            step sizes and extrapolate the best step size from among them. The minimum number of steps is 2.
        data (mot.lib.kernel_data.KernelData): the user provided data for the ``void* data`` pointer.
        step_offset (int): the offset in the steps, if set we start the steps from the given offset.
        max_step_sizes (float or ndarray or None): the maximum step size, or the maximum step size per parameter.
            If None is given, we use 0.1 for all parameters. If a float is given, we use that for all parameters.
            If a list is given, it should be of the same length as the number of parameters.
        scaling_factors (List[float] or ndarray): per estimable parameter a single float with the parameter scaling
            for that parameter. Use 1 as identity.

            Since numerical differentiation is sensitive to differences in step sizes, it is better to rescale
            the parameters to a unitary range instead of changing the step sizes for the parameters.
            This vector should contain scaling factors such that when the parameter is multiplied with this value,
            the order of magnitude of the parameter is about one.

        parameter_transform_func (mot.lib.cl_function.CLFunction or None): A transformation that can prepare the
            parameter plus/minus the proposed step before evaluation.

            As an example, suppose we are taking the derivative of a a polar coordinate :math:`\theta` defined on
            :math:`[0, 2\pi]`. While taking the derivative, the function might propose positions outside of the range
            of :math:`\theta`. This function allows changing the parameter vector before it is put into the model.

            Please note that this function should return a parameter vector that is equivalent (but not necessarily
            equal) to the provided proposal.

            Signature:

            .. code-block:: c

                void <func_name>(void* data, local mot_float_type* x);

        cl_runtime_info (mot.configuration.CLRuntimeInfo): the runtime information

    Returns:
        ndarray: the gradients for each of the parameters for each of the problems
    """
    if len(parameters.shape) == 1:
        parameters = parameters[None, :]
    nmr_params = parameters.shape[1]

    if max_step_sizes is None:
        max_step_sizes = 0.1
    if isinstance(max_step_sizes, Number):
        max_step_sizes = [max_step_sizes] * nmr_params
    max_step_sizes = np.array(max_step_sizes)

    if scaling_factors is None:
        scaling_factors = 1
    if isinstance(scaling_factors, Number):
        scaling_factors = [scaling_factors] * nmr_params
    scaling_factors = np.array(scaling_factors)

    def finalize_derivatives(derivatives):
        """Transforms the derivatives from vector to matrix and apply the parameter scalings."""
        return _results_vector_to_matrix(derivatives, nmr_params) * np.outer(
            scaling_factors, scaling_factors)

    with config_context(CLRuntimeAction(cl_runtime_info or CLRuntimeInfo())):
        derivatives = _compute_derivatives(
            objective_func,
            parameters,
            step_ratio,
            step_offset,
            nmr_steps,
            lower_bounds,
            upper_bounds,
            max_step_sizes,
            scaling_factors,
            data=data,
            parameter_transform_func=parameter_transform_func)

        if nmr_steps == 1:
            return finalize_derivatives(derivatives[..., 0])

        derivatives, errors = _richardson_extrapolation(
            derivatives, step_ratio)

        if nmr_steps <= 3:
            return finalize_derivatives(derivatives[..., 0])

        if derivatives.shape[2] > 2:
            derivatives, errors = _wynn_extrapolate(derivatives)

        if derivatives.shape[2] == 1:
            return finalize_derivatives(derivatives[..., 0])

        derivatives, errors = _median_outlier_extrapolation(
            derivatives, errors)
        return finalize_derivatives(derivatives)
Exemplo n.º 14
0
def compute_fim(model, input_data, optimization_results, output_folder=None, cl_device_ind=None, cl_load_balancer=None,
                initialization_data=None):
    """Compute the Fisher Information Matrix (FIM).

    This is typically done as post-processing step during the model fitting process, but can also be performed
    separately after optimization.

    Since the FIM depends on which parameters were optimized, results will change if different parameters are fixed.
    That is, this function will compute the FIM for every estimable parameter (free-non-fixed parameters). If you want
    to have the exact same FIM results as when you computed the FIM as optimization post-processing it is important
    to have exactly the same maps fixed.

    Contrary to the post-processing of the optimization maps, all FIM results are written to a single sub-folder in the
    provided output folder.

    Args:
        model (str or :class:`~mdt.models.base.EstimableModel`):
            The name of a composite model or an implementation of a composite model.
        input_data (:class:`~mdt.lib.input_data.MRIInputData`): the input data object containing all
            the info needed for the model fitting.
        optimization_results (dict or str): the optimization results, either a dictionary with results or the
            path to a folder.
        output_folder (string): Optionally, the path to the folder where to place the output
        cl_device_ind (List[Union[mot.lib.cl_environments.CLEnvironment, int]]
                             or mot.lib.cl_environments.CLEnvironment or int): the CL devices to use.
            Either provide MOT CLEnvironment's or indices from into the list from the function mdt.get_cl_devices().
        cl_load_balancer (mot.lib.load_balancers.LoadBalancer or Tuple[float]): the load balancer to use. Can also
            be an array of fractions (summing to 1) with one fraction per device. For example, for two devices one
            can specify ``cl_load_balancer = [0.3, 0.7]`` to let one device to more work than another.
        initialization_data (dict): provides (extra) initialization data to
            use during model fitting. This dictionary can contain the following elements:

            * ``inits``: dictionary with per parameter an initialization point
            * ``fixes``: dictionary with per parameter a fixed point, this will remove that parameter from the fitting
            * ``lower_bounds``: dictionary with per parameter a lower bound
            * ``upper_bounds``: dictionary with per parameter a upper bound
            * ``unfix``: a list of parameters to unfix

            For example::

                initialization_data = {
                    'fixes': {'Stick0.theta: np.array(...), ...},
                    'inits': {...}
                }

    Returns:
        dict: all the computed FIM maps in a flattened dictionary.
    """
    initialization_data = initialization_data or {}

    if isinstance(optimization_results, str):
        optimization_results = get_all_nifti_data(optimization_results)

    if not check_user_components():
        init_user_settings(pass_if_exists=True)

    cl_runtime_info = CLRuntimeInfo(cl_environments=cl_device_ind,
                                    double_precision=True,
                                    load_balancer=cl_load_balancer)

    if isinstance(model, str):
        model_name = model
        model_instance = get_model(model)()
    else:
        model_name = model.name
        model_instance = model

    model_instance.set_input_data(input_data)

    initialization_data = SimpleInitializationData(**initialization_data)
    initialization_data.apply_to_model(model_instance, input_data)

    with mot.configuration.config_context(CLRuntimeAction(cl_runtime_info)):
        opt_points = create_roi(optimization_results, input_data.mask)
        opt_array = combine_dict_to_array(opt_points, model_instance.get_free_param_names())

        covars = model_instance.compute_covariance_matrix(opt_array)
        covariance_names = model_instance.get_covariance_output_names()

        return_results = {}
        for ind, name in enumerate(covariance_names):
            if name.endswith('.std'):
                return_results[name] = np.nan_to_num(np.sqrt(covars[..., ind]))
            else:
                return_results[name] = covars[..., ind]

        return_results = restore_volumes(return_results, input_data.mask)
        write_volume_maps(return_results, os.path.join(output_folder, model_name, 'FIM'))

        return return_results
Exemplo n.º 15
0
def sample_model(model, input_data, output_folder, nmr_samples=None, burnin=None, thinning=None,
                 method=None, recalculate=False, cl_device_ind=None, cl_load_balancer=None, double_precision=False,
                 store_samples=True, sample_items_to_save=None, tmp_results_dir=True,
                 initialization_data=None, post_processing=None, post_sampling_cb=None,
                 sampler_options=None):
    """Sample a composite model using Markov Chain Monte Carlo sampling.

    Args:
        model (str or :class:`~mdt.models.base.EstimableModel`): the model to sample
        input_data (:class:`~mdt.lib.input_data.MRIInputData`): the input data object containing all
            the info needed for the model fitting.
        output_folder (string): The path to the folder where to place the output, we will make a subdir with the
            model name in it (for the optimization results) and then a subdir with the samples output.
        nmr_samples (int): the number of samples we would like to return.
        burnin (int): the number of samples to burn-in, that is, to discard before returning the desired
            number of samples
        thinning (int): how many sample we wait before storing a new one. This will draw extra samples such that
                the total number of samples generated is ``nmr_samples * (thinning)`` and the number of samples stored
                is ``nmr_samples``. If set to one or lower we store every sample after the burn in.
        method (str): The sampling method to use, one of:
            - 'AMWG', for the Adaptive Metropolis-Within-Gibbs
            - 'SCAM', for the Single Component Adaptive Metropolis
            - 'FSL', for the sampling method used in the FSL toolbox
            - 'MWG', for the Metropolis-Within-Gibbs (simple random walk metropolis without updates)

            If not given, defaults to 'AMWG'.

        recalculate (boolean): If we want to recalculate the results if they are already present.
        cl_device_ind (List[Union[mot.lib.cl_environments.CLEnvironment, int]]
                             or mot.lib.cl_environments.CLEnvironment or int): the CL devices to use.
            Either provide MOT CLEnvironment's or indices from into the list from the function mdt.get_cl_devices().
        cl_load_balancer (mot.lib.load_balancers.LoadBalancer or Tuple[float]): the load balancer to use. Can also
            be an array of fractions (summing to 1) with one fraction per device. For example, for two devices one
            can specify ``cl_load_balancer = [0.3, 0.7]`` to let one device to more work than another.
        double_precision (boolean): if we would like to do the calculations in double precision
        store_samples (boolean): determines if we store any of the samples. If set to False we will store none
            of the samples.
        sample_items_to_save (list): list of output names we want to store the samples of. If given, we only
            store the items specified in this list. Valid items are the free parameter names of the model and the
            items 'LogLikelihood' and 'LogPrior'.
        tmp_results_dir (str, True or None): The temporary dir for the calculations. Set to a string to use
                that path directly, set to True to use the config value, set to None to disable.
        initialization_data (dict): provides (extra) initialization data to
            use during model fitting. This dictionary can contain the following elements:

            * ``inits``: dictionary with per parameter an initialization point
            * ``fixes``: dictionary with per parameter a fixed point, this will remove that parameter from the fitting
            * ``lower_bounds``: dictionary with per parameter a lower bound
            * ``upper_bounds``: dictionary with per parameter a upper bound
            * ``unfix``: a list of parameters to unfix

            For example::

                initialization_data = {
                    'fixes': {'Stick0.theta: np.array(...), ...},
                    'inits': {...}
                }

        post_processing (dict): a dictionary with flags for post-processing options to enable or disable.
            For valid elements, please see the configuration file settings for ``sample`` under ``post_processing``.
            Valid input for this parameter is for example: {'univariate_normal': True} to enable automatic calculation
            of the univariate normal distribution for the model parameters.
        post_sampling_cb (Callable[
            [mot.sample.base.SamplingOutput, mdt.models.composite.DMRICompositeModel], Optional[Dict]]):
                additional post-processing called after sampling. This function can optionally return a (nested)
                dictionary with as keys dir-/file-names and as values maps to be stored in the results directory.
        sampler_options (dict): specific options for the MCMC routine. These will be provided to the sampling routine
            as additional keyword arguments to the constructor.

    Returns:
        dict: if store_samples is True then we return the samples per parameter as a numpy memmap. If store_samples
            is False we return None
    """
    initialization_data = initialization_data or {}

    if not check_user_components():
        init_user_settings(pass_if_exists=True)

    cl_runtime_info = CLRuntimeInfo(cl_environments=cl_device_ind,
                                    double_precision=double_precision,
                                    load_balancer=cl_load_balancer)

    settings = get_general_sampling_settings()
    if nmr_samples is None:
        nmr_samples = settings['nmr_samples']
    if burnin is None:
        burnin = settings['burnin']
    if thinning is None:
        thinning = settings['thinning']

    if isinstance(model, str):
        model_instance = get_model(model)()
    else:
        model_instance = model

    initialization_data = SimpleInitializationData(**initialization_data)
    initialization_data.apply_to_model(model_instance, input_data)

    if post_processing:
        model_instance.update_active_post_processing('sampling', post_processing)

    with mot.configuration.config_context(CLRuntimeAction(cl_runtime_info)):
        from mdt.lib.processing.model_sampling import sample_composite_model
        return sample_composite_model(model_instance, input_data, output_folder, nmr_samples, thinning, burnin,
                                      get_temporary_results_dir(tmp_results_dir),
                                      method=method, recalculate=recalculate,
                                      store_samples=store_samples,
                                      sample_items_to_save=sample_items_to_save,
                                      post_sampling_cb=post_sampling_cb,
                                      sampler_options=sampler_options)
Exemplo n.º 16
0
    def evaluate(self,
                 inputs,
                 nmr_instances,
                 use_local_reduction=False,
                 local_size=None,
                 cl_runtime_info=None,
                 do_data_transfers=True,
                 is_blocking=True,
                 return_events=False,
                 wait_for=None):

        cl_runtime_info = cl_runtime_info or CLRuntimeInfo()

        def resolve_cl_function_and_kernel_data():
            kernel_data = convert_inputs_to_kernel_data(
                inputs, self.get_parameters(), nmr_instances)
            for data in kernel_data.values():
                data.set_mot_float_dtype(cl_runtime_info.mot_float_dtype)

            cl_function = self
            if not self.is_kernel_func():
                cl_function, extra_data = self.get_kernel_wrapped(
                    kernel_data, nmr_instances)
                kernel_data.update(extra_data)

            return cl_function, kernel_data

        def get_kernel_source(cl_function, kernel_data):
            kernel_source = ''
            kernel_source += get_cl_utility_definitions(
                cl_runtime_info.double_precision)
            kernel_source += '\n'.join(data.get_type_definitions()
                                       for data in kernel_data.values())
            kernel_source += cl_function.get_cl_code()
            return kernel_source

        def get_kernels(kernel_source, function_name):
            hashed_source = hash(kernel_source)
            kernels = {}
            for env in cl_runtime_info.cl_environments:
                key = (hashed_source, env.context,
                       cl_runtime_info.compile_flags)
                if key not in self._compilation_cache:
                    self._compilation_cache[key] = cl.Program(
                        env.context, kernel_source).build(' '.join(
                            cl_runtime_info.compile_flags))
                kernels[env] = getattr(self._compilation_cache[key],
                                       function_name)
            return kernels

        cl_function, kernel_data = resolve_cl_function_and_kernel_data()
        kernel_source = get_kernel_source(cl_function, kernel_data)
        kernels = get_kernels(kernel_source,
                              cl_function.get_cl_function_name())

        processor = MultiDeviceProcessor(
            kernels,
            kernel_data,
            cl_runtime_info.cl_environments,
            cl_runtime_info.load_balancer,
            nmr_instances,
            use_local_reduction=use_local_reduction,
            local_size=local_size,
            do_data_transfers=do_data_transfers)
        events = processor.process(wait_for=wait_for)

        return_data = None
        if is_blocking:
            processor.finish()
            if self.get_return_type() != 'void':
                return_data = kernel_data['__return_values'].get_data()

        if return_events:
            return return_data, events
        return return_data