# ----------------------------------------------------------------------------
# Name:
#
#   volumedata.py
#
# Purpose:
#
#   This file contains the implementation of the VolumeData class. This class
#   facilitates the handling of volume data (such as .vis/.ccp4 files) in
#   a logical manner from within Python.
#
# Copyright of:
#
#   Copyright Schrodinger, LLC. All rights reserved.
#
# Version:
#
#   Version         Author          Notes
#       1.0            DDR          Original Implementation
#
# Notes:
#
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Module imports.
import copy as copy
import numpy as np
import scipy.ndimage.interpolation as interpolation
from . import vdcoordinateframe as vdcoordinateframe
from . import vdexception as vdexception
# End of module imports.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Global constants.
_X = 0
_Y = 1
_Z = 2
_BAD_DATA = "The data has the wrong shape."
_BAD_VD = "The VolumeData objects are not compatible."
_BAD_COORD = "Coordinate must be a 3-element array"
# End of global constants.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Class definition:
#
#   VolumeData
#
# ----------------------------------------------------------------------------
[docs]class VolumeData(object):
    """
    The VolumeData class is responsible for handling the underlying storage as
    well as marrying together the concepts of array-coordinates and
    world-coordinates. The class itself is a fairly simple aggregation of the
    Numpy array class (to handle the basic storage and provide a huge library
    of functionality) and the _VDCoordinateFrame class (to marry up the concept
    of array-coordinates and world-coordinates).
    """
    # ------------------------------------------------------------------------
[docs]    def __init__(self, N=None, resolution=None, origin=None):
        """
        This function creates a new VolumeData object. The object
        represents a three-dimensional volume with the specified resolution,
        origin and dimensions.
        The underlying ``_VDCoordinateFrame`` is exposed via the
        CoordinateFrame property.
        :param N: The number of array-coordinates along the X, Y and Z axes
            respectively.
        :type N: `iterable< int, 3 >`
        :param resolution: The resolution of the X, Y and Z axes respectively.
            Specified in world-coordinate units
        :type resolution: `iterable< float, 3 >`
        :param origin: The origin of the X, Y and Z axes respectively.
            Specified in world-coordinates
        :type origin: `iterable< float, 3 >`
        """
        self._coordinateFrame = \
            
vdcoordinateframe._VDCoordinateFrame(N, resolution, origin)
        self._data = np.zeros(N, dtype=np.float32) 
    # ------------------------------------------------------------------------
    # Property definitions:
    def _get_CoordinateFrame(self):
        return self._coordinateFrame
    CoordinateFrame = property(_get_CoordinateFrame)
    # ------------------------------------------------------------------------
[docs]    def IsCompatible(self, vd):
        """
        This function can be used to test whether vd is compatible with this
        VolumeData. Compatible VolumeData objects have compatible
        coordinate-frames.
        :param vd: The volume-data to be tested for compatibility with this
            VolumeData
        :type vd: `VolumeData`
        :return: True if vd is compatible with this VolumeData
        :rtype: `bool`
        """
        return self._coordinateFrame.IsCompatible(vd._coordinateFrame) 
    # ------------------------------------------------------------------------
[docs]    def ToArrayCoordinate(self, world):
        """
        Converts the specified world-coordinate to the corresponding
        array-coordinate.
        :param world: The world-coordinate to be converted X, Y, Z
        :type world: `iterable< float, 3>`
        :return: The array-coordinate corresponding to world. This need not
            be a valid array-coordinate
        :rtype: `iterable< float, 3 >`
        """
        return self._coordinateFrame.ToArrayCoordinate(world) 
    # ------------------------------------------------------------------------
[docs]    def ToArrayCoordinateL(self, worldCoordinates):
        """
        Converts the specified set of world-coordinates to the corresponding
        array-coordinates.
        :param worldCoordinates: The set of world-coordinates to be converted
        :type worldCoordinates: `iterable< iterable< float, 3 > >`
        :return: The array-coordinates corresponding to worldCoordinates
        :rtype: `iterable< iterable< float, 3 > >`
        """
        return self._coordinateFrame.ToArrayCoordinateL(worldCoordinates) 
    # ------------------------------------------------------------------------
