<commit_before># -*- coding: utf-8 -*-
#
# Copyright (C) 2017 KuraLabs S.R.L
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
Base class for all Flowbber components.

All Flowbber components extend from the Component class.
"""

from time import time
from queue import Empty
from abc import ABCMeta, abstractmethod
from multiprocessing import Queue, Process

from setproctitle import setproctitle

from ..config import Configurator
from ..logging import get_logger

log = get_logger(__name__)

class ComponentError(Exception):
    """
    Generic exception raised when a component fails.
    """

    def __init__(self, execution):
        super().__init__(str(execution))
        self.execution = execution

class TimeExceededError(ComponentError):
    """
    Exception raised when a component execution time was exceeded.
    """
    pass

class CrashError(ComponentError):
    """
    Exception raised when a component crashed.
    """
    pass

class ExecutionInfo:
    """
    Component execution information object.

    :var status: Word used to describe the status of the execution.
     For example: ``succeeded``, ``crashed``, ``killed``, ``hanged`` or
     ``timed out``.
    :var duration: Duration time in seconds of the execution of the process.
    :var pid: Pid of the executing process.
    :var exitcode: Exit code of the executing process.
     Can be None if status is ``hanged``.
    :var data: Data returned by the executing process, if any.
    """

    def __init__(self, status, duration, pid, exitcode, data):
        self.status = status
        self.duration = duration
        self.pid = pid
        self.exitcode = exitcode
        self.data = data

    def __str__(self):
        return (
            'Execution of PID {execution.pid} {execution.status}. '
            'Exit code is {execution.exitcode} and the process ran for '
            '{execution.duration} seconds'.format(execution=self)
        )

class NamedABCMeta(ABCMeta):
    """
    Base metaclass for abstract classes that pretty print the name of the
    class.
    """

    def __str__(cls):  # noqa: False N805
        return cls.__name__

    def __repr__(cls):  # noqa: False N805
        return str(cls)

class Component(metaclass=NamedABCMeta):
    """
    Base Component class.

    All Component classes (Sink, Aggregator, Source) extend from this class.

    **Properties**:

    :var int index: Position of this component in the pipeline definition.
    :var str id: Unique identifier for this component.
    :var bool optional: Successful execution of this component is optional.
     That is, it is allowed to fail and the pipeline won't fail.
    :var int timeout: Execution timeout for this component, in seconds.
     None means no timeout, wait forever.
    :var namedtuple config: Frozen configuration after validation.

    **Parameters**:

    :param int index: Value to set the index property.
    :param str type_: Type key used to fetch this component.
    :param str id_: Value to set the id property.
    :param bool optional: Value to set the optional property.
    :param int timeout: Value to set the timeout property.
    :param dict config: User configuration for this component.
    """

    @abstractmethod
    def __init__(
        self, index, type_, id_,
        optional=False, timeout=None, config=None
    ):
        self._index = index
        self._type_ = type_
        self._id = id_
        self._optional = optional
        self._timeout = timeout

        self._result = None
        self._start = None
        self._process = None

        configurator = Configurator()
        self.declare_config(configurator)
        self.config = configurator.validate(config or {})

    @property
    def index(self):
        """
        Index of this component in the stage.
        """
        return self._index

    @property
    def id(self):
        """
        Component unique identifier.
        """
        return self._id

    @property
    def optional(self):
        """
        Component successful execution is optional or not.
        """
        return self._optional

    @property
    def timeout(self):
        """
        Timeout for this component.
        """
        return self._timeout

    def declare_config(self, config):
        """
        Declare the configuration options of this component.

        :param config: The configuration manager for this component.
        :type config: :class:`flowbber.config.Configurator`
        """
        pass

    @abstractmethod
    def _component_execute(self, *args):
        """
        Abstract method that components must implement to provide its specific
        behavior.

        Implementations can receive arbitrary arguments and must return the
        data they want to return back to the called process.
        """
        pass

    def _process_execute(self, *args):
        """
        Execute this component.

        This method MUST be run in a subprocess.
        """
        setproctitle(str(self))

        assert self._result.empty()

        # We reset the start time so that the measurement is more accurate
        # and will not account for the time the process took to start
        self.start = time()
        data = None

        try:
            data = self._component_execute(*args)

        finally:
            self._result.put(
                (time() - self.start, data)
            )

    def _reset(self, procargs):
        """
        Reset this component so its ready for another execution.

        :param tuple procargs: Process execution arguments.
        """
        self._result = Queue(maxsize=1)
        self._start = time()
        self._process = Process(
            target=self._process_execute,
            name=str(self),
            args=procargs,
        )

    def start(self, *args):
        """
        Start the component execution.
        """
        self._reset(args)
        self._process.start()

    def join(self):
        """
        Join the component and get its execution information.

        :return: The execution information of this component.
        :rtype: :class:`ExecutionInfo`.
        """

        # Calculate timeout from elapsed time
        timeout = None
        if self.timeout is not None:
            timeout = max([0, self.timeout - (time() - self.start)])

        # Get results
        try:
            duration, data = self._result.get(True, timeout)

            # Standard Python crash
            if data is None:
                execution = ExecutionInfo(
                    'crashed', duration,
                    self._process.pid,
                    self._process.exitcode,
                    None
                )
                raise CrashError(execution)

            # All good
            execution = ExecutionInfo(
                'succeeded', duration,
                self._process.pid,
                self._process.exitcode,
                data
            )
            return execution

        except Empty as e:
            # In most cases the duration of the process will be unable to be
            # determined
            duration = None

            # Check if killed without executing the finally clause
            if not self._process.is_alive():
                log.warning(
                    '{name} #{component.index} "{component.id}" driving '
                    'process with PID {process.pid} was killed '
                    '({process.exitcode}).\n'
                    'Possible causes can be a segfault, SIGTERM, SIGKILL '
                    'or OOM killer'.format(
                        name=self.__class__.__name__,
                        component=self,
                        process=self._process,
                    )
                )

                status = 'killed'

            # Real timeout, process still alive, lets kill it
            else:
                self._process.terminate()

                # Check if process hanged
                self._process.join(0.1)
                if self._process.is_alive():
                    log.warning(
                        'Execution of {name} #{component.index} '
                        '"{component.id}" timed out and its driving process '
                        'with PID {component.pid} seems to have hanged'.format(
                            name=self.__class__.__name__.lower(),
                            component=self,
                            process=self._process,
                        )
                    )

                    status = 'hanged'

                else:
                    # At least we can offer an estimate if we kill the process
                    duration = time() - self.start
                    status = 'timed out'

            # Note: exitcode can be None if the process hanged
            execution = ExecutionInfo(
                status, duration,
                self._process.pid,
                self._process.exitcode,
                None
            )
            raise TimeExceededError(execution)

    def __lt__(self, other):
        """
        "Less than" magic method allows to sort a collection of this objects
        by its timeout.
        """

        if self.timeout is None and other.timeout is None:
            return True  # Or False?
        if self.timeout is None:
            return False
        if other.timeout is None:
            return True

        return self.timeout < other.timeout

    def __str__(self):
        return '#{} {}.{}.{}'.format(
            self._index,
            self.__class__.__name__,
            self._type_,
            self._id
        )

    def __repr__(self):
        return str(self)

__all__ = [
    'TimeExceededError',
    'CrashError',
    'ExecutionInfo',
    'Component',
]
<commit_msg>fix: dev: Making reordering of components by timeout stable.<commit_after># -*- coding: utf-8 -*-
#
# Copyright (C) 2017 KuraLabs S.R.L
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

"""
Base class for all Flowbber components.

