Omaha #4480: Implement API for retrieving pure python soundings through DAF.

Change-Id: I11801ce387b2822e3276c4bfa715bc475afbe506

Former-commit-id: f222bee8ed317033e97fcea8d0611d666653402a
This commit is contained in:
David Gillingham 2015-06-24 15:57:21 -05:00
parent 1b3a96d2e6
commit b59b5d3cd7
8 changed files with 657 additions and 61 deletions

View file

@ -28,6 +28,8 @@
# ------------ ---------- ----------- --------------------------
# 05/29/13 2023 dgilling Initial Creation.
# 02/12/14 2672 bsteffen Allow String constructor to parse floats.
# 06/29/15 4480 dgilling Implement __hash__, __eq__,
# __str__ and rich comparison operators.
#
@ -58,6 +60,120 @@ class Level(object):
if levelTwo:
self.leveltwovalue = numpy.float64(levelTwo)
def __hash__(self):
# XOR-ing the 3 items in a tuple ensures that order of the
# values matters
hashCode = hash(self.masterLevel) ^ hash(self.levelonevalue) ^ hash(self.leveltwovalue)
hashCode ^= hash((self.masterLevel, self.levelonevalue, self.leveltwovalue))
return hashCode
def __eq__(self, other):
if type(self) != type(other):
return False
else:
return (self.masterLevel, self.levelonevalue, self.leveltwovalue) == \
(other.masterLevel, other.levelonevalue, other.leveltwovalue)
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if type(self) != type(other):
return NotImplemented
elif self.masterLevel.getName() != other.masterLevel.getName():
return NotImplemented
myLevel1 = self.levelonevalue
myLevel2 = self.leveltwovalue
otherLevel1 = other.levelonevalue
otherLevel2 = other.leveltwovalue
if myLevel1 == INVALID_VALUE and myLevel2 != INVALID_VALUE:
myLevel1 = myLevel2
myLevel2 = INVALID_VALUE
if otherLevel1 == INVALID_VALUE and otherLevel2 != INVALID_VALUE:
otherLevel1 = otherLevel2
otherLevel2 = INVALID_VALUE
# We default to descending order to make sorting levels from the DAF easier
compareType = self.masterLevel.getType() if self.masterLevel.getType() else "DEC"
if myLevel1 != INVALID_VALUE and otherLevel1 != INVALID_VALUE:
level1Cmp = self.__compareLevelValues(compareType, myLevel1, otherLevel1)
if level1Cmp == -1:
if myLevel2 != INVALID_VALUE and otherLevel2 != INVALID_VALUE:
level2Cmp = self.__compareLevelValues(compareType, myLevel2, otherLevel2)
return level2Cmp == -1
elif myLevel2 != INVALID_VALUE:
level2Cmp = self.__compareLevelValues(compareType, myLevel2, otherLevel1)
return level2Cmp == -1
else:
return True
return False
def __le__(self, other):
if type(self) != type(other):
return NotImplemented
elif self.masterLevel.getName() != other.masterLevel.getName():
return NotImplemented
return self.__lt__(other) or self.__eq__(other)
def __gt__(self, other):
if type(self) != type(other):
return NotImplemented
elif self.masterLevel.getName() != other.masterLevel.getName():
return NotImplemented
myLevel1 = self.levelonevalue
myLevel2 = self.leveltwovalue
otherLevel1 = other.levelonevalue
otherLevel2 = other.leveltwovalue
if myLevel1 == INVALID_VALUE and myLevel2 != INVALID_VALUE:
myLevel1 = myLevel2
myLevel2 = INVALID_VALUE
if otherLevel1 == INVALID_VALUE and otherLevel2 != INVALID_VALUE:
otherLevel1 = otherLevel2
otherLevel2 = INVALID_VALUE
# We default to descending order to make sorting levels from the DAF easier
compareType = self.masterLevel.getType() if self.masterLevel.getType() else "DEC"
if myLevel1 != INVALID_VALUE and otherLevel1 != INVALID_VALUE:
level1Cmp = self.__compareLevelValues(compareType, myLevel1, otherLevel1)
if level1Cmp == 1:
if myLevel2 != INVALID_VALUE and otherLevel2 != INVALID_VALUE:
level2Cmp = self.__compareLevelValues(compareType, myLevel2, otherLevel2)
return level2Cmp == 1
elif otherLevel2 != INVALID_VALUE:
level2Cmp = self.__compareLevelValues(compareType, myLevel1, otherLevel2)
return level2Cmp == 1
else:
return True
return False
def __ge__(self, other):
if type(self) != type(other):
return NotImplemented
elif self.masterLevel.getName() != other.masterLevel.getName():
return NotImplemented
return self.__gt__(other) or self.__eq__(other)
def __compareLevelValues(self, compareType, val1, val2):
returnVal = 0
if val1 < val2:
returnVal = -1 if compareType == 'INC' else 1
elif val2 < val1:
returnVal = 1 if compareType == 'INC' else -1
return returnVal
def __str__(self):
retVal = ""
if INVALID_VALUE != self.levelonevalue:
retVal += str(self.levelonevalue)
if INVALID_VALUE != self.leveltwovalue:
retVal += "_" + str(self.leveltwovalue)
retVal += str(self.masterLevel.getName())
return retVal
def getId(self):
return self.id
@ -87,4 +203,3 @@ class Level(object):
def setIdentifier(self, identifier):
self.identifier = identifier