[docs]    def ToWorldCoordinate(self, array):
        """
        Converts the specified array-coordinate to the corresponding
        world-coordinate.
        :param array: The array-coordinate to be converted X, Y, Z
        :type array: `iterable< float, 3 >`
        :return: The world-coordinate corresponding to array
        :rtype: `iterable< float, 3 >`
        """
        return self._coordinateFrame.ToWorldCoordinate(array) 
    # ------------------------------------------------------------------------
[docs]    def ToWorldCoordinateL(self, arrayCoordinates):
        """
        Converts the specified set of array-coordinates to the corresponding
        world-coordinates.
        :param arrayCoordinates: The set of array-coordinates to be converted
        :type arrayCoordinates: `iterable< iterable< float, 3 > >`
        :return: The world-coordinates corresponding to arrayCoordinates
        :rtype: `iterable< iterable< float, 3 > >`
        """
        return self._coordinateFrame.ToWorldCoordinateL(arrayCoordinates) 
    # ------------------------------------------------------------------------
[docs]    def InBounds(self, world):
        """
        Tests whether the world-coordinate corresponds to a position that is
        within the bounds of the array-coordinates.
        :param world: The world-coordinate to be tested X, Y, Z
        :type world: `iterable< float, 3 >`
        :return: True if the world-coordinate is in bounds.
        :rtype: `bool`
        """
        return self._coordinateFrame.InBounds(world) 
    # ------------------------------------------------------------------------
[docs]    def ArrayCoordinates(self):
        """
        This function returns an iterator which allows the array-coordinates
        corresponding to this VolumeData to be traversed. The order of the
        traversal is not specified. The de-referenced iterator provides an
        object of the form iterable< int, 3 >, the X, Y and Z
        array-coordinates.
        :return: Array-coordinate iterator.
        :rtype: `iterator< iterable< int, 3 > >`
        """
        return self._coordinateFrame.ArrayCoordinates() 
    # ------------------------------------------------------------------------
[docs]    def getAllArrayCoordinates(self):
        """
        This function returns an object of the class
        iterable< iterable< int, 3 > >. This contains all of the valid
        array-coordinates. The ordering of entries in this object is guaranteed
        to be the same as that returned by self.getAllWorldCoordinates().
        :return: The array-coordinates
        :rtype: `iterable< iterable< int, 3 > >`
        """
        return self._coordinateFrame.getAllArrayCoordinates() 
    # ------------------------------------------------------------------------
[docs]    def WorldCoordinates(self):
        """
        This function returns an iterator which allows the world-coordinates
        corresponding to this VolumeData to be traversed. The order of
        traversal is not specified. The de-referenced iterator provides an
        object of the form iterable< float, 3 >, the X, Y and Z
        world-coordinates.
        :return: World-coordinate iterator
        :rtype: `iterator< iterable< float, 3 > >`
        """
        return self._coordinateFrame.WorldCoordinates() 
    # ------------------------------------------------------------------------
[docs]    def getAllWorldCoordinates(self):
        """
        This function returns an object of the class
        iterable< iterable< float, 3 > >. This contains all of the
        world-coordinates. The ordering of entries in this object is guaranteed
        to be the same as that returned by self.getAllArrayCoordinates().
        :return: The world-coordinates
        :rtype: `iterable< iterable< float, 3 > >`
        """
        return self._coordinateFrame.getAllWorldCoordinates() 
    # ------------------------------------------------------------------------
[docs]    def Coordinates(self):
        """
        This function returns an iterator which allows the array-coordinates
        and world-coordinates corresponding to this VolumeData to be traversed.
        The order of traversal is not specified. The de-referenced iterator
        returns an object of the form
        tuple< iterable< int, 3 >, iterable< float, 3 > >, the X, Y, Z
        coordinates of the array and world respectively.
        :return: Array and world-coordinate iterator
        :rtype: `iterator< tuple< iterable< int, 3 >, iterable< float, 3 > > >`
        """
        return self._coordinateFrame.Coordinates() 
    # ------------------------------------------------------------------------
[docs]    def getData(self):
        """
        This function allows access to the underlying data. The object
        returned by this function may be used anywhere a Numpy array object
        would be used. This is the fastest method accessing values from this
        VolumeData object, however, the access is restricted to valid
        array-coordinates only.
        :return: The underlying volume-data
        :rtype: `numpy.array`
        """
        return self._data 
    # ------------------------------------------------------------------------