All Flowbber components extend from the Component class.
"""

from time import time
from queue import Empty
from abc import ABCMeta, abstractmethod
from multiprocessing import Queue, Process

from setproctitle import setproctitle

from ..config import Configurator
from ..logging import get_logger

log = get_logger(__name__)

class ComponentError(Exception):
    """
    Generic exception raised when a component fails.
    """

    def __init__(self, execution):
        super().__init__(str(execution))
        self.execution = execution

class TimeExceededError(ComponentError):
    """
    Exception raised when a component execution time was exceeded.
    """
    pass

class CrashError(ComponentError):
    """
    Exception raised when a component crashed.
    """
    pass

class ExecutionInfo:
    """
    Component execution information object.

    :var status: Word used to describe the status of the execution.
     For example: ``succeeded``, ``crashed``, ``killed``, ``hanged`` or
     ``timed out``.
    :var duration: Duration time in seconds of the execution of the process.
    :var pid: Pid of the executing process.
    :var exitcode: Exit code of the executing process.
     Can be None if status is ``hanged``.
    :var data: Data returned by the executing process, if any.
    """

    def __init__(self, status, duration, pid, exitcode, data):
        self.status = status
        self.duration = duration
        self.pid = pid
        self.exitcode = exitcode
        self.data = data

    def __str__(self):
        return (
            'Execution of PID {execution.pid} {execution.status}. '
            'Exit code is {execution.exitcode} and the process ran for '
            '{execution.duration} seconds'.format(execution=self)
        )

class NamedABCMeta(ABCMeta):
    """
    Base metaclass for abstract classes that pretty print the name of the
    class.
    """

    def __str__(cls):  # noqa: False N805
        return cls.__name__

    def __repr__(cls):  # noqa: False N805
        return str(cls)

class Component(metaclass=NamedABCMeta):
    """
    Base Component class.

    All Component classes (Sink, Aggregator, Source) extend from this class.

    **Properties**:

    :var int index: Position of this component in the pipeline definition.
    :var str id: Unique identifier for this component.
    :var bool optional: Successful execution of this component is optional.
     That is, it is allowed to fail and the pipeline won't fail.
    :var int timeout: Execution timeout for this component, in seconds.
     None means no timeout, wait forever.
    :var namedtuple config: Frozen configuration after validation.

    **Parameters**:

    :param int index: Value to set the index property.
    :param str type_: Type key used to fetch this component.
    :param str id_: Value to set the id property.
    :param bool optional: Value to set the optional property.
    :param int timeout: Value to set the timeout property.
    :param dict config: User configuration for this component.
    """

    @abstractmethod
    def __init__(
        self, index, type_, id_,
        optional=False, timeout=None, config=None
    ):
        self._index = index
        self._type_ = type_
        self._id = id_
        self._optional = optional
        self._timeout = timeout

        self._result = None
        self._start = None
        self._process = None

        configurator = Configurator()
        self.declare_config(configurator)
        self.config = configurator.validate(config or {})

    @property
    def index(self):
        """
        Index of this component in the stage.
        """
        return self._index

    @property
    def id(self):
        """
        Component unique identifier.
        """
        return self._id

    @property
    def optional(self):
        """
        Component successful execution is optional or not.
        """
        return self._optional

    @property
    def timeout(self):
        """
        Timeout for this component.
        """
        return self._timeout

    def declare_config(self, config):
        """
        Declare the configuration options of this component.

        :param config: The configuration manager for this component.
        :type config: :class:`flowbber.config.Configurator`
        """
        pass

    @abstractmethod
    def _component_execute(self, *args):
        """
        Abstract method that components must implement to provide its specific
        behavior.

        Implementations can receive arbitrary arguments and must return the
        data they want to return back to the called process.
        """
        pass

    def _process_execute(self, *args):
        """
        Execute this component.

        This method MUST be run in a subprocess.
        """
        setproctitle(str(self))

        assert self._result.empty()

        # We reset the start time so that the measurement is more accurate
        # and will not account for the time the process took to start
        self.start = time()
        data = None

        try:
            data = self._component_execute(*args)

        finally:
            self._result.put(
                (time() - self.start, data)
            )

    def _reset(self, procargs):
        """
        Reset this component so its ready for another execution.

        :param tuple procargs: Process execution arguments.
        """
        self._result = Queue(maxsize=1)
        self._start = time()
        self._process = Process(
            target=self._process_execute,
            name=str(self),
            args=procargs,
        )

    def start(self, *args):
        """
        Start the component execution.
        """
        self._reset(args)
        self._process.start()

    def join(self):
        """
        Join the component and get its execution information.

        :return: The execution information of this component.
        :rtype: :class:`ExecutionInfo`.
        """

        # Calculate timeout from elapsed time
        timeout = None
        if self.timeout is not None:
            timeout = max([0, self.timeout - (time() - self.start)])

        # Get results
        try:
            duration, data = self._result.get(True, timeout)

            # Standard Python crash
            if data is None:
                execution = ExecutionInfo(
                    'crashed', duration,
                    self._process.pid,
                    self._process.exitcode,
                    None
                )
                raise CrashError(execution)

            # All good
            execution = ExecutionInfo(
                'succeeded', duration,
                self._process.pid,
                self._process.exitcode,
                data
            )
            return execution

        except Empty as e:
            # In most cases the duration of the process will be unable to be
            # determined
            duration = None

            # Check if killed without executing the finally clause
            if not self._process.is_alive():
                log.warning(
                    '{name} #{component.index} "{component.id}" driving '
                    'process with PID {process.pid} was killed '
                    '({process.exitcode}).\n'
                    'Possible causes can be a segfault, SIGTERM, SIGKILL '
                    'or OOM killer'.format(
                        name=self.__class__.__name__,
                        component=self,
                        process=self._process,
                    )
                )

                status = 'killed'

            # Real timeout, process still alive, lets kill it
            else:
                self._process.terminate()

                # Check if process hanged
                self._process.join(0.1)
                if self._process.is_alive():
                    log.warning(
                        'Execution of {name} #{component.index} '
                        '"{component.id}" timed out and its driving process '
                        'with PID {component.pid} seems to have hanged'.format(
                            name=self.__class__.__name__.lower(),
                            component=self,
                            process=self._process,
                        )
                    )

                    status = 'hanged'

                else:
                    # At least we can offer an estimate if we kill the process
                    duration = time() - self.start
                    status = 'timed out'

            # Note: exitcode can be None if the process hanged
            execution = ExecutionInfo(
                status, duration,
                self._process.pid,
                self._process.exitcode,
                None
            )
            raise TimeExceededError(execution)

    def __lt__(self, other):
        """
        "Less than" magic method allows to sort a collection of this objects
        by its timeout.
        """

        if self.timeout is None and other.timeout is None:
            return False
        if self.timeout is None:
            return False
        if other.timeout is None:
            return True

        return self.timeout < other.timeout

    def __str__(self):
        return '#{} {}.{}.{}'.format(
            self._index,
            self.__class__.__name__,
            self._type_,
            self._id
        )

    def __repr__(self):
        return str(self)

__all__ = [
    'TimeExceededError',
    'CrashError',
    'ExecutionInfo',
    'Component',
]
<|endoftext|>


<commit_before>"""