View file

@ -27,6 +27,8 @@
# Date Ticket# Engineer Description
# ------------ ---------- ----------- --------------------------
# 05/29/13 2023 dgilling Initial Creation.
# 06/29/15 4480 dgilling Implement __hash__, __eq__
# and __str__.
#
#
@ -39,6 +41,27 @@ class MasterLevel(object):
self.type = None
self.identifier = None
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
if type(self) != type(other):
return False
else:
return self.name == other.name
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
retVal = "MasterLevel["
retVal += "name=" + str(self.name) + ","
retVal += "type=" + str(self.type) + ","
retVal += "unit=" + str(self.unitString) + ","
retVal += "description=" + str(self.description)
retVal += "]"
return retVal
def getName(self):
return self.name

View file

@ -31,6 +31,9 @@
# 01/22/14 2667 bclement preserved milliseconds in string representation
# 03/03/14 2673 bsteffen allow construction using a Date for refTime
# 06/24/14 3096 mnash implement __cmp__
# 06/24/15 4480 dgilling implement __hash__ and __eq__,
# replace __cmp__ with rich comparison
# operators.
#
import calendar
@ -66,9 +69,7 @@ class DataTime(object):
self.refTime = long(self.refTime.getTime())
else:
self.refTime = long(refTime)
dateObj = Date()
dateObj.setTime(self.refTime)
self.refTime = dateObj
self.refTime = Date(self.refTime)
if self.validPeriod is None:
validTimeMillis = self.refTime.getTime() + long(self.fcstTime * 1000)
@ -112,38 +113,6 @@ class DataTime(object):
def setLevelValue(self, levelValue):
self.levelValue = numpy.float64(levelValue)
def __cmp__(self, other):
if other is None :
return 1
# compare the valid times, which are the ref times + forecast times
validTimeCmp = cmp(self.getRefTime().getTime() + self.getFcstTime(),
other.getRefTime().getTime() + other.getFcstTime())
if validTimeCmp != 0 :
return validTimeCmp
# compare the forecast times
fcstTimeCmp = cmp(self.getFcstTime(), other.getFcstTime())
if fcstTimeCmp != 0 :
return fcstTimeCmp
# compare the level values
levelCmp = cmp(self.getLevelValue(), other.getLevelValue())
if levelValue != 0 :
return levelValue
# compare the valid periods
period1 = self.getValidPeriod()
period2 = other.getValidPerid()
if period1 is None :
return -1
elif period2 is None :
return 1
return cmp(period1.getDuration(), period2.getDuration())
def __str__(self):
buffer = StringIO.StringIO()
@ -172,3 +141,86 @@ class DataTime(object):
strVal = buffer.getvalue()
buffer.close()
return strVal
def __repr__(self):
return "<DataTime instance: " + str(self) + " >"
def __hash__(self):
hashCode = hash(self.refTime) ^ hash(self.fcstTime)
if self.validPeriod is not None and self.validPeriod.isValid():
hashCode ^= hash(self.validPeriod.getStart())
hashCode ^= hash(self.validPeriod.getEnd())
hashCode ^= hash(self.levelValue)
return hashCode
def __eq__(self, other):
if type(self) != type(other):
return False
if other.getRefTime() is None:
return self.fcstTime == other.fcstTime
dataTime1 = (self.refTime, self.fcstTime, self.validPeriod, self.levelValue)
dataTime2 = (other.refTime, other.fcstTime, other.validPeriod, other.levelValue)
return dataTime1 == dataTime2
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if type(self) != type(other):
return NotImplemented
myValidTime = self.getRefTime().getTime() + self.getFcstTime()
otherValidTime = other.getRefTime().getTime() + other.getFcstTime()
if myValidTime < otherValidTime:
return True
if self.fcstTime < other.fcstTime:
return True
if self.levelValue < other.levelValue:
return True
myValidPeriod = self.validPeriod
otherValidPeriod = other.validPeriod
if myValidPeriod != otherValidPeriod:
if myValidPeriod.duration() < otherValidPeriod.duration():
return True
return myValidPeriod.getStartInMillis() < otherValidPeriod.getStartInMillis()
return False
def __le__(self, other):
if type(self) != type(other):
return NotImplemented
return self.__lt__(other) or self.__eq__(other)
def __gt__(self, other):
if type(self) != type(other):
return NotImplemented
myValidTime = self.getRefTime().getTime() + self.getFcstTime()
otherValidTime = other.getRefTime().getTime() + other.getFcstTime()
if myValidTime > otherValidTime:
return True
if self.fcstTime > other.fcstTime:
return True
if self.levelValue > other.levelValue:
return True
myValidPeriod = self.validPeriod
otherValidPeriod = other.validPeriod
if myValidPeriod != otherValidPeriod:
if myValidPeriod.duration() > otherValidPeriod.duration():
return True
return myValidPeriod.getStartInMillis() > otherValidPeriod.getStartInMillis()
return False
def __ge__(self, other):
if type(self) != type(other):
return NotImplemented
return self.__gt__(other) or self.__eq__(other)

