| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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. |
| |
| ''' |
| Classes: |
| Dataset - Container for a dataset's attributes and data. |
| Bounds - Container for holding spatial and temporal bounds information |
| for operations on a Dataset. |
| ''' |
| |
| import numpy |
| import logging |
| import datetime as dt |
| |
| import ocw.utils as utils |
| |
| from mpl_toolkits.basemap import shiftgrid |
| |
| logger = logging.getLogger(__name__) |
| |
| class Dataset: |
| '''Container for a dataset's attributes and data.''' |
| |
| def __init__(self, lats, lons, times, values, variable=None, name=""): |
| '''Default Dataset constructor |
| |
| :param lats: One dimensional numpy array of unique latitude values. |
| :type lats: numpy array |
| :param lons: One dimensional numpy array of unique longitude values. |
| :type lons: numpy array |
| :param times: One dimensional numpy array of unique python datetime |
| objects. |
| :type times: numpy array |
| :param values: Three dimensional numpy array of parameter values with |
| shape [timesLength, latsLength, lonsLength]. |
| :type values: numpy array |
| :param variable: Name of the value variable. |
| :type variable: string |
| :param name: An optional string name for the Dataset. |
| :type variable: string |
| |
| :raises: ValueError |
| ''' |
| self._validate_inputs(lats, lons, times, values) |
| lats, lons, values = utils.normalize_lat_lon_values(lats, lons, values) |
| |
| self.lats = lats |
| self.lons = lons |
| self.times = times |
| self.values = values |
| self.variable = variable |
| self.name = name |
| |
| def spatial_boundaries(self): |
| '''Calculate the spatial boundaries. |
| |
| :returns: The Dataset's bounding latitude and longitude values as a |
| tuple in the form (min_lat, max_lat, min_lon, max_lon) |
| :rtype: (float, float, float, float) |
| |
| ''' |
| return (float(min(self.lats)), float(max(self.lats)), |
| float(min(self.lons)), float(max(self.lons))) |
| |
| |
| def time_range(self): |
| '''Calculate the temporal range |
| |
| :returns: The start and end date of the Dataset's temporal range as |
| a tuple in the form (start_time, end_time). |
| :rtype: (datetime, datetime) |
| ''' |
| sorted_time = numpy.sort(self.times) |
| start_time = sorted_time[0] |
| end_time = sorted_time[-1] |
| |
| return (start_time, end_time) |
| |
| |
| def spatial_resolution(self): |
| '''Calculate the latitudinal and longitudinal spatial resolution. |
| |
| .. warning:: This only works with properly gridded data. |
| |
| :returns: The Dataset's latitudinal and longitudinal spatial resolution |
| as a tuple of the form (lat_resolution, lon_resolution). |
| :rtype: (float, float) |
| ''' |
| sorted_lats = numpy.sort(list(set(self.lats))) |
| sorted_lons = numpy.sort(list(set(self.lons))) |
| lat_resolution = sorted_lats[1] - sorted_lats[0] |
| lon_resolution = sorted_lons[1] - sorted_lons[0] |
| |
| return (lat_resolution, lon_resolution) |
| |
| |
| def temporal_resolution(self): |
| '''Calculate the temporal resolution. |
| |
| :raises ValueError: If timedelta.days as calculated from the sorted \ |
| list of times is an unrecognized value a ValueError is raised. |
| |
| :returns: The temporal resolution. |
| :rtype: string |
| ''' |
| sorted_times = numpy.sort(self.times) |
| time_resolution = sorted_times[1] - sorted_times[0] |
| num_days = time_resolution.days |
| |
| if num_days == 0: |
| num_hours = time_resolution.seconds / 3600 |
| time_resolution = 'hourly' if num_hours >= 1 else 'minutely' |
| elif num_days == 1: |
| time_resolution = 'daily' |
| elif num_days <= 31: |
| time_resolution = 'monthly' |
| elif num_days > 31: |
| time_resolution = 'yearly' |
| else: |
| error = ( |
| "Unable to calculate the temporal resolution for the " |
| "dataset. The number of days between two time values is " |
| "an unexpected value: " + str(num_days) |
| ) |
| |
| logger.error(error) |
| raise ValueError(error) |
| |
| return time_resolution |
| |
| def _validate_inputs(self, lats, lons, times, values): |
| """Check that Dataset inputs are valid. |
| |
| :raises: ValueError |
| """ |
| err_msg = None |
| #Setup and Check parameter dimensionality is correct |
| lat_dim = len(lats.shape) |
| lon_dim = len(lons.shape) |
| time_dim = len(times.shape) |
| value_dim = len(values.shape) |
| lat_count = lats.shape[0] |
| lon_count = lons.shape[0] |
| time_count = times.shape[0] |
| |
| if lat_dim != 1: |
| err_msg = "Latitude Array should be 1 dimensional. %s dimensions found." % lat_dim |
| elif lon_dim != 1: |
| err_msg = "Longitude Array should be 1 dimensional. %s dimensions found." % lon_dim |
| elif time_dim != 1: |
| err_msg = "Time Array should be 1 dimensional. %s dimensions found." % time_dim |
| elif value_dim != 3: |
| err_msg = "Value Array should be 3 dimensional. %s dimensions found." % value_dim |
| # Finally check that the Values array conforms to the proper shape |
| elif values.shape != (time_count, lat_count, lon_count): |
| err_msg = """Value Array must be of shape (times, lats, lons). |
| Expected shape (%s, %s, %s) but received (%s, %s, %s)""" % (time_count, |
| lat_count, |
| lon_count, |
| values.shape[0], |
| values.shape[1], |
| values.shape[2]) |
| if err_msg: |
| logger.error(err_msg) |
| raise ValueError(err_msg) |
| |
| |
| class Bounds(object): |
| '''Container for holding spatial and temporal bounds information. |
| |
| Certain operations require valid bounding information to be present for |
| correct functioning. Bounds guarantees that a function receives well |
| formed information without the need to do the validation manually. |
| |
| Spatial and temporal bounds must follow the following guidelines. |
| |
| * Latitude values must be in the range [-90, 90] |
| * Longitude values must be in the range [-180, 180] |
| * Lat/Lon Min values must be less than the corresponding Lat/Lon Max values. |
| * Temporal bounds must a valid datetime object |
| ''' |
| |
| def __init__(self, lat_min, lat_max, lon_min, lon_max, start, end): |
| '''Default Bounds constructor |
| |
| :param lat_min: The minimum latitude bound. |
| :type lat_min: float |
| |
| :param lat_max: The maximum latitude bound. |
| :type lat_max: float |
| |
| :param lon_min: The minimum longitude bound. |
| :type lon_min: float |
| |
| :param lon_max: The maximum longitude bound. |
| :type lon_max: float |
| |
| :param start: The starting datetime bound. |
| :type start: datetime |
| |
| :param end: The ending datetime bound. |
| :type end: datetime |
| |
| :raises: ValueError |
| ''' |
| self._lat_min = float(lat_min) |
| self._lat_max = float(lat_max) |
| self._lon_min = float(lon_min) |
| self._lon_max = float(lon_max) |
| self._start = start |
| self._end = end |
| |
| @property |
| def lat_min(self): |
| return self._lat_min |
| |
| @lat_min.setter |
| def lat_min(self, value): |
| if not (-90 <= value <= 90 and value < self._lat_max): |
| error = "Attempted to set lat_min to invalid value: %s" % (value) |
| logger.error(error) |
| raise ValueError(error) |
| |
| self._lat_min = value |
| |
| @property |
| def lat_max(self): |
| return self._lat_max |
| |
| @lat_max.setter |
| def lat_max(self, value): |
| if not (-90 <= value <= 90 and value > self._lat_min): |
| error = "Attempted to set lat_max to invalid value: %s" % (value) |
| logger.error(error) |
| raise ValueError(error) |
| |
| self._lat_max = value |
| |
| @property |
| def lon_min(self): |
| return self._lon_min |
| |
| @lon_min.setter |
| def lon_min(self, value): |
| if not (-180 <= value <= 180 and value < self._lon_max): |
| error = "Attempted to set lon_min to invalid value: %s" % (value) |
| logger.error(error) |
| raise ValueError(error) |
| |
| self._lon_min = value |
| |
| @property |
| def lon_max(self): |
| return self._lon_max |
| |
| @lon_max.setter |
| def lon_max(self, value): |
| if not (-180 <= value <= 180 and value > self._lon_min): |
| error = "Attempter to set lon_max to invalid value: %s" % (value) |
| logger.error(error) |
| raise ValueError(error) |
| |
| self._lon_max = value |
| |
| @property |
| def start(self): |
| return self._start |
| |
| @start.setter |
| def start(self, value): |
| if not (type(value) is dt.datetime and value < self._end): |
| error = "Attempted to set start to invalid value: %s" % (value) |
| logger.error(error) |
| raise ValueError(error) |
| |
| self._start = value |
| |
| @property |
| def end(self): |
| return self._end |
| |
| @end.setter |
| def end(self, value): |
| if not (type(value) is dt.datetime and value > self._start): |
| error = "Attempted to set end to invalid value: %s" % (value) |
| logger.error(error) |
| raise ValueError(error) |
| |
| self._end = value |