.. _reconst-mcsd:

================================================
Reconstruction with Multi-Shell Multi-Tissue CSD
================================================

This example shows how to use Multi-Shell Multi-Tissue Constrained Spherical
Deconvolution (MSMT-CSD) introduced by Tournier et al. [Jeurissen2014]_. This
tutorial goes through the steps involved in implementing the method.

This method provides improved White Matter(WM), Grey Matter (GM), and
Cerebrospinal fluid (CSF) volume fraction maps, which is otherwise
overestimated in the standard CSD (SSST-CSD). This is done by using b-value
dependencies of the different tissue types to estimate ODFs. This method thus
extends the SSST-CSD introduced in [Tournier2007]_.

The reconstruction of the fiber orientation distribution function
(fODF) in MSMT-CSD involves the following steps:
    1. Generate a mask using Median Otsu (optional step)
    2. Denoise the data using MP-PCA (optional step)
    3. Generate  Anisotropic Powermap (if T1 unavailable)
    4. Fit DTI model to the data
    5. Tissue Classification (needs to be at least two classes of tissues)
    6. Estimation of the fiber response function
    7. Use the response function to reconstruct the fODF

First, we import all the modules we need from dipy as follows:
"""

import numpy as np
import dipy.reconst.shm as shm
import dipy.direction.peaks as dp
import dipy.reconst.dti as dti
import matplotlib.pyplot as plt

from dipy.denoise.localpca import mppca
from dipy.data import (fetch_cfin_multib, read_cfin_dwi)
from dipy.core.gradients import gradient_table
from dipy.segment.mask import median_otsu
from dipy.reconst.csdeconv import auto_response
from dipy.segment.tissue import TissueClassifierHMRF
from dipy.sims.voxel import multi_shell_fiber_response
from dipy.reconst.mcsd import MultiShellDeconvModel
from dipy.viz import window, actor

from dipy.data import get_sphere
sphere = get_sphere('symmetric724')

"""
For this example, we use fetch to download a multi-shell dataset which was
kindly provided by Hansen and Jespersen (more details about the data are
provided in their paper [Hansen2016]_). The total size of the downloaded data
is 192 MBytes, however you only need to fetch it once.
"""

fetch_cfin_multib()
img, gtab = read_cfin_dwi()
data = img.get_data()
affine = img.affine

"""
For the sake of simplicity, we only select two non-zero b-values for this
example.
"""

bvals = gtab.bvals
bvecs = gtab.bvecs

sel_b = np.logical_or(np.logical_or(bvals == 0, bvals == 1000), bvals == 2000)
data = data[..., sel_b]

"""
The gradient table is also selected to have the selected b-values (0, 1000 and
2000)
"""

gtab = gradient_table(bvals[sel_b], bvecs[sel_b])

"""
We make use of the ``median_otsu`` method to generate the mask for the data as
follows:
"""

b0_mask, mask = median_otsu(data, median_radius=2, numpass=1, vol_idx=[0, 1])

print(data.shape)

"""
As one can see from its shape, the selected data contains a total of 67
volumes of images corresponding to all the diffusion gradient directions
of the selected b-values and call the ``mppca`` as follows:
"""

denoised_arr = mppca(data, mask=mask, patch_radius=2)

"""
Now we will use the denoised array (``denoised_arr``) obtained from ``mppca``
in the rest of the steps in the tutorial.