View file

@ -28,6 +28,7 @@
# ??/??/?? xxxxxxxx Initial Creation.
# 01/22/14 2667 bclement fixed millisecond support
# 02/28/14 2667 bclement constructor can take extra micros for start and end
# 06/24/15 4480 dgilling fix __eq__.
#
#
#
@ -51,7 +52,15 @@ class TimeRange(object):
return "(" + self.start.strftime("%b %d %y %H:%M:%S %Z") + ", " + self.end.strftime("%b %d %y %H:%M:%S %Z") + ")"
def __eq__(self, other):
return ((self.start == other.start) and (self.end == other.end))
if type(self) != type(other):
return False
if self.isValid() and other.isValid():
return self.getStart() == other.getStart() and self.getEnd() == other.getEnd()
elif not self.isValid() and not other.isValid():
return True
else:
return False
def __ne__(self, other):
return (not self.__eq__(other))
@ -132,7 +141,7 @@ class TimeRange(object):
return convTime == self.start
def isValid(self):
return (self.start != self.end)
return bool(self.start != self.end)
def overlaps(self, timeRange):
return (timeRange.contains(self.start) or self.contains(timeRange.start))

View file

@ -18,25 +18,22 @@
# further licensing information.
##
## NOTE: This is a dummy class that is only used for deserialization
## support. Further work required if it is need in the pure Python
## environment.
# File auto-generated against equivalent DynamicSerialize Java class
# and then modified post-generation to add additional features to better
# match Java implementation.
#
# SOFTWARE HISTORY
#
# Date Ticket# Engineer Description
# ------------ ---------- ----------- --------------------------
# ??/??/?? xxxxxxxx Initial Creation.
# 06/24/15 4480 dgilling implement based on Date class.
#
class Timestamp(object):
from dynamicserialize.dstypes.java.util import Date
class Timestamp(Date):
def __init__(self, time=None):
self.time = time
def getTime(self):
return self.time
def setTime(self, timeInMillis):
self.time = timeInMillis
def __str__(self):
return self.__repr__()
def __repr__(self):
from time import gmtime, strftime
return strftime("%b %d %y %H:%M:%S GMT", gmtime(self.time/1000.0))
super(Timestamp, self).__init__(time)