[docs]    def setData(self, data):
        """
        This function allows the underlying data to be set. The function makes
        a copy of data.
        :param data: The data to be assigned to  this VolumeData's underlying
            data. The size of this three-dimensional array should be the same
            as this VolumeData
        :type data: `numpy.array`
        """
        if ((data.shape[_X] != self._coordinateFrame.N[_X]) or
            (data.shape[_Y] != self._coordinateFrame.N[_Y]) or
            (data.shape[_Z] != self._coordinateFrame.N[_Z])):
            raise vdexception.VDException(_BAD_DATA)
        self._data = copy.copy(data) 
    def __iadd__(self, rhs):
        """
        __iadd__, __isub__, __imul__, __idiv__: These functions perform the
        standard mathematic operations on this VolumeData.
        :param rhs: If rhs is numerical the appropriate operation will be
            performed on every element of the data. If rhs is a VolumeData
            instance each element of this VolumeData will be modified with the
            corresponding element of rhs. This implies that the two VolumeData
            objects must be compatible.
        :type rhs: `VolumeData` or `numerical`
        """
        if isinstance(rhs, VolumeData):
            if not self.IsCompatible(rhs):
                raise vdexception.VDException(_BAD_VD)
            self._data += rhs._data
        else:
            self._data += rhs
        return self
    def __isub__(self, rhs):
        """ see __iadd__ docstring """
        if isinstance(rhs, VolumeData):
            if not self.IsCompatible(rhs):
                raise vdexception.VDException(_BAD_VD)
            self._data -= rhs._data
        else:
            self._data -= rhs
        return self
    def __imul__(self, rhs):
        """ see __iadd__ docstring """
        if isinstance(rhs, VolumeData):
            if not self.IsCompatible(rhs):
                raise vdexception.VDException(_BAD_VD)
            self._data *= rhs._data
        else:
            self._data *= rhs
        return self
    def __idiv__(self, rhs):
        """ see __iadd__ docstring """
        if isinstance(rhs, VolumeData):
            if not self.IsCompatible(rhs):
                raise vdexception.VDException(_BAD_VD)
            self._data /= rhs._data
        else:
            self._data /= rhs
        return self
    # ------------------------------------------------------------------------
[docs]    def getAtArrayCoordinate(self,
                             array,
                             interpolationOrder=0,
                             oobMethod="constant",
                             oobConstant=0.0):
        """
        This function is used to retrieve values from this VolumeData
        object using array-coordinates. The function is capable of retrieving
        values at invalid array-coordinates using a mixture of interpolation
        and OOB-handling.
        :param array: The array-coordinates to retrieve. Need not be valid
            array-coordinates
        :type array: `iterable< float, 3 >`
        :param interpolationOrder: The degree of interpolation to use when
            retrieving the values. 0-5
        :type interpolationOrder: `int`
        :param oobMethod: What to do with requests that lie outside of the
            bounds of this VolumeData object. The options are "constant", which
            returns the value of oobConstant. "nearest" which returns the value
            of the nearest valid point or "wrap", which effectively tiles the data
            into an infinite repeating lattice.
        :type oobMethod: `string`
        :param oobConstant: Of the class float. The value to return if the
            request is OOB and the oobMethod is "constant"
        :type oobConstant: `float`
        :return: The value stored at the requested array-coordinate
        :rtype: `float`
        """
        if len(array) != 3:
            raise vdexception.VDException(_BAD_COORD)
        # Annoyingly map_coordinates requires [ [ x ], [ y ], [ z ] ]
        # format, rather than [ x, y, z ], so we need a transpose.
        array2 = np.array(array).reshape(3, 1)
        return interpolation.map_coordinates(self._data,
                                             array2,
                                             order=interpolationOrder,
                                             mode=oobMethod,
                                             cval=oobConstant) 
    # ------------------------------------------------------------------------
