# ----------------------------------------------------------------------------
# Name:
#
# volumedatautils.py
#
# Purpose:
#
# This file contains the implementation of the VolumeDataUtils module. The
# VolumeDataUtils module contains a number of utility functions for creating,
# handling and using VolumeData objects.
#
# Copyright of:
#
# Copyright Schrodinger, LLC. All rights reserved.
#
# Version:
#
# Version Author Notes
# 1.0 DDR Original Implementation
#
# Notes:
#
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Module imports.
from past.utils import old_div
import numpy as np
import scipy.spatial as spatial
from . import vdexception
from . import volumedata
# End of module imports.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Global constants.
_X = 0
_Y = 1
_Z = 2
_DOWN_COLUMNS = 0
_ACROSS_ROWS = 1
_INVALID_VOLUME_SPEC = "min/maxWorldCoordinates and resolutions must be same size."
_BAD_RESOLUTION = "resolution must be a 3-element array"
_BAD_COORD = "Coordinate must be a 3-element array"
_INVALID_RESOLUTION_MODE = "resolutionMode must be highest/lowest/fixed."
# End of global constants.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Function definition:
#
# CreateLike
#
# ---------------------------------------------------------------------------
[docs]def CreateLike(volumeData, initialValue=0.0):
"""
This function creates a new VolumeData instance that is identical in size
and resolution to volumeData. All data-points within the newly constructed
VolumeData instance are set to initialValue.
:param volumeData: The template VolumeData instance
:type volumeData: ``volumeData``
:param initialValue: The initial value to assign to every data-point of
the new VolumeData instance
:type initialValue: `float`
:return: The newly constructed VolumeData instance
:rtype: ``VolumeData``
"""
N = volumeData.CoordinateFrame.N
resolution = volumeData.CoordinateFrame.resolution
origin = volumeData.CoordinateFrame.origin
ret = volumedata.VolumeData(N=N, resolution=resolution, origin=origin)
ret.setData(np.ones_like(ret.getData()) * initialValue)
return ret
# End of function: CreateLike.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Function definition:
#
# Resize
#
# ----------------------------------------------------------------------------
[docs]def Resize(volumeData,
N=None,
resolution=None,
origin=None,
interpolationOrder=0,
oobMethod="constant",
oobConstant=0.0):
"""
This function is used to resize the specified volumeData object, returning
a new VolumeData instance with the specified N, resolution and origin.
:param volumeData: The VolumeData instance to be resized.
:type volumeData: VolumeData
: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
: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: A new VolumeData instance covering the specified region of space,
filled with data from the argument volumeData instance.
:rtype: VolumeData
"""
# ------------------------------------------------------------------------
# Create the new VolumeData that will hold the resized data.
ret = volumedata.VolumeData(N=N, resolution=resolution, origin=origin)
# ------------------------------------------------------------------------
# Acquire the array and world-coordinates of this new VolumeData instance.
newArrayCoordinates = ret.getAllArrayCoordinates()
newWorldCoordinates = ret.getAllWorldCoordinates()
# ------------------------------------------------------------------------
# Get the values from volumeData at these world-coordinates.
values = volumeData.getAtWorldCoordinateL(
newWorldCoordinates,
interpolationOrder=interpolationOrder,
oobMethod=oobMethod,
oobConstant=oobConstant)
# ------------------------------------------------------------------------
# Place these values into the new VolumeData.
for i, value in enumerate(values):
x = newArrayCoordinates[i][_X]
y = newArrayCoordinates[i][_Y]
z = newArrayCoordinates[i][_Z]
ret.getData()[x][y][z] = value
return ret
# End of function: Resize.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Function definition:
#
# CalculateContainingVolume
#
# ----------------------------------------------------------------------------
[docs]def CalculateContainingVolume(minWorldCoordinates,
maxWorldCoordinates,
resolutions,
resolutionMode="highest",
resolutionValue=None):
"""
This function calculates values for N, origin and resolution that span the
specified world-coordinate range.
``len(resolutions) == len(minWorldCoordinates) == len(maxWorldCoordinates)``
:param minWorldCoordinates: The minimum world-coordinates to consider
:type minWorldCoordinates: `iterable<iterable<float, 3>>`
:param maxWorldCoordinates: The maximum world-coordinates to consider
:type maxWorldCoordinates: `iterable<iterable<float, 3>>`
:param resolutions: The resolutions to consider
:type resolutions: `iterable<float, 3>`
:param resolutionMode: Either "highest", in which case the highest
resolution is used for the resized VolumeDatas, "lowest", in which
case the lowest resolution is used, or "fixed", in which case the resolution
specified by resolutionValue will be used
:type resolutionMode: `string`
:param resolutionValue: The resolution value to use when the resolutionMode
is "fixed". Ignored otherwise
:type resolutionValue: `iterable<float, 3>`
:return: These are the values of N, resolution and origin that span the
input world-coordinate range
:rtype: `tuple<iterable<int, 3>, iterable<float, 3>, iterable<float, 3>>`
"""
if ((len(minWorldCoordinates) != len(maxWorldCoordinates)) or
(len(minWorldCoordinates) != len(resolutions))):
raise vdexception.VDException(_INVALID_VOLUME_SPEC)
if ((len(minWorldCoordinates[0]) != 3) or
(len(maxWorldCoordinates[0]) != 3)):
raise vdexception.VDException(_BAD_COORD)
if len(resolutions[0]) != 3:
raise vdexception.VDException(_BAD_RESOLUTION)
# ------------------------------------------------------------------------
# Ensure the coordinates and resolutions are passed in a manner that is
# compatible with np.min/np.max
minWorldCoordinates = np.array(minWorldCoordinates)
maxWorldCoordinates = np.array(maxWorldCoordinates)
resolutions = np.array(resolutions)
origin = np.min(minWorldCoordinates, axis=_DOWN_COLUMNS)
maxWorldCoordinate = np.max(maxWorldCoordinates, axis=_DOWN_COLUMNS)
extent = maxWorldCoordinate - origin
if resolutionMode == "highest":
resolution = np.min(resolutions, axis=_DOWN_COLUMNS)
elif resolutionMode == "lowest":
resolution = np.max(resolutions, axis=_DOWN_COLUMNS)
elif resolutionMode == "fixed":
if resolutionValue is None:
raise vdexception.VDException(_BAD_RESOLUTION)
resolution = resolutionValue
else:
raise vdexception.VDException(_INVALID_RESOLUTION_MODE)
N = np.ceil(old_div(extent, resolution)).astype(np.int)
return N, resolution, origin
# End of function: CalculateContainingVolume.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Function definition:
#
# MakeConsistent
#
# ----------------------------------------------------------------------------
[docs]def MakeConsistent(volumeDatas,
resolutionMode="highest",
resolutionValue=None,
interpolationOrder=0,
oobMethod="constant",
oobConstant=0.0):
"""
This function can be used to make all of the VolumeData instances in
volumeDatas consistent with each other. This is done by resizing all of
the VolumeData instances so that they span the union volume of the
individual VolumeData instances.
:param volumeDatas: The VolumeData instances to make consistent with each
other.
:type volumeDatas: `iterable<VolumeData>`
:param resolutionMode: Either "highest", in which case the highest
resolution is used for the resized VolumeDatas, "lowest", in which
case the lowest resolution is used, or "fixed", in which case the
resolution specified by resolutionValue will be used
:type resolutionMode: `string`
:param resolutionValue: The resolution value to use when the resolutionMode
is "fixed". Ignored otherwise
:type resolutionValue: `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: These are the set of consistent VolumeData instances formed from
volumeDatas
:rtype: `iterable< VolumeData >`
"""
# ------------------------------------------------------------------------
# Retrieve the extents of the various VolumeData instances. Convert these
# to a min, max and resolution for the consistent grids.
minWorldCoordinates = [vd.CoordinateFrame.origin for vd in volumeDatas]
maxWorldCoordinates = [vd.CoordinateFrame.max for vd in volumeDatas]
resolutions = [vd.CoordinateFrame.resolution for vd in volumeDatas]
N, resolution, origin = CalculateContainingVolume(
minWorldCoordinates,
maxWorldCoordinates,
resolutions,
resolutionMode=resolutionMode,
resolutionValue=resolutionValue)
# ------------------------------------------------------------------------
# Resize all of the grids.
ret = [
Resize(vd,
N=N,
resolution=resolution,
origin=origin,
interpolationOrder=interpolationOrder,
oobMethod=oobMethod,
oobConstant=oobConstant) for vd in volumeDatas
]
return ret
# End of function: MakeConsistent.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Class definition:
#
# DataPointLocator
#
# ----------------------------------------------------------------------------
[docs]class DataPointLocator(object):
"""
The DataPointLocator class is a simple utility class that allows
data-points that are 'close' to a given position in space to be located
easily. This class uses spatial partitioning (k-d tree) to ensure that
the search for data-points is carried out in a computationally efficient
manner.
The class is designed to be used in the following manner:
1) Instantiate a new DataPointLocator object. This sets up the instance
ready to carry out the search. At this point the results list is empty.
2) Execute the various SearchForDataPointsWithinX methods to fill the
internal results list with array-coordinates that are within the
specified world-distance of the various search origins.
3) Optionally 'uniquify' the results list.
4) Examine the results list to find those points that were within the
specified distances of the search origins.
"""
# ------------------------------------------------------------------------
[docs] def __init__(self, volumeData):
"""
This function creates a new DataPointLocator instance.
:param volumeData: The VolumeData instance to run the queries against
:type volumeData: ``VolumeData``
"""
# --------------------------------------------------------------------
# Instantiate the kd-tree and fill it with the world-coordinates of
# the volumeData.
self._kdtree = spatial.KDTree(volumeData.getAllWorldCoordinates())
self._volumeData = volumeData
# --------------------------------------------------------------------
# Set up the empty results list. This will store indices into the
# array-coordinates of self._volumeData.
self._results = list()
# ------------------------------------------------------------------------
[docs] def SearchForDataPointsWithin(self, world, distance):
"""
This function searches for valid array-coordinates whose corresponding
world-coordinate lies within distance of the specified
world-coordinate. The array-coordinates are appended to the internal
results list.
:param world: The search origin, specified in world-coordinates
:type world: `iterable<float, 3>`
:param distance: Locate all valid array-coordinates whose corresponding
world-coordinate lies with distance of world
:type distance: `float`
"""
indices = self._kdtree.query_ball_point(world, distance)
self._results.extend(indices)
# ------------------------------------------------------------------------
[docs] def UniquifyResults(self):
"""
This function uniquifies the result list so that each array-coordinate
appears only once.
"""
self._results = list(set(self._results))
# ------------------------------------------------------------------------
[docs] def ClearResults(self):
"""
This function clears the current result list.
"""
self._results = list()
# ------------------------------------------------------------------------
def _getResults(self):
"""
This function and the synonymous property Results allows access to the
result list.
:return: The array-coordinates located by the various calls to
SearchForDataPointsWithinX
:rtype: `iterable< iterable< int, 3 > >`
"""
# --------------------------------------------------------------------
# Create an array containing the array-coordinates that correspond
# to the results list.
arrayCoordinates = self._volumeData.getAllArrayCoordinates()
ret = [arrayCoordinates[index] for index in self._results]
return ret
Results = property(_getResults)
"""
This property allows access to the result list.
"""
# End of class definition: DataPointLocator.
# ----------------------------------------------------------------------------
# End of file: volumedatautils.py
# ----------------------------------------------------------------------------