View file

@ -24,14 +24,17 @@
# Date Ticket# Engineer Description
# ------------ ---------- ----------- --------------------------
# 04/28/2015 4027 randerso Added optional construction parameter to set the time
# 06/26/2015 4480 dgilling Implement __eq__ and __hash__.
#
##
from time import gmtime, strftime
class Date(object):
def __init__(self, timeInMillis=None):
self.time = None
self.time = timeInMillis
def getTime(self):
return self.time
@ -43,6 +46,13 @@ class Date(object):
return self.__repr__()
def __repr__(self):
from time import gmtime, strftime
return strftime("%b %d %y %H:%M:%S GMT", gmtime(self.time/1000.0))
return strftime("%b %d %y %H:%M:%S GMT", gmtime(self.time/1000.0))
def __eq__(self, other):
return self.time == other.time
def __ne__(self, other):
return not self.__eq__(other)
def __hash__(self):
return hash(self.time)

View file

@ -0,0 +1,107 @@
# #
# This software was developed and / or modified by Raytheon Company,
# pursuant to Contract DG133W-05-CQ-1067 with the US Government.
#
# U.S. EXPORT CONTROLLED TECHNICAL DATA
# This software product contains export-restricted data whose
# export/transfer/disclosure is restricted by U.S. law. Dissemination
# to non-U.S. persons whether in the United States or abroad requires
# an export license or other authorization.
#
# Contractor Name: Raytheon Company
# Contractor Address: 6825 Pine Street, Suite 340
# Mail Stop B8
# Omaha, NE 68106
# 402.291.0100
#
# See the AWIPS II Master Rights File ("Master Rights File.pdf") for
# further licensing information.
# #
#
# Functions for converting between the various "Java" dynamic serialize types
# used by EDEX to the native python time datetime.
#
#
# SOFTWARE HISTORY
#
# Date Ticket# Engineer Description
# ------------ ---------- ----------- --------------------------
# 06/24/15 #4480 dgilling Initial Creation.
#
import datetime
import time
from dynamicserialize.dstypes.java.util import Date
from dynamicserialize.dstypes.java.sql import Timestamp
from dynamicserialize.dstypes.com.raytheon.uf.common.time import TimeRange
MAX_TIME = pow(2, 31) - 1
MICROS_IN_SECOND = 1000000
def convertToDateTime(timeArg):
"""
Converts the given object to a python datetime object. Supports native
python representations like datetime and struct_time, but also
the dynamicserialize types like Date and Timestamp. Raises TypeError
if no conversion can be performed.
Args:
timeArg: a python object representing a date and time. Supported
types include datetime, struct_time, float, int, long and the
dynamicserialize types Date and Timestamp.
Returns:
A datetime that represents the same date/time as the passed in object.
"""
if isinstance(timeArg, datetime.datetime):
return timeArg
elif isinstance(timeArg, time.struct_time):
return datetime.datetime(*timeArg[:6])
elif isinstance(timeArg, float):
# seconds as float, should be avoided due to floating point errors
totalSecs = long(timeArg)
micros = int((timeArg - totalSecs) * MICROS_IN_SECOND)
return _convertSecsAndMicros(totalSecs, micros)
elif isinstance(timeArg, (int, long)):
# seconds as integer
totalSecs = timeArg
return _convertSecsAndMicros(totalSecs, 0)
elif isinstance(timeArg, (Date, Timestamp)):
totalSecs = timeArg.getTime()
return _convertSecsAndMicros(totalSecs, 0)
else:
objType = str(type(timeArg))
raise TypeError("Cannot convert object of type " + objType + " to datetime.")
def _convertSecsAndMicros(seconds, micros):
if seconds < MAX_TIME:
rval = datetime.datetime.utcfromtimestamp(seconds)
else:
extraTime = datetime.timedelta(seconds=(seconds - MAX_TIME))
rval = datetime.datetime.utcfromtimestamp(MAX_TIME) + extraTime
return rval.replace(microsecond=micros)
def constructTimeRange(*args):
"""
Builds a python dynamicserialize TimeRange object from the given
arguments.
Args:
args*: must be a TimeRange or a pair of objects that can be
converted to a datetime via convertToDateTime().
Returns:
A TimeRange.
"""
if len(args) == 1 and isinstance(args[0], TimeRange):
return args[0]
if len(args) != 2:
raise TypeError("constructTimeRange takes exactly 2 arguments, " + str(len(args)) + " provided.")
startTime = convertToDateTime(args[0])
endTime = convertToDateTime(args[1])
return TimeRange(startTime, endTime)