[docs]    def getAtArrayCoordinateL(self,
                              arrayCoordinates,
                              interpolationOrder=0,
                              oobMethod="constant",
                              oobConstant=0.0):
        """
        This function can be used to retrieve a large number of values at
        specified array-coordinates. It is similar to the getAtArrayCoordinate
        function, however, in this case the array-coordinates are specified
        as a list rather than a single coordinate.
        :param arrayCoordinates: The array-coordinates whose values are to be
            retrieved
        :type arrayCoordinates: `iterable< iterable< float, 3 > >`
        :param interpolationOrder: The degree of interpolation to use when
            retrieving the values. 0-5
        :type interpolationOrder: `int`
        :param oobMethod: What to do with requests that lie outside of the
            bounds of this VolumeData object. The options are "constant", which
            returns the value of oobConstant. "nearest" which returns the value
            of the nearest valid point or "wrap", which effectively tiles the data
            into an infinite repeating lattice.
        :type oobMethod: `string`
        :param oobConstant: Of the class float. The value to return if the
            request is OOB and the oobMethod is "constant"
        :type oobConstant: `float`
        :return: The value stored at the requested array-coordinates. The
            values are returned in an order that is equivalent to arrayCoordinates
        :rtype: `iterable< float >`
        """
        if len(arrayCoordinates[0]) != 3:
            raise vdexception.VDException(_BAD_COORD)
        # Annoyingly map_coordinates requires [ [ x ], [ y ], [ z ] ]
        # format, rather than [ x, y, z ], so we need a transpose.
        arrayCoordinates2 = np.array(arrayCoordinates).transpose()
        return interpolation.map_coordinates(self._data,
                                             arrayCoordinates2,
                                             order=interpolationOrder,
                                             mode=oobMethod,
                                             cval=oobConstant) 
    # ------------------------------------------------------------------------
[docs]    def getAtWorldCoordinate(self,
                             world,
                             interpolationOrder=0,
                             oobMethod="constant",
                             oobConstant=0.0):
        """
        This function is used to retrieve values from this VolumeData object
        using world-coordinates. The function is capable of retrieving values
        at any world-coordinate using a mixture of interpolation and
        OOB-handling
        :param world: The world-coordinates to retrieve
        :type world: `iterable< float, 3 >`
        :param interpolationOrder: The degree of interpolation to use when
            retrieving the values. 0-5
        :type interpolationOrder: `int`
        :param oobMethod: What to do with requests that lie outside of the
            bounds of this VolumeData object. The options are "constant", which
            returns the value of oobConstant. "nearest" which returns the value
            of the nearest valid point or "wrap", which effectively tiles the data
            into an infinite repeating lattice.
        :type oobMethod: `string`
        :param oobConstant: Of the class float. The value to return if the
            request is OOB and the oobMethod is "constant"
        :type oobConstant: `float`
        :return: The value stored at the requested world-coordinate
        :rtype: `float`
        """
        array = self.ToArrayCoordinate(world)
        return self.getAtArrayCoordinate(array,
                                         interpolationOrder=interpolationOrder,
                                         oobMethod=oobMethod,
                                         oobConstant=oobConstant) 
    # ------------------------------------------------------------------------
[docs]    def getAtWorldCoordinateL(self,
                              worldCoordinates,
                              interpolationOrder=0,
                              oobMethod="constant",
                              oobConstant=0.0):
        """
        This function can be used to retrieve a large number of values at
        specified world-coordinates. It is similar to the getAtWorldCoordinate
        function, however, in this case the world-coordinates are specified as
        a list rather than a single coordinate.
        :param worldCoordinates: The world-coordinates whose values are to be
            retrieved
        :type worldCoordinates: `iterable< iterable< float, 3 > >`
        :param interpolationOrder: The degree of interpolation to use when
            retrieving the values. 0-5
        :type interpolationOrder: `int`
        :param oobMethod: What to do with requests that lie outside of the
            bounds of this VolumeData object. The options are "constant", which
            returns the value of oobConstant. "nearest" which returns the value
            of the nearest valid point or "wrap", which effectively tiles the data
            into an infinite repeating lattice.
        :type oobMethod: `string`
        :param oobConstant: Of the class float. The value to return if the
            request is OOB and the oobMethod is "constant"
        :type oobConstant: `float`
        :return: The value stored at the requested world-coordinates. The
            values are returned in an order that is equivalent to worldCoordinates
        :rtype: `iterable< float >`
        """
        arrayCoordinates = self.ToArrayCoordinateL(worldCoordinates)
        return self.getAtArrayCoordinateL(arrayCoordinates,
                                          interpolationOrder=interpolationOrder,
                                          oobMethod=oobMethod,
                                          oobConstant=oobConstant)  
# End of class definition: VolumeData
# ----------------------------------------------------------------------------
# End of file: volumedata.py
# ----------------------------------------------------------------------------