As for the next step, we generate the anisotropic powermap introduced by
[Dell'Acqua2014]_. To do so, we make use of the Qball Model as follows:
"""

qball_model = shm.QballModel(gtab, 8)

"""
We generate the peaks from the ``qball_model`` as follows:
"""

peaks = dp.peaks_from_model(model=qball_model, data=denoised_arr,
                            relative_peak_threshold=.5,
                            min_separation_angle=25,
                            sphere=sphere, mask=mask)

ap = shm.anisotropic_power(peaks.shm_coeff)

plt.matshow(np.rot90(ap[:, :, 10]), cmap=plt.cm.bone)
plt.savefig("anisotropic_power_map.png")
plt.close()

"""
.. figure:: anisotropic_power_map.png
   :align: center

   Anisotropic Power Map (Axial Slice)
"""

print(ap.shape)

"""
The above figure is a visualization of the axial slice of the Anisotropic
Power Map. It can be treated as a peudo-T1 for the classification purposes
using the HMRF classifier if the T1 image is not available.

As we can see from the shape of the Anisotropic Power Map, it is 3D and can be
used for tissue classification using Hidden Markov Random Fields (HMRF). The
HMRF needs the specification of the number of classes. For the case of MSMT-CSD
the ``nclass`` parameter needs to be ``>=2``. In our case, we set it to 3:
namely corticospinal fluid (csf), white matter (wm) and gray matter (gm).
"""

nclass = 3

"""
Then, the smoothness factor of the segmentation. Good performance is achieved
with values between 0 and 0.5.
"""

beta = 0.1

"""
We then call the ``TissueClassifierHMRF`` with the parameters specified as
above:
"""

hmrf = TissueClassifierHMRF()
initial_segmentation, final_segmentation, PVE = hmrf.classify(ap, nclass, beta)
print(PVE.shape)

"""
Now that we hae the segmentation step, we would like to classify the tissues
into ``wm``, ``gm`` and ``csf`` We do
so using the Fractional Anisotropy (FA) and Mean Diffusivity (MD) metrics
obtained from the Diffusion Tensor Imaging Model (DTI) fit as follows:
"""

# Construct the  DTI model
tenmodel = dti.TensorModel(gtab)

# fit the denoised data with DTI model
tenfit = tenmodel.fit(denoised_arr)

# obtain the FA and MD metrics
FA = tenfit.fa
MD = tenfit.md

"""
Now that we have the FA and the MD obtained from DTI, we use it to distinguish
between the ``wm``, ``gm`` and ``csf``. As we can see
from the shape of the PVE, the last dimension refers to the classification. We
will now index them as: 0 -> ``csf``, 1 -> ``gm`` and 2 ->``wm`` as per their
FA values and the confidence of prediction obtained from
``TissueClassifierHMRF``.
"""

csf = PVE[..., 0]
gm = PVE[..., 1]
wm = PVE[..., 2]

indices_csf = np.where(((FA < 0.2) & (csf > 0.95)))
indices_gm = np.where(((FA < 0.2) & (gm > 0.95)))

selected_csf = np.zeros(FA.shape, dtype='bool')
selected_gm = np.zeros(FA.shape, dtype='bool')

selected_csf[indices_csf] = True
selected_gm[indices_gm] = True

csf_md = np.mean(MD[selected_csf])
gm_md = np.mean(MD[selected_gm])

"""
The ``auto_response`` function will calculate FA for an ROI of radius equal to
``roi_radius`` in the center of the volume and return the response function
estimated in that region for the voxels with FA higher than 0.7.
"""

response, ratio = auto_response(gtab, denoised_arr, roi_radius=10, fa_thr=0.7)
evals_d = response[0]

"""
We will now use the evals obtained from the ``auto_response`` to generate the
``multi_shell_fiber_response`` rquired by the MSMT-CSD model. Note that we
mead diffusivities of ``csf`` and ``gm`` as inputs to generate th response.
"""

response_mcsd = multi_shell_fiber_response(sh_order=8, bvals=bvals,
                                           evals=evals_d, csf_md=csf_md,
                                           gm_md=gm_md)

"""
Now we build the MSMT-CSD model with the ``response_mcsd`` as input. We then
call the ``fit`` function to fit one slice of the 3D data and visualize it.
"""

mcsd_model = MultiShellDeconvModel(gtab, response_mcsd)
mcsd_fit = mcsd_model.fit(denoised_arr[:, :, 10:11])

"""
From the fit obtained in the previous step, we generate the ODFs which can be
visualized as follows:
"""

mcsd_odf = mcsd_fit.odf(sphere)
fodf_spheres = actor.odf_slicer(mcsd_odf, sphere=sphere, scale=0.01,
                                norm=False, colormap='plasma')

interactive = False
ren = window.Renderer()
ren.add(fodf_spheres)
ren.reset_camera_tight()

print('Saving illustration as msdodf.png')
window.record(ren, out_path='msdodf.png', size=(600, 600), magnification=2)

if interactive:
    window.show(ren)

"""
.. figure:: msdodf.png
   :align: center

   MSMT-CSD Peaks and ODFs.

References
----------

.. [Jeurissen2014] B. Jeurissen, et al., "Multi-tissue constrained spherical
                    deconvolution for improved analysis of multi-shell
                    diffusion MRI data." NeuroImage 103 (2014): 411-426.

.. [Tournier2007] J-D. Tournier, F. Calamante and A. Connelly, "Robust
                    determination of the fibre orientation distribution in
                    diffusion MRI: Non-negativity constrained super-resolved
                    spherical deconvolution", Neuroimage, vol. 35, no. 4,
                    pp. 1459-1472, (2007).

.. [Hansen2016]  B. Hansen and SN. Jespersen Data for evaluation of fast
                    kurtosis strategies, b-value optimization and exploration
                    of diffusion MRI contrast. Scientific Data 3: 160072
                    doi:10.1038/sdata.2016.72, (2016)

.. [Dell'Acqua2014], F. Dell'Acqua, et. al., "Anisotropic Power Maps: A
                    diffusion contrast to reveal low anisotropy tissues from
                    HARDI data", Proceedings of International Society for
                    Magnetic Resonance in Medicine. Milan, Italy, (2014).

.. include:: ../links_names.inc
"""<commit_msg>added refs<commit_after>"""

.. _reconst-mcsd:

================================================
Reconstruction with Multi-Shell Multi-Tissue CSD
================================================

This example shows how to use Multi-Shell Multi-Tissue Constrained Spherical
Deconvolution (MSMT-CSD) introduced by Tournier et al. [Jeurissen2014]_. This
tutorial goes through the steps involved in implementing the method.

This method provides improved White Matter(WM), Grey Matter (GM), and
Cerebrospinal fluid (CSF) volume fraction maps, which is otherwise
overestimated in the standard CSD (SSST-CSD). This is done by using b-value
dependencies of the different tissue types to estimate ODFs. This method thus
extends the SSST-CSD introduced in [Tournier2007]_.

The reconstruction of the fiber orientation distribution function
(fODF) in MSMT-CSD involves the following steps:
    1. Generate a mask using Median Otsu (optional step)
    2. Denoise the data using MP-PCA (optional step)
    3. Generate  Anisotropic Powermap (if T1 unavailable)
    4. Fit DTI model to the data
    5. Tissue Classification (needs to be at least two classes of tissues)
    6. Estimation of the fiber response function
    7. Use the response function to reconstruct the fODF

First, we import all the modules we need from dipy as follows:
"""

import numpy as np
import dipy.reconst.shm as shm
import dipy.direction.peaks as dp
import dipy.reconst.dti as dti
import matplotlib.pyplot as plt

from dipy.denoise.localpca import mppca
from dipy.data import (fetch_cfin_multib, read_cfin_dwi)
from dipy.core.gradients import gradient_table
from dipy.segment.mask import median_otsu
from dipy.reconst.csdeconv import auto_response
from dipy.segment.tissue import TissueClassifierHMRF
from dipy.sims.voxel import multi_shell_fiber_response
from dipy.reconst.mcsd import MultiShellDeconvModel
from dipy.viz import window, actor

from dipy.data import get_sphere
sphere = get_sphere('symmetric724')

"""
For this example, we use fetch to download a multi-shell dataset which was
kindly provided by Hansen and Jespersen (more details about the data are
provided in their paper [Hansen2016]_). The total size of the downloaded data
is 192 MBytes, however you only need to fetch it once.
"""

fetch_cfin_multib()
img, gtab = read_cfin_dwi()
data = img.get_data()
affine = img.affine

"""
For the sake of simplicity, we only select two non-zero b-values for this
example.
"""

bvals = gtab.bvals
bvecs = gtab.bvecs

sel_b = np.logical_or(np.logical_or(bvals == 0, bvals == 1000), bvals == 2000)
data = data[..., sel_b]

"""
The gradient table is also selected to have the selected b-values (0, 1000 and
2000)
"""

gtab = gradient_table(bvals[sel_b], bvecs[sel_b])

"""
We make use of the ``median_otsu`` method to generate the mask for the data as
follows:
"""

b0_mask, mask = median_otsu(data, median_radius=2, numpass=1, vol_idx=[0, 1])

print(data.shape)

"""
As one can see from its shape, the selected data contains a total of 67
volumes of images corresponding to all the diffusion gradient directions
of the selected b-values and call the ``mppca`` as follows:
"""

denoised_arr = mppca(data, mask=mask, patch_radius=2)

"""
Now we will use the denoised array (``denoised_arr``) obtained from ``mppca``
in the rest of the steps in the tutorial.

As for the next step, we generate the anisotropic powermap introduced by
[Dell'Acqua2014]_. To do so, we make use of the Qball Model as follows:
"""

qball_model = shm.QballModel(gtab, 8)

"""
We generate the peaks from the ``qball_model`` as follows:
"""

peaks = dp.peaks_from_model(model=qball_model, data=denoised_arr,
                            relative_peak_threshold=.5,
                            min_separation_angle=25,
                            sphere=sphere, mask=mask)

ap = shm.anisotropic_power(peaks.shm_coeff)

plt.matshow(np.rot90(ap[:, :, 10]), cmap=plt.cm.bone)
plt.savefig("anisotropic_power_map.png")
plt.close()

"""
.. figure:: anisotropic_power_map.png
   :align: center

   Anisotropic Power Map (Axial Slice)
"""

print(ap.shape)

"""
The above figure is a visualization of the axial slice of the Anisotropic
Power Map. It can be treated as a peudo-T1 for the classification purposes
using the HMRF classifier if the T1 image is not available.

As we can see from the shape of the Anisotropic Power Map, it is 3D and can be
used for tissue classification using Hidden Markov Random Fields (HMRF). The
HMRF needs the specification of the number of classes. For the case of MSMT-CSD
the ``nclass`` parameter needs to be ``>=2``. In our case, we set it to 3:
namely corticospinal fluid (csf), white matter (wm) and gray matter (gm).
"""

nclass = 3

"""
Then, the smoothness factor of the segmentation. Good performance is achieved
with values between 0 and 0.5.
"""

beta = 0.1

"""
We then call the ``TissueClassifierHMRF`` with the parameters specified as
above:
"""

hmrf = TissueClassifierHMRF()
initial_segmentation, final_segmentation, PVE = hmrf.classify(ap, nclass, beta)
print(PVE.shape)

"""
Now that we hae the segmentation step, we would like to classify the tissues
into ``wm``, ``gm`` and ``csf`` We do
so using the Fractional Anisotropy (FA) and Mean Diffusivity (MD) metrics
obtained from the Diffusion Tensor Imaging Model (DTI) fit as follows:
"""

# Construct the  DTI model
tenmodel = dti.TensorModel(gtab)

# fit the denoised data with DTI model
tenfit = tenmodel.fit(denoised_arr)

# obtain the FA and MD metrics
FA = tenfit.fa
MD = tenfit.md

"""
Now that we have the FA and the MD obtained from DTI, we use it to distinguish
between the ``wm``, ``gm`` and ``csf``. As we can see
from the shape of the PVE, the last dimension refers to the classification. We
will now index them as: 0 -> ``csf``, 1 -> ``gm`` and 2 ->``wm`` as per their
FA values and the confidence of prediction obtained from
``TissueClassifierHMRF``.
"""

csf = PVE[..., 0]
gm = PVE[..., 1]
wm = PVE[..., 2]

indices_csf = np.where(((FA < 0.2) & (csf > 0.95)))
indices_gm = np.where(((FA < 0.2) & (gm > 0.95)))

selected_csf = np.zeros(FA.shape, dtype='bool')
selected_gm = np.zeros(FA.shape, dtype='bool')

selected_csf[indices_csf] = True
selected_gm[indices_gm] = True

csf_md = np.mean(MD[selected_csf])
gm_md = np.mean(MD[selected_gm])

"""
The ``auto_response`` function will calculate FA for an ROI of radius equal to
``roi_radius`` in the center of the volume and return the response function
estimated in that region for the voxels with FA higher than 0.7.
"""

response, ratio = auto_response(gtab, denoised_arr, roi_radius=10, fa_thr=0.7)
evals_d = response[0]

"""
We will now use the evals obtained from the ``auto_response`` to generate the
``multi_shell_fiber_response`` rquired by the MSMT-CSD model. Note that we
mead diffusivities of ``csf`` and ``gm`` as inputs to generate th response.
"""

response_mcsd = multi_shell_fiber_response(sh_order=8, bvals=bvals,
                                           evals=evals_d, csf_md=csf_md,
                                           gm_md=gm_md)

"""
Now we build the MSMT-CSD model with the ``response_mcsd`` as input. We then
call the ``fit`` function to fit one slice of the 3D data and visualize it.
"""

mcsd_model = MultiShellDeconvModel(gtab, response_mcsd)
mcsd_fit = mcsd_model.fit(denoised_arr[:, :, 10:11])

"""
From the fit obtained in the previous step, we generate the ODFs which can be
visualized as follows:
"""

mcsd_odf = mcsd_fit.odf(sphere)
fodf_spheres = actor.odf_slicer(mcsd_odf, sphere=sphere, scale=0.01,
                                norm=False, colormap='plasma')

interactive = False
ren = window.Renderer()
ren.add(fodf_spheres)
ren.reset_camera_tight()

print('Saving illustration as msdodf.png')
window.record(ren, out_path='msdodf.png', size=(600, 600))

if interactive:
    window.show(ren)

"""
.. figure:: msdodf.png
   :align: center

   MSMT-CSD Peaks and ODFs.

References
----------

.. [Jeurissen2014] B. Jeurissen, et al., "Multi-tissue constrained spherical
                    deconvolution for improved analysis of multi-shell
                    diffusion MRI data." NeuroImage 103 (2014): 411-426.

.. [Tournier2007] J-D. Tournier, F. Calamante and A. Connelly, "Robust
                    determination of the fibre orientation distribution in
                    diffusion MRI: Non-negativity constrained super-resolved
                    spherical deconvolution", Neuroimage, vol. 35, no. 4,
                    pp. 1459-1472, (2007).

.. [Hansen2016]  B. Hansen and SN. Jespersen Data for evaluation of fast
                    kurtosis strategies, b-value optimization and exploration
                    of diffusion MRI contrast. Scientific Data 3: 160072
                    doi:10.1038/sdata.2016.72, (2016)

.. [Dell'Acqua2014], F. Dell'Acqua, et. al., "Anisotropic Power Maps: A
                    diffusion contrast to reveal low anisotropy tissues from
                    HARDI data", Proceedings of International Society for
                    Magnetic Resonance in Medicine. Milan, Italy, (2014).

.. include:: ../links_names.inc
"""<|endoftext|>


<commit_before>import numpy as np
from smif.convert import SpaceTimeConvertor
from smif.convert.area import RegionRegister
from smif.convert.interval import TimeIntervalRegister
from test_area import regions_half_squares, regions_rect
from test_interval import (months, one_day, remap_months, seasons,
                           twenty_four_hours)

class TestSpaceTimeConvertor_TimeOnly:

    def test_one_region_pass_through_time(self, months, regions_half_squares):
        """Only one region, 12 months, neither space nor time conversion is required
        """
        data = np.array([[
            31,
            28,
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31
        ]], dtype=float)

        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'months'
        )
        assert np.allclose(actual, data)

    def test_one_region_time_aggregation(self, months, seasons, regions_half_squares):
        """Only one region, time aggregation is required
        """
        data = np.array([[
            31,
            28,
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31
        ]], dtype=float)  # area a, months 1-12

        expected = np.array([[
            31 + 31 + 28,
            31 + 30 + 31,
            30 + 31 + 31,
            30 + 31 + 30
        ]], dtype=float)  # area a, seasons 1-4

        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')
        intervals.register(seasons, 'seasons')

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'seasons'
        )
        assert np.allclose(actual, expected)

    def test_two_region_time_aggregation(self, months, seasons, regions_half_squares):
        """Two regions, time aggregation by region is required
        """
        data = np.array([
            # area a, months 1-12
            [
                31,
                28,
                31,
                30,
                31,
                30,
                31,
                31,
                30,
                31,
                30,
                31,
            ],
            # area b, months 1-12
            [
                31+1,
                28+1,
                31+1,
                30+1,
                31+1,
                30+1,
                31+1,
                31+1,
                30+1,
                31+1,
                30+1,
                31+1,
            ]
        ], dtype=float)

        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')
        intervals.register(seasons, 'seasons')

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'seasons'
        )

        expected = np.array([
            [
                31 + 31 + 28,
                31 + 30 + 31,
                30 + 31 + 31,
                30 + 31 + 30,
            ],
            [
                31 + 31 + 28 + 3,
                31 + 30 + 31 + 3,
                30 + 31 + 31 + 3,
                30 + 31 + 30 + 3,
            ]
        ], dtype=float)

        assert np.allclose(actual, expected)

    def test_one_region_convert_from_hour_to_day(self, regions_half_squares,
                                                 twenty_four_hours, one_day):
        """One region, time aggregation required
        """

        data = np.ones((1, 24))  # area a, hours 0-23

        regions = RegionRegister()
        regions.register(regions_half_squares)

        intervals = TimeIntervalRegister()
        intervals.register(twenty_four_hours, 'hourly_day')
        intervals.register(one_day, 'one_day')

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'hourly_day',
            'one_day'
        )
        expected = np.array([[24]])  # area a, day 0
        assert np.allclose(actual, expected)

    def test_remap_timeslices_to_months(self,
                                        months,
                                        remap_months,
                                        regions_half_squares):
        """One region, time remapping required
        """
        data = np.array([[
            30+31+31,
            28+31+30,
            31+31+30,
            30+31+31
        ]], dtype=float)
        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')
        intervals.register(remap_months, 'remap_months')

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'remap_months',
            'months',
        )
        expected = np.array([
            30.666666666,
            29.666666666,
            29.666666666,
            29.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666
        ], dtype=float)
        assert np.allclose(actual, expected)

    def test_remap_months_to_timeslices(self,
                                        months,
                                        remap_months,
                                        regions_half_squares):
        """One region, time remapping required
        """
        data = np.array([[
            31,
            28,
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31
        ]], dtype=float)
        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')
        intervals.register(remap_months, 'remap_months')

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'remap_months'
        )
        expected = np.array([[
            30+31+31,
            28+31+30,
            31+31+30,
            30+31+31
        ]], dtype=float)
        assert np.allclose(actual, expected)

class TestSpaceTimeConvertor_RegionOnly:

    def test_half_to_one_region_pass_through_time(self, months,
                                                  regions_half_squares,
                                                  regions_rect):
        """Convert from half a region to one region, pass through time

        """

        data = np.ones((2, 12)) / 2  # area a,b, months 1-12

        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')

        regions = RegionRegister()
        regions.register(regions_half_squares)
        regions.register(regions_rect)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'rect',
            'months',
            'months'
        )
        expected = np.ones((1, 12))  # area zero, months 1-12
        assert np.allclose(actual, expected)

    def test_one_region_convert_from_hour_to_day(self, regions_half_squares,
                                                 regions_rect,
                                                 one_day):
        """Two regions aggregated to one, one interval
        """

        data = np.array([[24], [24]])  # area a,b, single interval

        intervals = TimeIntervalRegister()
        intervals.register(one_day, 'one_day')

        regions = RegionRegister()
        regions.register(regions_half_squares)
        regions.register(regions_rect)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'rect',
            'one_day',
            'one_day'
        )
        expected = np.array([[48]])  # area zero, single interval
        assert np.allclose(actual, expected)

class TestSpaceTimeConvertorBoth:

    def test_convert_both_space_and_time(self, seasons, months,
                                         regions_half_squares,
                                         regions_rect):

        data = np.ones((2, 12)) / 2  # area a,b, months 1-12

        intervals = TimeIntervalRegister()
        intervals.register(months, 'months')
        intervals.register(seasons, 'seasons')

        regions = RegionRegister()
        regions.register(regions_half_squares)
        regions.register(regions_rect)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'rect',
            'months',
            'seasons'
        )
        expected = np.ones((1, 4)) * 3  # area zero, seasons 1-4
        assert np.allclose(actual, expected)
<commit_msg>Convert: refactor complete<commit_after>import numpy as np
from smif.convert import SpaceTimeConvertor
from smif.convert.area import RegionRegister
from smif.convert.interval import IntervalSet, TimeIntervalRegister
from test_area import regions_half_squares, regions_rect
from test_interval import (months, one_day, remap_months, seasons,
                           twenty_four_hours)

class TestSpaceTimeConvertor_TimeOnly:

    def test_one_region_pass_through_time(self, months, regions_half_squares):
        """Only one region, 12 months, neither space nor time conversion is required
        """
        data = np.array([[
            31,
            28,
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31
        ]], dtype=float)

        intervals = TimeIntervalRegister()
        intervalset = IntervalSet('months', months)
        intervals.register(intervalset)

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'months'
        )
        assert np.allclose(actual, data)

    def test_one_region_time_aggregation(self, months, seasons, regions_half_squares):
        """Only one region, time aggregation is required
        """
        data = np.array([[
            31,
            28,
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31
        ]], dtype=float)  # area a, months 1-12

        expected = np.array([[
            31 + 31 + 28,
            31 + 30 + 31,
            30 + 31 + 31,
            30 + 31 + 30
        ]], dtype=float)  # area a, seasons 1-4

        intervals = TimeIntervalRegister()
        months_set = IntervalSet('months', months)
        intervals.register(months_set)
        season_set = IntervalSet('seasons', seasons)
        intervals.register(season_set)

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'seasons'
        )
        assert np.allclose(actual, expected)

    def test_two_region_time_aggregation(self, months, seasons, regions_half_squares):
        """Two regions, time aggregation by region is required
        """
        data = np.array([
            # area a, months 1-12
            [
                31,
                28,
                31,
                30,
                31,
                30,
                31,
                31,
                30,
                31,
                30,
                31,
            ],
            # area b, months 1-12
            [
                31+1,
                28+1,
                31+1,
                30+1,
                31+1,
                30+1,
                31+1,
                31+1,
                30+1,
                31+1,
                30+1,
                31+1,
            ]
        ], dtype=float)

        intervals = TimeIntervalRegister()

        months_set = IntervalSet('months', months)
        intervals.register(months_set)
        season_set = IntervalSet('seasons', seasons)
        intervals.register(season_set)

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'seasons'
        )

        expected = np.array([
            [
                31 + 31 + 28,
                31 + 30 + 31,
                30 + 31 + 31,
                30 + 31 + 30,
            ],
            [
                31 + 31 + 28 + 3,
                31 + 30 + 31 + 3,
                30 + 31 + 31 + 3,
                30 + 31 + 30 + 3,
            ]
        ], dtype=float)

        assert np.allclose(actual, expected)

    def test_one_region_convert_from_hour_to_day(self, regions_half_squares,
                                                 twenty_four_hours, one_day):
        """One region, time aggregation required
        """

        data = np.ones((1, 24))  # area a, hours 0-23

        regions = RegionRegister()
        regions.register(regions_half_squares)

        intervals = TimeIntervalRegister()
        hourly_set = IntervalSet('hourly_day', twenty_four_hours)
        intervals.register(hourly_set)
        day_set = IntervalSet('one_day', one_day)
        intervals.register(day_set)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'hourly_day',
            'one_day'
        )
        expected = np.array([[24]])  # area a, day 0
        assert np.allclose(actual, expected)

    def test_remap_timeslices_to_months(self,
                                        months,
                                        remap_months,
                                        regions_half_squares):
        """One region, time remapping required
        """
        data = np.array([[
            30+31+31,
            28+31+30,
            31+31+30,
            30+31+31
        ]], dtype=float)
        intervals = TimeIntervalRegister()
        intervals.register(IntervalSet('months', months))
        intervals.register(IntervalSet('remap_months', remap_months))

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'remap_months',
            'months',
        )
        expected = np.array([
            30.666666666,
            29.666666666,
            29.666666666,
            29.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666,
            30.666666666
        ], dtype=float)
        assert np.allclose(actual, expected)

    def test_remap_months_to_timeslices(self,
                                        months,
                                        remap_months,
                                        regions_half_squares):
        """One region, time remapping required
        """
        data = np.array([[
            31,
            28,
            31,
            30,
            31,
            30,
            31,
            31,
            30,
            31,
            30,
            31
        ]], dtype=float)
        intervals = TimeIntervalRegister()
        intervals.register(IntervalSet('months', months))
        intervals.register(IntervalSet('remap_months', remap_months))

        regions = RegionRegister()
        regions.register(regions_half_squares)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'half_squares',
            'months',
            'remap_months'
        )
        expected = np.array([[
            30+31+31,
            28+31+30,
            31+31+30,
            30+31+31
        ]], dtype=float)
        assert np.allclose(actual, expected)

class TestSpaceTimeConvertor_RegionOnly:

    def test_half_to_one_region_pass_through_time(self, months,
                                                  regions_half_squares,
                                                  regions_rect):
        """Convert from half a region to one region, pass through time

        """

        data = np.ones((2, 12)) / 2  # area a,b, months 1-12

        intervals = TimeIntervalRegister()
        intervals.register(IntervalSet('months', months))

        regions = RegionRegister()
        regions.register(regions_half_squares)
        regions.register(regions_rect)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'rect',
            'months',
            'months'
        )
        expected = np.ones((1, 12))  # area zero, months 1-12
        assert np.allclose(actual, expected)

    def test_one_region_convert_from_hour_to_day(self, regions_half_squares,
                                                 regions_rect,
                                                 one_day):
        """Two regions aggregated to one, one interval
        """

        data = np.array([[24], [24]])  # area a,b, single interval

        intervals = TimeIntervalRegister()
        intervals.register(IntervalSet('one_day', one_day))

        regions = RegionRegister()
        regions.register(regions_half_squares)
        regions.register(regions_rect)

        convertor = SpaceTimeConvertor(regions, intervals)
        actual = convertor.convert(
            data,
            'half_squares',
            'rect',
            'one_day',
            'one_day'
        )
        expected = np.array([[48]])  # area zero, single interval
        assert np.allclose(actual, expected)

class TestSpaceTimeConvertorBoth:

    def test_convert_both_space_and_time(self, seasons, months,
                                         regions_half_squares,
                                         regions_rect):

        data = np.ones((2, 12)) / 2  # area a,b, months 1-12

        intervals = TimeIntervalRegister()
        intervals.register(IntervalSet('months', months))
        intervals.register(IntervalSet('seasons', seasons))

        regions = RegionRegister()
        regions.register(regions_half_squares)
        regions.register(regions_rect)

        convertor = SpaceTimeConvertor(regions, intervals)

        actual = convertor.convert(
            data,
            'half_squares',
            'rect',
            'months',
            'seasons'
        )
        expected = np.ones((1, 4)) * 3  # area zero, seasons 1-4
        assert np.allclose(actual, expected)
<|endoftext|>


<commit_before>from django.db import models
from evilsheep.catalog.models import Product
# Create your models here.

class CartItem(models.Model):
    cart_id  = models.CharField(max_length=100)
    date_added = models.DateField(auto_now_add=True)
    quantity = models.IntegerField(default=1)
    product = models.ForeignKey(Product, unique=False)

    class Meta:
        ordering = ['date_added']

    def total(self):
        return self.quantity * self.product

    def name(self):
        return self.product.name

    def price(self):
        return self.product.price

    def get_absolute_url(self):
        return self.product.get_absolute_url()

    def augement_quantity(self, quantity):
        self.quantity = self.quantity + int(quantity)
        self.save()

    def __unicode__(self):
        return '%d %s in cart %s' % (self.quantity, self.product, self.cart_id)
<commit_msg>fixed bug in calculating total<commit_after>from django.db import models
from evilsheep.catalog.models import Product
# Create your models here.

class CartItem(models.Model):
    cart_id  = models.CharField(max_length=100)
    date_added = models.DateField(auto_now_add=True)
    quantity = models.IntegerField(default=1)
    product = models.ForeignKey(Product, unique=False)

    class Meta:
        ordering = ['date_added']

    def total(self):
        return self.quantity * self.product.price

    def name(self):
        return self.product.name

    def price(self):
        return self.product.price

    def get_absolute_url(self):
        return self.product.get_absolute_url()

    def augement_quantity(self, quantity):
        self.quantity = self.quantity + int(quantity)
        self.save()

    def __unicode__(self):
        return '%d %s in cart %s' % (self.quantity, self.product, self.cart_id)
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# (C) British Crown Copyright 2017 Met Office.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Module to determine the occurrence of a phenomenon within a vicinity and
apply neighbourhood processing."""

import iris

from improver.utilities.spatial import OccurrenceWithinVicinity
from improver.nbhood.nbhood import NeighbourhoodProcessing

class ProbabilityOfOccurrence(object):

    """
    Calculate the probability of a phenomenon occurring within specified
    distance.
    """

    def __init__(self, distance, neighbourhood_method, radii, lead_times=None,
                 unweighted_mode=False, ens_factor=1.0):
        """
        Initialise the class.

        Args:
            distance : float
                Distance in metres used to define the vicinity within which to
                search for an occurrence.
            neighbourhood_method : str
                Name of the neighbourhood method to use. Options: 'circular',
                'square'.
            radii : float or List (if defining lead times)
                The radii in metres of the neighbourhood to apply.
                Rounded up to convert into integer number of grid
                points east and north, based on the characteristic spacing
                at the zero indices of the cube projection-x and y coords.
            lead_times : None or List
                List of lead times or forecast periods, at which the radii
                within 'radii' are defined. The lead times are expected
                in hours.
            unweighted_mode : boolean
                If True, use a circle with constant weighting.
                If False, use a circle for neighbourhood kernel with
                weighting decreasing with radius.
            ens_factor : float
                The factor with which to adjust the neighbourhood size
                for more than one ensemble member.
                If ens_factor = 1.0 this essentially conserves ensemble
                members if every grid square is considered to be the
                equivalent of an ensemble member.
                Optional, defaults to 1.0

        Raises:
            ValueError : Raise error if non-square neighbourhood method
                is requested.

        """
        self.distance = distance
        if neighbourhood_method in ["square"]:
            self.neighbourhood_method = neighbourhood_method
        else:
            msg = ("Only a square neighbourhood is accepted for "
                   "probability of occurrence calculations. "
                   "Requested a {} neighbourhood.".format(
                       neighbourhood_method))
            raise ValueError(msg)
        self.radii = radii
        self.lead_times = lead_times
        self.unweighted_mode = unweighted_mode
        self.ens_factor = ens_factor

    def __repr__(self):
        """Represent the configured plugin instance as a string."""
        result = ('<ProbabilityOfOccurrence: distance: {}; '
                  'neighbourhood_method: {}; radii: {}; '
                  'lead_times: {}; unweighted_mode: {}; '
                  'ens_factor: {}>')
        return result.format(
            self.distance, self.neighbourhood_method, self.radii,
            self.lead_times, self.unweighted_mode, self.ens_factor)

    def process(self, cube):
        """
        Identify the probability of having a phenomenon occur within a
        vicinity.
        The steps for this are as follows:
        1. Calculate the occurrence of a phenomenon within a defined vicinity.
        2. If the cube contains a realization dimension coordinate, find the
           mean.
        3. Compute neighbourhood processing.

        Args:
            cube : Iris.cube.Cube
                A cube that has been thresholded.

        Returns:
            cube : Iris.cube.Cube
                A cube containing neighbourhood probabilities to represent the
                probability of an occurrence within the vicinity given a
                pre-defined spatial uncertainty.

        """
        cube = OccurrenceWithinVicinity(self.distance).process(cube)
        try:
            cube.coord_dims('realization')
        except iris.exceptions.CoordinateNotFoundError:
            pass
        else:
            cube = cube.collapsed('realization', iris.analysis.MEAN)

        cube = NeighbourhoodProcessing(
            self.neighbourhood_method, self.radii, self.lead_times,
            self.unweighted_mode, self.ens_factor).process(cube)
        cube.rename(cube.name() + '_in_vicinity')
        return cube
<commit_msg>Vicinity fix.<commit_after># -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# (C) British Crown Copyright 2017 Met Office.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Module to determine the occurrence of a phenomenon within a vicinity and
apply neighbourhood processing."""

import iris

from improver.utilities.spatial import OccurrenceWithinVicinity
from improver.nbhood.nbhood import NeighbourhoodProcessing

class ProbabilityOfOccurrence(object):

    """
    Calculate the probability of a phenomenon occurring within specified
    distance.
    """

    def __init__(self, distance, neighbourhood_method, radii, lead_times=None,
                 unweighted_mode=False, ens_factor=1.0):
        """
        Initialise the class.

        Args:
            distance : float
                Distance in metres used to define the vicinity within which to
                search for an occurrence.
            neighbourhood_method : str
                Name of the neighbourhood method to use. Options: 'circular',
                'square'.
            radii : float or List (if defining lead times)
                The radii in metres of the neighbourhood to apply.
                Rounded up to convert into integer number of grid
                points east and north, based on the characteristic spacing
                at the zero indices of the cube projection-x and y coords.
            lead_times : None or List
                List of lead times or forecast periods, at which the radii
                within 'radii' are defined. The lead times are expected
                in hours.
            unweighted_mode : boolean
                If True, use a circle with constant weighting.
                If False, use a circle for neighbourhood kernel with
                weighting decreasing with radius.
            ens_factor : float
                The factor with which to adjust the neighbourhood size
                for more than one ensemble member.
                If ens_factor = 1.0 this essentially conserves ensemble
                members if every grid square is considered to be the
                equivalent of an ensemble member.
                Optional, defaults to 1.0

        Raises:
            ValueError : Raise error if non-square neighbourhood method
                is requested.

        """
        self.distance = distance
        if neighbourhood_method in ["square"]:
            self.neighbourhood_method = neighbourhood_method
        else:
            msg = ("Only a square neighbourhood is accepted for "
                   "probability of occurrence calculations. "
                   "Requested a {} neighbourhood.".format(
                       neighbourhood_method))
            raise ValueError(msg)
        self.radii = radii
        self.lead_times = lead_times
        self.unweighted_mode = unweighted_mode
        self.ens_factor = ens_factor

    def __repr__(self):
        """Represent the configured plugin instance as a string."""
        result = ('<ProbabilityOfOccurrence: distance: {}; '
                  'neighbourhood_method: {}; radii: {}; '
                  'lead_times: {}; unweighted_mode: {}; '
                  'ens_factor: {}>')
        return result.format(
            self.distance, self.neighbourhood_method, self.radii,
            self.lead_times, self.unweighted_mode, self.ens_factor)

    def process(self, cube):
        """
        Identify the probability of having a phenomenon occur within a
        vicinity.
        The steps for this are as follows:
        1. Calculate the occurrence of a phenomenon within a defined vicinity.
        2. If the cube contains a realization dimension coordinate, find the
           mean.
        3. Compute neighbourhood processing.

        Args:
            cube : Iris.cube.Cube
                A cube that has been thresholded.

        Returns:
            cube : Iris.cube.Cube
                A cube containing neighbourhood probabilities to represent the
                probability of an occurrence within the vicinity given a
                pre-defined spatial uncertainty.

        """
        cube = OccurrenceWithinVicinity(self.distance).process(cube)
        try:
            if cube.coord_dims('realization'):
                cube = cube.collapsed('realization', iris.analysis.MEAN)
        except iris.exceptions.CoordinateNotFoundError:
            pass

        cube = NeighbourhoodProcessing(
            self.neighbourhood_method, self.radii, self.lead_times,
            self.unweighted_mode, self.ens_factor).process(cube)
        cube.rename(cube.name() + '_in_vicinity')
        return cube
<|endoftext|>