View file

@ -0,0 +1,283 @@
# #
# This software was developed and / or modified by Raytheon Company,
# pursuant to Contract DG133W-05-CQ-1067 with the US Government.
#
# U.S. EXPORT CONTROLLED TECHNICAL DATA
# This software product contains export-restricted data whose
# export/transfer/disclosure is restricted by U.S. law. Dissemination
# to non-U.S. persons whether in the United States or abroad requires
# an export license or other authorization.
#
# Contractor Name: Raytheon Company
# Contractor Address: 6825 Pine Street, Suite 340
# Mail Stop B8
# Omaha, NE 68106
# 402.291.0100
#
# See the AWIPS II Master Rights File ("Master Rights File.pdf") for
# further licensing information.
# #
#
# Classes for retrieving soundings based on gridded data from the Data Access
# Framework
#
#
#
# SOFTWARE HISTORY
#
# Date Ticket# Engineer Description
# ------------ ---------- ----------- --------------------------
# 06/24/15 #4480 dgilling Initial Creation.
#
from collections import defaultdict
from shapely.geometry import Point
from ufpy import DateTimeConverter
from ufpy.dataaccess import DataAccessLayer
from dynamicserialize.dstypes.com.raytheon.uf.common.time import DataTime
from dynamicserialize.dstypes.com.raytheon.uf.common.dataplugin.level import Level
def getSounding(modelName, weatherElements, levels, samplePoint, refTime=None, timeRange=None):
""""
Performs a series of Data Access Framework requests to retrieve a sounding object
based on the specified request parameters.
Args:
modelName: the grid model datasetid to use as the basis of the sounding.
weatherElements: a list of parameters to return in the sounding.
levels: a list of levels to sample the given weather elements at
samplePoint: a lat/lon pair to perform the sampling of data at.
refTime: (optional) the grid model reference time to use for the sounding.
If not specified, the latest ref time in the system will be used.
timeRange: (optional) a TimeRange to specify which forecast hours to use.
If not specified, will default to all forecast hours.
Returns:
A _SoundingCube instance, which acts a 3-tiered dictionary, keyed
by DataTime, then by level and finally by weather element. If no
data is available for the given request parameters, None is returned.
"""
(locationNames, parameters, levels, envelope, refTime, timeRange) = \
__sanitizeInputs(modelName, weatherElements, levels, samplePoint, refTime, timeRange)
requestArgs = { 'datatype' : 'grid',
'locationNames' : locationNames,
'parameters' : parameters,
'levels' : levels,
'envelope' : envelope,
}
req = DataAccessLayer.newDataRequest(**requestArgs)
forecastHours = __determineForecastHours(req, refTime, timeRange)
if not forecastHours:
return None
response = DataAccessLayer.getGeometryData(req, forecastHours)
soundingObject = _SoundingCube(response)
return soundingObject
def setEDEXHost(host):
"""
Changes the EDEX host the Data Access Framework is communicating with.
Args:
host: the EDEX host to connect to
"""
if host:
DataAccessLayer.changeEDEXHost(str(host))
def __sanitizeInputs(modelName, weatherElements, levels, samplePoint, refTime, timeRange):
locationNames = [str(modelName)]
parameters = __buildStringList(weatherElements)
levels = __buildStringList(levels)
envelope = Point(samplePoint)
if refTime is not None:
refTime = DataTime(refTime=DateTimeConverter.convertToDateTime(refTime))
if timeRange is not None:
timeRange = DateTimeConverter.constructTimeRange(*timeRange)
return (locationNames, parameters, levels, envelope, refTime, timeRange)
def __determineForecastHours(request, refTime, timeRange):
dataTimes = DataAccessLayer.getAvailableTimes(request, False)
timesGen = [(DataTime(refTime=dataTime.getRefTime()), dataTime) for dataTime in dataTimes]
dataTimesMap = defaultdict(list)
for baseTime, dataTime in timesGen:
dataTimesMap[baseTime].append(dataTime)
if refTime is None:
refTime = max(dataTimesMap.keys())
forecastHours = dataTimesMap[refTime]
if timeRange is None:
return forecastHours
else:
return [forecastHour for forecastHour in forecastHours if timeRange.contains(forecastHour.getValidPeriod())]
def __buildStringList(param):
if __notStringIter(param):
return [str(item) for item in param]
else:
return [str(param)]
def __notStringIter(iterable):
if not isinstance(iterable, basestring):
try:
iter(iterable)
return True
except TypeError:
return False
class _SoundingCube(object):
"""
The top-level sounding object returned when calling SoundingsSupport.getSounding.
This object acts as a 3-tiered dict which is keyed by time then level
then parameter name. Calling times() will return all valid keys into this
object.
"""
def __init__(self, geometryDataObjects):
self._dataDict = {}
self._sortedTimes = []
if geometryDataObjects:
for geometryData in geometryDataObjects:
dataTime = geometryData.getDataTime()
level = geometryData.getLevel()
for parameter in geometryData.getParameters():
self.__addItem(parameter, dataTime, level, geometryData.getNumber(parameter))
def __addItem(self, parameter, dataTime, level, value):
timeLayer = self._dataDict.get(dataTime, _SoundingTimeLayer(dataTime))
self._dataDict[dataTime] = timeLayer
timeLayer._addItem(parameter, level, value)
if dataTime not in self._sortedTimes:
self._sortedTimes.append(dataTime)
self._sortedTimes.sort()
def __getitem__(self, key):
return self._dataDict[key]
def __len__(self):
return len(self._dataDict)
def times(self):
"""
Returns the valid times for this sounding.
Returns:
A list containing the valid DataTimes for this sounding in order.
"""
return self._sortedTimes
class _SoundingTimeLayer(object):
"""
The second-level sounding object returned when calling SoundingsSupport.getSounding.
This object acts as a 2-tiered dict which is keyed by level then parameter
name. Calling levels() will return all valid keys into this
object. Calling time() will return the DataTime for this particular layer.
"""
def __init__(self, dataTime):
self._dataTime = dataTime
self._dataDict = {}
def _addItem(self, parameter, level, value):
asString = str(level)
levelLayer = self._dataDict.get(asString, _SoundingTimeAndLevelLayer(self._dataTime, asString))
levelLayer._addItem(parameter, value)
self._dataDict[asString] = levelLayer
def __getitem__(self, key):
asString = str(key)
if asString in self._dataDict:
return self._dataDict[asString]
else:
raise KeyError("Level " + str(key) + " is not a valid level for this sounding.")
def __len__(self):
return len(self._dataDict)
def time(self):
"""
Returns the DataTime for this sounding cube layer.
Returns:
The DataTime for this sounding layer.
"""
return self._dataTime
def levels(self):
"""
Returns the valid levels for this sounding.
Returns:
A list containing the valid levels for this sounding in order of
closest to surface to highest from surface.
"""
sortedLevels = [Level(level) for level in self._dataDict.keys()]
sortedLevels.sort()
return [str(level) for level in sortedLevels]
class _SoundingTimeAndLevelLayer(object):
"""
The bottom-level sounding object returned when calling SoundingsSupport.getSounding.
This object acts as a dict which is keyed by parameter name. Calling
parameters() will return all valid keys into this object. Calling time()
will return the DataTime for this particular layer. Calling level() will
return the level for this layer.
"""
def __init__(self, time, level):
self._time = time
self._level = level
self._parameters = {}
def _addItem(self, parameter, value):
self._parameters[parameter] = value
def __getitem__(self, key):
return self._parameters[key]
def __len__(self):
return len(self._parameters)
def level(self):
"""
Returns the level for this sounding cube layer.
Returns:
The level for this sounding layer.
"""
return self._level
def parameters(self):
"""
Returns the valid parameters for this sounding.
Returns:
A list containing the valid parameter names.
"""
return list(self._parameters.keys())
def time(self):
"""
Returns the DataTime for this sounding cube layer.
Returns:
The DataTime for this sounding layer.
"""
return self._time