diff --git a/.travis.yml b/.travis.yml index 85a7cacca4..13f0622c3b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,7 @@ install: conda install --quiet --file minimal-conda-requirements.txt; else if [[ "$TRAVIS_PYTHON_VERSION" == 3* ]]; then - sed -e '/ecmwf_grib/d' -e '/esmpy/d' -e '/iris-grib/d' -e 's/#.\+$//' conda-requirements.txt | xargs conda install --quiet; + sed -e '/esmpy/d' -e 's/#.\+$//' conda-requirements.txt | xargs conda install --quiet; else conda install --quiet --file conda-requirements.txt; fi diff --git a/conda-requirements.txt b/conda-requirements.txt index 30869af920..aa80d181bf 100644 --- a/conda-requirements.txt +++ b/conda-requirements.txt @@ -26,7 +26,7 @@ requests # Optional iris dependencies nc_time_axis -iris-grib +python-eccodes esmpy>=7.0 gdal libmo_unpack diff --git a/lib/iris/fileformats/__init__.py b/lib/iris/fileformats/__init__.py index ce15901285..4eba3548c3 100644 --- a/lib/iris/fileformats/__init__.py +++ b/lib/iris/fileformats/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -27,13 +27,11 @@ UriProtocol, LeadingLine) from . import abf from . import um + try: - import iris_grib as igrib + from . import grib as igrib except ImportError: - try: - from . import grib as igrib - except ImportError: - igrib = None + igrib = None from . import name from . import netcdf diff --git a/lib/iris/fileformats/grib/__init__.py b/lib/iris/fileformats/grib/__init__.py index f31e20dd1f..c52fdc61c1 100644 --- a/lib/iris/fileformats/grib/__init__.py +++ b/lib/iris/fileformats/grib/__init__.py @@ -17,13 +17,7 @@ """ Conversion of cubes to/from GRIB. -See also: `ECMWF GRIB API `_. - -.. deprecated:: 1.10 - - This module is now deprecated. For GRIB file support in iris, please use - the separate package - `iris_grib `_ instead. +See: `ECMWF GRIB API `_. """ @@ -32,7 +26,7 @@ import six import datetime -import math #for fmod +import math # for fmod import warnings import biggus @@ -41,56 +35,40 @@ import gribapi import numpy as np import numpy.ma as ma -import scipy.interpolate -from iris._deprecation import warn_deprecated -from iris.analysis._interpolate_private import Linear1dExtrapolator +import iris import iris.coord_systems as coord_systems -from iris.exceptions import TranslationError -# NOTE: careful here, to avoid circular imports (as iris imports grib) +from iris.exceptions import TranslationError, NotYetImplementedError, IrisError from iris.fileformats.grib import grib_phenom_translation as gptx from iris.fileformats.grib import _save_rules -import iris.fileformats.grib._load_convert +from iris.fileformats.grib._load_convert import convert as load_convert from iris.fileformats.grib.message import GribMessage -import iris.fileformats.grib.load_rules - -# Issue a blanket deprecation for this module. -warn_deprecated( - "The module iris.fileformats.grib is deprecated since v1.10. " - "Please install the package 'iris_grib' package instead.") __all__ = ['load_cubes', 'save_grib2', 'load_pairs_from_fields', - 'save_pairs_from_cube', 'save_messages', 'GribWrapper', - 'as_messages', 'as_pairs', 'grib_generator', 'reset_load_rules', - 'hindcast_workaround'] - - -#: Set this flag to True to enable support of negative forecast periods -#: when loading and saving GRIB files. -#: -#: .. deprecated:: 1.10 -hindcast_workaround = False + 'save_pairs_from_cube', 'save_messages'] CENTRE_TITLES = {'egrr': 'U.K. Met Office - Exeter', 'ecmf': 'European Centre for Medium Range Weather Forecasts', 'rjtd': 'Tokyo, Japan Meteorological Agency', - '55' : 'San Francisco', - 'kwbc': 'US National Weather Service, National Centres for Environmental Prediction'} - -TIME_RANGE_INDICATORS = {0:'none', 1:'none', 3:'time mean', 4:'time sum', - 5:'time _difference', 10:'none', - # TODO #567 Further exploration of the following mappings - 51:'time mean', 113:'time mean', 114:'time sum', - 115:'time mean', 116:'time sum', 117:'time mean', - 118:'time _covariance', 123:'time mean', - 124:'time sum', 125:'time standard_deviation'} - -PROCESSING_TYPES = {0:'time mean', 1:'time sum', 2:'time maximum', 3:'time minimum', - 4:'time _difference', 5:'time _root mean square', - 6:'time standard_deviation', 7:'time _convariance', - 8:'time _difference', 9:'time _ratio'} + '55': 'San Francisco', + 'kwbc': ('US National Weather Service, National Centres for ' + 'Environmental Prediction')} + +TIME_RANGE_INDICATORS = {0: 'none', 1: 'none', 3: 'time mean', 4: 'time sum', + 5: 'time _difference', 10: 'none', + # TODO #567 Further exploration of following mappings + 51: 'time mean', 113: 'time mean', 114: 'time sum', + 115: 'time mean', 116: 'time sum', 117: 'time mean', + 118: 'time _covariance', 123: 'time mean', + 124: 'time sum', 125: 'time standard_deviation'} + +PROCESSING_TYPES = {0: 'time mean', 1: 'time sum', 2: 'time maximum', + 3: 'time minimum', 4: 'time _difference', + 5: 'time _root mean square', 6: 'time standard_deviation', + 7: 'time _convariance', 8: 'time _difference', + 9: 'time _ratio'} TIME_CODES_EDITION1 = { 0: ('minutes', 60), @@ -111,48 +89,20 @@ 254: ('seconds', 1), } -TIME_CODES_EDITION2 = { - 0: ('minutes', 60), - 1: ('hours', 60*60), - 2: ('days', 24*60*60), - # NOTE: do *not* support calendar-dependent units at all. - # So the following possible keys remain unsupported: - # 3: 'months', - # 4: 'years', - # 5: 'decades', - # 6: '30 years', - # 7: 'century', - 10: ('3 hours', 3*60*60), - 11: ('6 hours', 6*60*60), - 12: ('12 hours', 12*60*60), - 13: ('seconds', 1), -} - unknown_string = "???" -def reset_load_rules(): - """ - Resets the GRIB load process to use only the standard conversion rules. - - .. deprecated:: 1.7 - - """ - warn_deprecated('reset_load_rules was deprecated in v1.7.') - - class GribDataProxy(object): """A reference to the data payload of a single Grib message.""" - __slots__ = ('shape', 'dtype', 'fill_value', 'path', 'offset', 'regularise') + __slots__ = ('shape', 'dtype', 'fill_value', 'path', 'offset') - def __init__(self, shape, dtype, fill_value, path, offset, regularise): + def __init__(self, shape, dtype, fill_value, path, offset): self.shape = shape self.dtype = dtype self.fill_value = fill_value self.path = path self.offset = offset - self.regularise = regularise @property def ndim(self): @@ -162,10 +112,6 @@ def __getitem__(self, keys): with open(self.path, 'rb') as grib_fh: grib_fh.seek(self.offset) grib_message = gribapi.grib_new_from_file(grib_fh) - - if self.regularise and _is_quasi_regular_grib(grib_message): - _regularise(grib_message) - data = _message_values(grib_message, self.shape) gribapi.grib_release(grib_message) @@ -173,13 +119,12 @@ def __getitem__(self, keys): def __repr__(self): msg = '<{self.__class__.__name__} shape={self.shape} ' \ - 'dtype={self.dtype!r} fill_value={self.fill_value!r} ' \ - 'path={self.path!r} offset={self.offset} ' \ - 'regularise={self.regularise}>' + 'dtype={self.dtype!r} fill_value={self.fill_value!r} ' \ + 'path={self.path!r} offset={self.offset}>' return msg.format(self=self) def __getstate__(self): - return {attr:getattr(self, attr) for attr in self.__slots__} + return {attr: getattr(self, attr) for attr in self.__slots__} def __setstate__(self, state): for key, value in six.iteritems(state): @@ -190,33 +135,29 @@ class GribWrapper(object): """ Contains a pygrib object plus some extra keys of our own. - .. deprecated:: 1.10 - The class :class:`iris.fileformats.grib.message.GribMessage` provides alternative means of working with GRIB message instances. """ - def __init__(self, grib_message, grib_fh=None, auto_regularise=True): - warn_deprecated('Deprecated at version 1.10') + def __init__(self, grib_message, grib_fh=None): """Store the grib message and compute our extra keys.""" self.grib_message = grib_message + + if self.edition != 1: + emsg = 'GRIB edition {} is not supported by {!r}.' + raise TranslationError(emsg.format(self.edition, + type(self).__name__)) + deferred = grib_fh is not None # Store the file pointer and message length from the current # grib message before it's changed by calls to the grib-api. if deferred: - # Note that, the grib-api has already read this message and + # Note that, the grib-api has already read this message and # advanced the file pointer to the end of the message. offset = grib_fh.tell() message_length = gribapi.grib_get_long(grib_message, 'totalLength') - if auto_regularise and _is_quasi_regular_grib(grib_message): - warnings.warn('Regularising GRIB message.') - if deferred: - self._regularise_shape(grib_message) - else: - _regularise(grib_message) - # Initialise the key-extension dictionary. # NOTE: this attribute *must* exist, or the the __getattr__ overload # can hit an infinite loop. @@ -237,102 +178,72 @@ def __init__(self, grib_message, grib_fh=None, auto_regularise=True): # Wrap the reference to the data payload within the data proxy # in order to support deferred data loading. # The byte offset requires to be reset back to the first byte - # of this message. The file pointer offset is always at the end + # of this message. The file pointer offset is always at the end # of the current message due to the grib-api reading the message. proxy = GribDataProxy(shape, np.zeros(0).dtype, np.nan, grib_fh.name, - offset - message_length, - auto_regularise) + offset - message_length) self._data = biggus.NumpyArrayAdapter(proxy) else: self.data = _message_values(grib_message, shape) - @staticmethod - def _regularise_shape(grib_message): - """ - Calculate the regularised shape of the reduced message and push - dummy regularised values into the message to force the gribapi - to update the message grid type from reduced to regular. - - """ - # Make sure to read any missing values as NaN. - gribapi.grib_set_double(grib_message, "missingValue", np.nan) - - # Get full longitude values, these describe the longitude value of - # *every* point in the grid, they are not 1d monotonic coordinates. - lons = gribapi.grib_get_double_array(grib_message, "longitudes") - - # Compute the new longitude coordinate for the regular grid. - new_nx = max(gribapi.grib_get_long_array(grib_message, "pl")) - new_x_step = (max(lons) - min(lons)) / (new_nx - 1) - if gribapi.grib_get_long(grib_message, "iScansNegatively"): - new_x_step *= -1 - - gribapi.grib_set_long(grib_message, "Nx", int(new_nx)) - gribapi.grib_set_double(grib_message, "iDirectionIncrementInDegrees", - float(new_x_step)) - # Spoof gribapi with false regularised values. - nj = gribapi.grib_get_long(grib_message, 'Nj') - temp = np.zeros((nj * new_nx,), dtype=np.float) - gribapi.grib_set_double_array(grib_message, 'values', temp) - gribapi.grib_set_long(grib_message, "jPointsAreConsecutive", 0) - gribapi.grib_set_long(grib_message, "PLPresent", 0) - def _confirm_in_scope(self): """Ensure we have a grib flavour that we choose to support.""" - #forbid alternate row scanning - #(uncommon entry from GRIB2 flag table 3.4, also in GRIB1) + # forbid alternate row scanning + # (uncommon entry from GRIB2 flag table 3.4, also in GRIB1) if self.alternativeRowScanning == 1: - raise iris.exceptions.IrisError("alternativeRowScanning == 1 not handled.") + raise IrisError("alternativeRowScanning == 1 not handled.") def __getattr__(self, key): """Return a grib key, or one of our extra keys.""" # is it in the grib message? try: - # we just get as the type of the "values" array...special case here... + # we just get as the type of the "values" + # array...special case here... if key in ["values", "pv", "latitudes", "longitudes"]: res = gribapi.grib_get_double_array(self.grib_message, key) - elif key in ('typeOfFirstFixedSurface', 'typeOfSecondFixedSurface'): + elif key in ('typeOfFirstFixedSurface', + 'typeOfSecondFixedSurface'): res = np.int32(gribapi.grib_get_long(self.grib_message, key)) else: key_type = gribapi.grib_get_native_type(self.grib_message, key) if key_type == int: - res = np.int32(gribapi.grib_get_long(self.grib_message, key)) + res = np.int32(gribapi.grib_get_long(self.grib_message, + key)) elif key_type == float: # Because some computer keys are floats, like - # longitudeOfFirstGridPointInDegrees, a float32 is not always enough... - res = np.float64(gribapi.grib_get_double(self.grib_message, key)) + # longitudeOfFirstGridPointInDegrees, a float32 + # is not always enough... + res = np.float64(gribapi.grib_get_double(self.grib_message, + key)) elif key_type == str: res = gribapi.grib_get_string(self.grib_message, key) else: - raise ValueError("Unknown type for %s : %s" % (key, str(key_type))) + emsg = "Unknown type for {} : {}" + raise ValueError(emsg.format(key, str(key_type))) except gribapi.GribInternalError: res = None - #...or is it in our list of extras? + # ...or is it in our list of extras? if res is None: if key in self.extra_keys: res = self.extra_keys[key] else: - #must raise an exception for the hasattr() mechanism to work + # must raise an exception for the hasattr() mechanism to work raise AttributeError("Cannot find GRIB key %s" % key) return res def _timeunit_detail(self): """Return the (string, seconds) describing the message time unit.""" - if self.edition == 1: - code_to_detail = TIME_CODES_EDITION1 - else: - code_to_detail = TIME_CODES_EDITION2 unit_code = self.indicatorOfUnitOfTimeRange - if unit_code not in code_to_detail: + if unit_code not in TIME_CODES_EDITION1: message = 'Unhandled time unit for forecast ' \ 'indicatorOfUnitOfTimeRange : ' + str(unit_code) - raise iris.exceptions.NotYetImplementedError(message) - return code_to_detail[unit_code] + raise NotYetImplementedError(message) + return TIME_CODES_EDITION1[unit_code] def _timeunit_string(self): """Get the udunits string for the message time unit.""" @@ -347,79 +258,56 @@ def _compute_extra_keys(self): global unknown_string self.extra_keys = {} + forecastTime = self.startStep - # work out stuff based on these values from the message - edition = self.edition - - # time-processed forcast time is from reference time to start of period - if edition == 2: - forecastTime = self.forecastTime - - uft = np.uint32(forecastTime) - BILL = 2**30 - - # Workaround grib api's assumption that forecast time is positive. - # Handles correctly encoded -ve forecast times up to one -1 billion. - if hindcast_workaround: - if 2 * BILL < uft < 3 * BILL: - msg = "Re-interpreting negative forecastTime from " \ - + str(forecastTime) - forecastTime = -(uft - 2 * BILL) - msg += " to " + str(forecastTime) - warnings.warn(msg) - - else: - forecastTime = self.startStep - - #regular or rotated grid? + # regular or rotated grid? try: - longitudeOfSouthernPoleInDegrees = self.longitudeOfSouthernPoleInDegrees - latitudeOfSouthernPoleInDegrees = self.latitudeOfSouthernPoleInDegrees + longitudeOfSouthernPoleInDegrees = \ + self.longitudeOfSouthernPoleInDegrees + latitudeOfSouthernPoleInDegrees = \ + self.latitudeOfSouthernPoleInDegrees except AttributeError: longitudeOfSouthernPoleInDegrees = 0.0 latitudeOfSouthernPoleInDegrees = 90.0 centre = gribapi.grib_get_string(self.grib_message, "centre") - - #default values - self.extra_keys = {'_referenceDateTime':-1.0, '_phenomenonDateTime':-1.0, - '_periodStartDateTime':-1.0, '_periodEndDateTime':-1.0, - '_levelTypeName':unknown_string, - '_levelTypeUnits':unknown_string, '_firstLevelTypeName':unknown_string, - '_firstLevelTypeUnits':unknown_string, '_firstLevel':-1.0, - '_secondLevelTypeName':unknown_string, '_secondLevel':-1.0, - '_originatingCentre':unknown_string, - '_forecastTime':None, '_forecastTimeUnit':unknown_string, - '_coord_system':None, '_x_circular':False, - '_x_coord_name':unknown_string, '_y_coord_name':unknown_string, - # These are here to avoid repetition in the rules files, - # and reduce the very long line lengths. - '_x_points':None, '_y_points':None, - '_cf_data':None} + # default values + self.extra_keys = {'_referenceDateTime': -1.0, + '_phenomenonDateTime': -1.0, + '_periodStartDateTime': -1.0, + '_periodEndDateTime': -1.0, + '_levelTypeName': unknown_string, + '_levelTypeUnits': unknown_string, + '_firstLevelTypeName': unknown_string, + '_firstLevelTypeUnits': unknown_string, + '_firstLevel': -1.0, + '_secondLevelTypeName': unknown_string, + '_secondLevel': -1.0, + '_originatingCentre': unknown_string, + '_forecastTime': None, + '_forecastTimeUnit': unknown_string, + '_coord_system': None, + '_x_circular': False, + '_x_coord_name': unknown_string, + '_y_coord_name': unknown_string, + # These are here to avoid repetition in the rules + # files, and reduce the very long line lengths. + '_x_points': None, + '_y_points': None, + '_cf_data': None} # cf phenomenon translation - if edition == 1: - # Get centre code (N.B. self.centre has default type = string) - centre_number = gribapi.grib_get_long(self.grib_message, "centre") - # Look for a known grib1-to-cf translation (or None). - cf_data = gptx.grib1_phenom_to_cf_info( - table2_version=self.table2Version, - centre_number=centre_number, - param_number=self.indicatorOfParameter) - self.extra_keys['_cf_data'] = cf_data - elif edition == 2: - # Don't attempt to interpret params if 'master tables version' is - # 255, as local params may then have same codes as standard ones. - if self.tablesVersion != 255: - # Look for a known grib2-to-cf translation (or None). - cf_data = gptx.grib2_phenom_to_cf_info( - param_discipline=self.discipline, - param_category=self.parameterCategory, - param_number=self.parameterNumber) - self.extra_keys['_cf_data'] = cf_data - - #reference date + # Get centre code (N.B. self.centre has default type = string) + centre_number = gribapi.grib_get_long(self.grib_message, "centre") + # Look for a known grib1-to-cf translation (or None). + cf_data = gptx.grib1_phenom_to_cf_info( + table2_version=self.table2Version, + centre_number=centre_number, + param_number=self.indicatorOfParameter) + self.extra_keys['_cf_data'] = cf_data + + # reference date self.extra_keys['_referenceDateTime'] = \ datetime.datetime(int(self.year), int(self.month), int(self.day), int(self.hour), int(self.minute)) @@ -427,62 +315,55 @@ def _compute_extra_keys(self): # forecast time with workarounds self.extra_keys['_forecastTime'] = forecastTime - #verification date + # verification date processingDone = self._get_processing_done() - #time processed? + # time processed? if processingDone.startswith("time"): - if self.edition == 1: - validityDate = str(self.validityDate) - validityTime = "{:04}".format(int(self.validityTime)) - endYear = int(validityDate[:4]) - endMonth = int(validityDate[4:6]) - endDay = int(validityDate[6:8]) - endHour = int(validityTime[:2]) - endMinute = int(validityTime[2:4]) - elif self.edition == 2: - endYear = self.yearOfEndOfOverallTimeInterval - endMonth = self.monthOfEndOfOverallTimeInterval - endDay = self.dayOfEndOfOverallTimeInterval - endHour = self.hourOfEndOfOverallTimeInterval - endMinute = self.minuteOfEndOfOverallTimeInterval + validityDate = str(self.validityDate) + validityTime = "{:04}".format(int(self.validityTime)) + endYear = int(validityDate[:4]) + endMonth = int(validityDate[4:6]) + endDay = int(validityDate[6:8]) + endHour = int(validityTime[:2]) + endMinute = int(validityTime[2:4]) # fixed forecastTime in hours self.extra_keys['_periodStartDateTime'] = \ (self.extra_keys['_referenceDateTime'] + datetime.timedelta(hours=int(forecastTime))) self.extra_keys['_periodEndDateTime'] = \ - datetime.datetime(endYear, endMonth, endDay, endHour, endMinute) + datetime.datetime(endYear, endMonth, endDay, endHour, + endMinute) else: - self.extra_keys['_phenomenonDateTime'] = self._get_verification_date() + self.extra_keys['_phenomenonDateTime'] = \ + self._get_verification_date() - - #originating centre - #TODO #574 Expand to include sub-centre + # originating centre + # TODO #574 Expand to include sub-centre self.extra_keys['_originatingCentre'] = CENTRE_TITLES.get( - centre, "unknown centre %s" % centre) + centre, "unknown centre %s" % centre) - #forecast time unit as a cm string - #TODO #575 Do we want PP or GRIB style forecast delta? + # forecast time unit as a cm string + # TODO #575 Do we want PP or GRIB style forecast delta? self.extra_keys['_forecastTimeUnit'] = self._timeunit_string() + # shape of the earth - #shape of the earth - - #pre-defined sphere + # pre-defined sphere if self.shapeOfTheEarth == 0: geoid = coord_systems.GeogCS(semi_major_axis=6367470) - #custom sphere + # custom sphere elif self.shapeOfTheEarth == 1: geoid = coord_systems.GeogCS( self.scaledValueOfRadiusOfSphericalEarth * 10 ** -self.scaleFactorOfRadiusOfSphericalEarth) - #IAU65 oblate sphere + # IAU65 oblate sphere elif self.shapeOfTheEarth == 2: geoid = coord_systems.GeogCS(6378160, inverse_flattening=297.0) - #custom oblate spheroid (km) + # custom oblate spheroid (km) elif self.shapeOfTheEarth == 3: geoid = coord_systems.GeogCS( semi_major_axis=self.scaledValueOfEarthMajorAxis * @@ -490,20 +371,20 @@ def _compute_extra_keys(self): semi_minor_axis=self.scaledValueOfEarthMinorAxis * 10 ** -self.scaleFactorOfEarthMinorAxis * 1000.) - #IAG-GRS80 oblate spheroid + # IAG-GRS80 oblate spheroid elif self.shapeOfTheEarth == 4: geoid = coord_systems.GeogCS(6378137, None, 298.257222101) - #WGS84 + # WGS84 elif self.shapeOfTheEarth == 5: geoid = \ coord_systems.GeogCS(6378137, inverse_flattening=298.257223563) - #pre-defined sphere + # pre-defined sphere elif self.shapeOfTheEarth == 6: geoid = coord_systems.GeogCS(6371229) - #custom oblate spheroid (m) + # custom oblate spheroid (m) elif self.shapeOfTheEarth == 7: geoid = coord_systems.GeogCS( semi_major_axis=self.scaledValueOfEarthMajorAxis * @@ -519,7 +400,8 @@ def _compute_extra_keys(self): gridType = gribapi.grib_get_string(self.grib_message, "gridType") - if gridType in ["regular_ll", "regular_gg", "reduced_ll", "reduced_gg"]: + if gridType in ["regular_ll", "regular_gg", "reduced_ll", + "reduced_gg"]: self.extra_keys['_x_coord_name'] = "longitude" self.extra_keys['_y_coord_name'] = "latitude" self.extra_keys['_coord_system'] = geoid @@ -531,10 +413,10 @@ def _compute_extra_keys(self): southPoleLon = longitudeOfSouthernPoleInDegrees southPoleLat = latitudeOfSouthernPoleInDegrees self.extra_keys['_coord_system'] = \ - iris.coord_systems.RotatedGeogCS( - -southPoleLat, - math.fmod(southPoleLon + 180.0, 360.0), - self.angleOfRotation, geoid) + coord_systems.RotatedGeogCS( + -southPoleLat, + math.fmod(southPoleLon + 180.0, 360.0), + self.angleOfRotation, geoid) elif gridType == 'polar_stereographic': self.extra_keys['_x_coord_name'] = "projection_x_coordinate" self.extra_keys['_y_coord_name'] = "projection_y_coordinate" @@ -548,7 +430,7 @@ def _compute_extra_keys(self): # Note: I think the grib api defaults LaDInDegrees to 60 for grib1. self.extra_keys['_coord_system'] = \ - iris.coord_systems.Stereographic( + coord_systems.Stereographic( pole_lat, self.orientationOfTheGridInDegrees, 0, 0, self.LaDInDegrees, ellipsoid=geoid) @@ -556,10 +438,7 @@ def _compute_extra_keys(self): self.extra_keys['_x_coord_name'] = "projection_x_coordinate" self.extra_keys['_y_coord_name'] = "projection_y_coordinate" - if self.edition == 1: - flag_name = "projectionCenterFlag" - else: - flag_name = "projectionCentreFlag" + flag_name = "projectionCenterFlag" if getattr(self, flag_name) == 0: pole_lat = 90 @@ -568,7 +447,7 @@ def _compute_extra_keys(self): else: raise TranslationError("Unhandled projectionCentreFlag") - LambertConformal = iris.coord_systems.LambertConformal + LambertConformal = coord_systems.LambertConformal self.extra_keys['_coord_system'] = LambertConformal( self.LaDInDegrees, self.LoVInDegrees, 0, 0, secant_latitudes=(self.Latin1InDegrees, self.Latin2InDegrees), @@ -603,9 +482,9 @@ def _compute_extra_keys(self): # convert the starting latlon into meters cartopy_crs = self.extra_keys['_coord_system'].as_cartopy_crs() x1, y1 = cartopy_crs.transform_point( - self.longitudeOfFirstGridPointInDegrees, - self.latitudeOfFirstGridPointInDegrees, - ccrs.Geodetic()) + self.longitudeOfFirstGridPointInDegrees, + self.latitudeOfFirstGridPointInDegrees, + ccrs.Geodetic()) if not np.all(np.isfinite([x1, y1])): raise TranslationError("Could not determine the first latitude" @@ -638,67 +517,114 @@ def _get_processing_done(self): """Determine the type of processing that was done on the data.""" processingDone = 'unknown' - edition = self.edition - - #grib1 - if edition == 1: - timeRangeIndicator = self.timeRangeIndicator - processingDone = TIME_RANGE_INDICATORS.get(timeRangeIndicator, - 'time _grib1_process_unknown_%i' % timeRangeIndicator) - - #grib2 - else: - - pdt = self.productDefinitionTemplateNumber - - #pdt 4.0? (standard forecast) - if pdt == 0: - processingDone = 'none' - - #pdt 4.8 or 4.9? (time-processed) - elif pdt in (8, 9): - typeOfStatisticalProcessing = self.typeOfStatisticalProcessing - processingDone = PROCESSING_TYPES.get(typeOfStatisticalProcessing, - 'time _grib2_process_unknown_%i' % typeOfStatisticalProcessing) + timeRangeIndicator = self.timeRangeIndicator + default = 'time _grib1_process_unknown_%i' % timeRangeIndicator + processingDone = TIME_RANGE_INDICATORS.get(timeRangeIndicator, default) return processingDone def _get_verification_date(self): reference_date_time = self._referenceDateTime - # calculate start time (edition-dependent) - if self.edition == 1: - time_range_indicator = self.timeRangeIndicator - P1 = self.P1 - P2 = self.P2 - if time_range_indicator == 0: time_diff = P1 #Forecast product valid at reference time + P1 P1>0), or Uninitialized analysis product for reference time (P1=0). Or Image product for reference time (P1=0) - elif time_range_indicator == 1: time_diff = P1 #Initialized analysis product for reference time (P1=0). - elif time_range_indicator == 2: time_diff = (P1 + P2) * 0.5 #Product with a valid time ranging between reference time + P1 and reference time + P2 - elif time_range_indicator == 3: time_diff = (P1 + P2) * 0.5 #Average(reference time + P1 to reference time + P2) - elif time_range_indicator == 4: time_diff = P2 #Accumulation (reference time + P1 to reference time + P2) product considered valid at reference time + P2 - elif time_range_indicator == 5: time_diff = P2 #Difference(reference time + P2 minus reference time + P1) product considered valid at reference time + P2 - elif time_range_indicator == 10: time_diff = P1 * 256 + P2 #P1 occupies octets 19 and 20; product valid at reference time + P1 - elif time_range_indicator == 51: #Climatological Mean Value: multiple year averages of quantities which are themselves means over some period of time (P2) less than a year. The reference time (R) indicates the date and time of the start of a period of time, given by R to R + P2, over which a mean is formed; N indicates the number of such period-means that are averaged together to form the climatological value, assuming that the N period-mean fields are separated by one year. The reference time indicates the start of the N-year climatology. N is given in octets 22-23 of the PDS. If P1 = 0 then the data averaged in the basic interval P2 are assumed to be continuous, i.e., all available data are simply averaged together. If P1 = 1 (the units of time - octet 18, code table 4 - are not relevant here) then the data averaged together in the basic interval P2 are valid only at the time (hour, minute) given in the reference time, for all the days included in the P2 period. The units of P2 are given by the contents of octet 18 and Table 4. - raise TranslationError("unhandled grib1 timeRangeIndicator " - "= 51 (avg of avgs)") - elif time_range_indicator == 113: time_diff = P1 #Average of N forecasts (or initialized analyses); each product has forecast period of P1 (P1=0 for initialized analyses); products have reference times at intervals of P2, beginning at the given reference time. - elif time_range_indicator == 114: time_diff = P1 #Accumulation of N forecasts (or initialized analyses); each product has forecast period of P1 (P1=0 for initialized analyses); products have reference times at intervals of P2, beginning at the given reference time. - elif time_range_indicator == 115: time_diff = P1 #Average of N forecasts, all with the same reference time; the first has a forecast period of P1, the remaining forecasts follow at intervals of P2. - elif time_range_indicator == 116: time_diff = P1 #Accumulation of N forecasts, all with the same reference time; the first has a forecast period of P1, the remaining follow at intervals of P2. - elif time_range_indicator == 117: time_diff = P1 #Average of N forecasts, the first has a period of P1, the subsequent ones have forecast periods reduced from the previous one by an interval of P2; the reference time for the first is given in octets 13-17, the subsequent ones have reference times increased from the previous one by an interval of P2. Thus all the forecasts have the same valid time, given by the initial reference time + P1. - elif time_range_indicator == 118: time_diff = P1 #Temporal variance, or covariance, of N initialized analyses; each product has forecast period P1=0; products have reference times at intervals of P2, beginning at the given reference time. - elif time_range_indicator == 123: time_diff = P1 #Average of N uninitialized analyses, starting at the reference time, at intervals of P2. - elif time_range_indicator == 124: time_diff = P1 #Accumulation of N uninitialized analyses, starting at the reference time, at intervals of P2. - else: - raise TranslationError("unhandled grib1 timeRangeIndicator " - "= %i" % time_range_indicator) - elif self.edition == 2: - time_diff = int(self.stepRange) # gribapi gives us a string! - + # calculate start time + time_range_indicator = self.timeRangeIndicator + P1 = self.P1 + P2 = self.P2 + if time_range_indicator == 0: + # Forecast product valid at reference time + P1 P1>0), + # or Uninitialized analysis product for reference time (P1=0). + # Or Image product for reference time (P1=0) + time_diff = P1 + elif time_range_indicator == 1: + # Initialized analysis product for reference time (P1=0). + time_diff = P1 + elif time_range_indicator == 2: + # Product with a valid time ranging between reference time + P1 + # and reference time + P2 + time_diff = (P1 + P2) * 0.5 + elif time_range_indicator == 3: + # Average(reference time + P1 to reference time + P2) + time_diff = (P1 + P2) * 0.5 + elif time_range_indicator == 4: + # Accumulation (reference time + P1 to reference time + P2) + # product considered valid at reference time + P2 + time_diff = P2 + elif time_range_indicator == 5: + # Difference(reference time + P2 minus reference time + P1) + # product considered valid at reference time + P2 + time_diff = P2 + elif time_range_indicator == 10: + # P1 occupies octets 19 and 20; product valid at + # reference time + P1 + time_diff = P1 * 256 + P2 + elif time_range_indicator == 51: + # Climatological Mean Value: multiple year averages of + # quantities which are themselves means over some period of + # time (P2) less than a year. The reference time (R) indicates + # the date and time of the start of a period of time, given by + # R to R + P2, over which a mean is formed; N indicates the number + # of such period-means that are averaged together to form the + # climatological value, assuming that the N period-mean fields + # are separated by one year. The reference time indicates the + # start of the N-year climatology. N is given in octets 22-23 + # of the PDS. If P1 = 0 then the data averaged in the basic + # interval P2 are assumed to be continuous, i.e., all available + # data are simply averaged together. If P1 = 1 (the units of + # time - octet 18, code table 4 - are not relevant here) then + # the data averaged together in the basic interval P2 are valid + # only at the time (hour, minute) given in the reference time, + # for all the days included in the P2 period. The units of P2 + # are given by the contents of octet 18 and Table 4. + raise TranslationError("unhandled grib1 timeRangeIndicator " + "= 51 (avg of avgs)") + elif time_range_indicator == 113: + # Average of N forecasts (or initialized analyses); each + # product has forecast period of P1 (P1=0 for initialized + # analyses); products have reference times at intervals of P2, + # beginning at the given reference time. + time_diff = P1 + elif time_range_indicator == 114: + # Accumulation of N forecasts (or initialized analyses); each + # product has forecast period of P1 (P1=0 for initialized + # analyses); products have reference times at intervals of P2, + # beginning at the given reference time. + time_diff = P1 + elif time_range_indicator == 115: + # Average of N forecasts, all with the same reference time; + # the first has a forecast period of P1, the remaining + # forecasts follow at intervals of P2. + time_diff = P1 + elif time_range_indicator == 116: + # Accumulation of N forecasts, all with the same reference + # time; the first has a forecast period of P1, the remaining + # follow at intervals of P2. + time_diff = P1 + elif time_range_indicator == 117: + # Average of N forecasts, the first has a period of P1, the + # subsequent ones have forecast periods reduced from the + # previous one by an interval of P2; the reference time for + # the first is given in octets 13-17, the subsequent ones + # have reference times increased from the previous one by + # an interval of P2. Thus all the forecasts have the same + # valid time, given by the initial reference time + P1. + time_diff = P1 + elif time_range_indicator == 118: + # Temporal variance, or covariance, of N initialized analyses; + # each product has forecast period P1=0; products have + # reference times at intervals of P2, beginning at the given + # reference time. + time_diff = P1 + elif time_range_indicator == 123: + # Average of N uninitialized analyses, starting at the + # reference time, at intervals of P2. + time_diff = P1 + elif time_range_indicator == 124: + # Accumulation of N uninitialized analyses, starting at + # the reference time, at intervals of P2. + time_diff = P1 else: - raise TranslationError( - "unhandled grib edition = {}".format(self.edition) - ) + raise TranslationError("unhandled grib1 timeRangeIndicator " + "= %i" % time_range_indicator) # Get the timeunit interval. interval_secs = self._timeunit_seconds() @@ -718,7 +644,7 @@ def phenomenon_points(self, time_unit): """ time_reference = '%s since epoch' % time_unit return cf_units.date2num(self._phenomenonDateTime, time_reference, - cf_units.CALENDAR_GREGORIAN) + cf_units.CALENDAR_GREGORIAN) def phenomenon_bounds(self, time_unit): """ @@ -760,184 +686,44 @@ def _message_values(grib_message, shape): return data -def _is_quasi_regular_grib(grib_message): - """Detect GRIB 'thinned' a.k.a 'reduced' a.k.a 'quasi-regular' grid.""" - reduced_grids = ("reduced_ll", "reduced_gg") - return gribapi.grib_get(grib_message, 'gridType') in reduced_grids - - -def _regularise(grib_message): - """ - Transform a reduced grid to a regular grid using interpolation. +def _load_generate(filename): + messages = GribMessage.messages_from_filename(filename) + for message in messages: + editionNumber = message.sections[0]['editionNumber'] + if editionNumber == 1: + message_id = message._raw_message._message_id + grib_fh = message._file_ref.open_file + message = GribWrapper(message_id, grib_fh=grib_fh) + elif editionNumber != 2: + emsg = 'GRIB edition {} is not supported by {!r}.' + raise TranslationError(emsg.format(editionNumber, + type(message).__name__)) + yield message - Uses 1d linear interpolation at constant latitude to make the grid - regular. If the longitude dimension is circular then this is taken - into account by the interpolation. If the longitude dimension is not - circular then extrapolation is allowed to make sure all end regular - grid points get a value. In practice this extrapolation is likely to - be minimal. - """ - # Make sure to read any missing values as NaN. - gribapi.grib_set_double(grib_message, "missingValue", np.nan) - - # Get full longitude values, these describe the longitude value of - # *every* point in the grid, they are not 1d monotonic coordinates. - lons = gribapi.grib_get_double_array(grib_message, "longitudes") - - # Compute the new longitude coordinate for the regular grid. - new_nx = max(gribapi.grib_get_long_array(grib_message, "pl")) - new_x_step = (max(lons) - min(lons)) / (new_nx - 1) - if gribapi.grib_get_long(grib_message, "iScansNegatively"): - new_x_step *= -1 - - new_lons = np.arange(new_nx) * new_x_step + lons[0] - # Get full latitude and data values, these describe the latitude and - # data values of *every* point in the grid, they are not 1d monotonic - # coordinates. - lats = gribapi.grib_get_double_array(grib_message, "latitudes") - values = gribapi.grib_get_double_array(grib_message, "values") - - # Retrieve the distinct latitudes from the GRIB message. GRIBAPI docs - # don't specify if these points are guaranteed to be oriented correctly so - # the safe option is to sort them into ascending (south-to-north) order - # and then reverse the order if necessary. - new_lats = gribapi.grib_get_double_array(grib_message, "distinctLatitudes") - new_lats.sort() - if not gribapi.grib_get_long(grib_message, "jScansPositively"): - new_lats = new_lats[::-1] - ny = new_lats.shape[0] - - # Use 1d linear interpolation along latitude circles to regularise the - # reduced data. - cyclic = _longitude_is_cyclic(new_lons) - new_values = np.empty([ny, new_nx], dtype=values.dtype) - for ilat, lat in enumerate(new_lats): - idx = np.where(lats == lat) - llons = lons[idx] - vvalues = values[idx] - if cyclic: - # For cyclic data we insert dummy points at each end to ensure - # we can interpolate to all output longitudes using pure - # interpolation. - cgap = (360 - llons[-1] - llons[0]) - llons = np.concatenate( - (llons[0:1] - cgap, llons, llons[-1:] + cgap)) - vvalues = np.concatenate( - (vvalues[-1:], vvalues, vvalues[0:1])) - fixed_latitude_interpolator = scipy.interpolate.interp1d( - llons, vvalues) - else: - # Allow extrapolation for non-cyclic data sets to ensure we can - # interpolate to all output longitudes. - fixed_latitude_interpolator = Linear1dExtrapolator( - scipy.interpolate.interp1d(llons, vvalues)) - new_values[ilat] = fixed_latitude_interpolator(new_lons) - new_values = new_values.flatten() - - # Set flags for the regularised data. - if np.isnan(new_values).any(): - # Account for any missing data. - gribapi.grib_set_double(grib_message, "missingValue", np.inf) - gribapi.grib_set(grib_message, "bitmapPresent", 1) - new_values = np.where(np.isnan(new_values), np.inf, new_values) - - gribapi.grib_set_long(grib_message, "Nx", int(new_nx)) - gribapi.grib_set_double(grib_message, - "iDirectionIncrementInDegrees", float(new_x_step)) - gribapi.grib_set_double_array(grib_message, "values", new_values) - gribapi.grib_set_long(grib_message, "jPointsAreConsecutive", 0) - gribapi.grib_set_long(grib_message, "PLPresent", 0) - - -def grib_generator(filename, auto_regularise=True): - """ - Returns a generator of :class:`~iris.fileformats.grib.GribWrapper` - fields from the given filename. - - .. deprecated:: 1.10 - - The function: - :meth:`iris.fileformats.grib.message.GribMessage.messages_from_filename` - provides alternative means of obtainig GRIB messages from a file. - - Args: - - * filename (string): - Name of the file to generate fields from. - - Kwargs: - - * auto_regularise (*True* | *False*): - If *True*, any field defined on a reduced grid will be interpolated - to an equivalent regular grid. If *False*, any field defined on a - reduced grid will be loaded on the raw reduced grid with no shape - information. The default behaviour is to interpolate fields on a - reduced grid to an equivalent regular grid. - - """ - warn_deprecated('Deprecated at version 1.10') - with open(filename, 'rb') as grib_fh: - while True: - grib_message = gribapi.grib_new_from_file(grib_fh) - if grib_message is None: - break - - grib_wrapper = GribWrapper(grib_message, grib_fh, auto_regularise) - - yield grib_wrapper - - # finished with the grib message - claimed by the ecmwf c library. - gribapi.grib_release(grib_message) - - -def load_cubes(filenames, callback=None, auto_regularise=True): +def load_cubes(filenames, callback=None): """ Returns a generator of cubes from the given list of filenames. Args: - * filenames (string/list): + * filenames: One or more GRIB filenames to load from. Kwargs: - * callback (callable function): + * callback: Function which can be passed on to :func:`iris.io.run_callback`. - * auto_regularise (*True* | *False*): - If *True*, any cube defined on a reduced grid will be interpolated - to an equivalent regular grid. If *False*, any cube defined on a - reduced grid will be loaded on the raw reduced grid with no shape - information. If `iris.FUTURE.strict_grib_load` is `True` then this - keyword has no effect, raw grids are always used. If the older GRIB - loader is in use then the default behaviour is to interpolate cubes - on a reduced grid to an equivalent regular grid. - - .. deprecated:: 1.8. Please use strict_grib_load and regrid instead. - + Returns: + A generator containing Iris cubes loaded from the GRIB files. """ - if iris.FUTURE.strict_grib_load: - grib_loader = iris.fileformats.rules.Loader( - GribMessage.messages_from_filename, - {}, - iris.fileformats.grib._load_convert.convert) - else: - if auto_regularise is not None: - # The old loader supports the auto_regularise keyword, but in - # deprecation mode, so warning if it is found. - msg = ('the`auto_regularise` kwarg is deprecated and ' - 'will be removed in a future release. Resampling ' - 'quasi-regular grids on load will no longer be ' - 'available. Resampling should be done on the ' - 'loaded cube instead using Cube.regrid.') - warn_deprecated(msg) - - grib_loader = iris.fileformats.rules.Loader( - grib_generator, {'auto_regularise': auto_regularise}, - iris.fileformats.grib.load_rules.convert) - return iris.fileformats.rules.load_cubes(filenames, callback, grib_loader) + import iris.fileformats.rules as iris_rules + grib_loader = iris_rules.Loader(_load_generate, + {}, + load_convert) + return iris_rules.load_cubes(filenames, callback, grib_loader) def load_pairs_from_fields(grib_messages): @@ -945,15 +731,6 @@ def load_pairs_from_fields(grib_messages): Convert an iterable of GRIB messages into an iterable of (Cube, Grib message) tuples. - Args: - - * grib_messages: - An iterable of :class:`iris.fileformats.grib.message.GribMessage`. - - Returns: - An iterable of tuples of (:class:`iris.cube.Cube`, - :class:`iris.fileformats.grib.message.GribMessage`). - This capability can be used to filter out fields before they are passed to the load pipeline, and amend the cubes once they are created, using GRIB metadata conditions. Where the filtering @@ -986,49 +763,45 @@ def load_pairs_from_fields(grib_messages): ... message.sections[1]['productionStatusOfProcessedData'] = 4 >>> cubes = load_pairs_from_fields(cleaned_messages) + Args: + + * grib_messages: + An iterable of :class:`iris.fileformats.grib.message.GribMessage`. + + Returns: + An iterable of tuples of (:class:`iris.cube.Cube`, + :class:`iris.fileformats.grib.message.GribMessage`). + """ - grib_conv = iris.fileformats.grib._load_convert.convert - return iris.fileformats.rules.load_pairs_from_fields(grib_messages, - grib_conv) + import iris.fileformats.rules as iris_rules + return iris_rules.load_pairs_from_fields(grib_messages, load_convert) -def save_grib2(cube, target, append=False, **kwargs): +def save_grib2(cube, target, append=False): """ Save a cube or iterable of cubes to a GRIB2 file. Args: - * cube - A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or - list of cubes. - * target - A filename or open file handle. + * cube: + The :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or list of + cubes to save to a GRIB2 file. + * target: + A filename or open file handle specifying the GRIB2 file to save + to. Kwargs: - * append - Whether to start a new file afresh or add the cube(s) to - the end of the file. - Only applicable when target is a filename, not a file - handle. Default is False. - - See also :func:`iris.io.save`. + * append: + Whether to start a new file afresh or add the cube(s) to the end of + the file. Only applicable when target is a filename, not a file + handle. Default is False. """ - messages = as_messages(cube) + messages = (message for _, message in save_pairs_from_cube(cube)) save_messages(messages, target, append=append) -def as_pairs(cube): - """ - .. deprecated:: 1.10 - Please use :func:`iris.fileformats.grib.save_pairs_from_cube` - for the same functionality. - - - """ - warn_deprecated('as_pairs is deprecated in v1.10; please use' - ' save_pairs_from_cube instead.') - return save_pairs_from_cube(cube) - - def save_pairs_from_cube(cube): """ Convert one or more cubes to (2D cube, GRIB message) pairs. @@ -1037,7 +810,9 @@ def save_pairs_from_cube(cube): save rules. Args: - * cube - A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or + + * cube: + A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or list of cubes. """ @@ -1053,22 +828,6 @@ def save_pairs_from_cube(cube): yield (slice2D, grib_message) -def as_messages(cube): - """ - .. deprecated:: 1.10 - Please use :func:`iris.fileformats.grib.save_pairs_from_cube` instead. - - Convert one or more cubes to GRIB messages. - Returns an iterable of grib_api GRIB messages. - - Args: - * cube - A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or - list of cubes. - - """ - return (message for cube, message in save_pairs_from_cube(cube)) - - def save_messages(messages, target, append=False): """ Save messages to a GRIB2 file. @@ -1076,15 +835,17 @@ def save_messages(messages, target, append=False): Args: - * messages - An iterable of grib_api message IDs. - * target - A filename or open file handle. + * messages: + An iterable of grib_api message IDs. + * target: + A filename or open file handle. Kwargs: - * append - Whether to start a new file afresh or add the cube(s) to - the end of the file. - Only applicable when target is a filename, not a file - handle. Default is False. + * append: + Whether to start a new file afresh or add the cube(s) to the end of + the file. Only applicable when target is a filename, not a file + handle. Default is False. """ # grib file (this bit is common to the pp and grib savers...) diff --git a/lib/iris/fileformats/grib/load_rules.py b/lib/iris/fileformats/grib/_grib1_load_rules.py similarity index 56% rename from lib/iris/fileformats/grib/load_rules.py rename to lib/iris/fileformats/grib/_grib1_load_rules.py index ceea13374f..295f0d523e 100644 --- a/lib/iris/fileformats/grib/load_rules.py +++ b/lib/iris/fileformats/grib/_grib1_load_rules.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2016, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -26,16 +26,16 @@ from cf_units import CALENDAR_GREGORIAN, Unit import numpy as np -from iris._deprecation import warn_deprecated from iris.aux_factory import HybridPressureFactory from iris.coords import AuxCoord, CellMethod, DimCoord +from iris.exceptions import TranslationError from iris.fileformats.rules import (ConversionMetadata, Factory, Reference, ReferenceTarget) -def convert(grib): +def grib1_convert(grib): """ - Converts a GRIB message into the corresponding items of Cube metadata. + Converts a GRIB1 message into the corresponding items of Cube metadata. Args: @@ -46,6 +46,11 @@ def convert(grib): A :class:`iris.fileformats.rules.ConversionMetadata` object. """ + if grib.edition != 1: + emsg = 'GRIB edition {} is not supported by {!r}.' + raise TranslationError(emsg.format(grib.edition, + type(grib).__name__)) + factories = [] references = [] standard_name = None @@ -56,18 +61,6 @@ def convert(grib): dim_coords_and_dims = [] aux_coords_and_dims = [] - # deprecation warning for this code path for edition 2 messages - if grib.edition == 2: - msg = ('This GRIB loader is deprecated and will be removed in ' - 'a future release. Please consider using the new ' - 'GRIB loader by setting the :class:`iris.Future` ' - 'option `strict_grib_load` to True; e.g.:\n' - 'iris.FUTURE.strict_grib_load = True\n' - 'Please report issues you experience to:\n' - 'https://groups.google.com/forum/#!topic/scitools-iris-dev/' - 'lMsOusKNfaU') - warn_deprecated(msg) - if \ (grib.gridType=="reduced_gg"): aux_coords_and_dims.append((AuxCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 0)) @@ -114,7 +107,6 @@ def convert(grib): dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units="m", coord_system=grib._coord_system), 1)) if \ - (grib.edition == 1) and \ (grib.table2Version < 128) and \ (grib.indicatorOfParameter == 11) and \ (grib._cf_data is None): @@ -122,7 +114,6 @@ def convert(grib): units = "kelvin" if \ - (grib.edition == 1) and \ (grib.table2Version < 128) and \ (grib.indicatorOfParameter == 33) and \ (grib._cf_data is None): @@ -130,7 +121,6 @@ def convert(grib): units = "m s-1" if \ - (grib.edition == 1) and \ (grib.table2Version < 128) and \ (grib.indicatorOfParameter == 34) and \ (grib._cf_data is None): @@ -138,35 +128,24 @@ def convert(grib): units = "m s-1" if \ - (grib.edition == 1) and \ (grib._cf_data is not None): standard_name = grib._cf_data.standard_name long_name = grib._cf_data.standard_name or grib._cf_data.long_name units = grib._cf_data.units if \ - (grib.edition == 1) and \ (grib.table2Version >= 128) and \ (grib._cf_data is None): long_name = "UNKNOWN LOCAL PARAM " + str(grib.indicatorOfParameter) + "." + str(grib.table2Version) units = "???" if \ - (grib.edition == 1) and \ (grib.table2Version == 1) and \ (grib.indicatorOfParameter >= 128): long_name = "UNKNOWN LOCAL PARAM " + str(grib.indicatorOfParameter) + "." + str(grib.table2Version) units = "???" if \ - (grib.edition == 2) and \ - (grib._cf_data is not None): - standard_name = grib._cf_data.standard_name - long_name = grib._cf_data.long_name - units = grib._cf_data.units - - if \ - (grib.edition == 1) and \ (grib._phenomenonDateTime != -1.0): aux_coords_and_dims.append((DimCoord(points=grib.startStep, standard_name='forecast_period', units=grib._forecastTimeUnit), None)) aux_coords_and_dims.append((DimCoord(points=grib.phenomenon_points('hours'), standard_name='time', units=Unit('hours since epoch', CALENDAR_GREGORIAN)), None)) @@ -189,166 +168,79 @@ def add_bounded_time_coords(aux_coords_and_dims, grib): None)) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 2): add_bounded_time_coords(aux_coords_and_dims, grib) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 3): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("mean", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 4): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("sum", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 5): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("_difference", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 51): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("mean", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 113): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("mean", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 114): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("sum", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 115): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("mean", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 116): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("sum", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 117): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("mean", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 118): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("_covariance", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 123): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("mean", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 124): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("sum", coords="time")) if \ - (grib.edition == 1) and \ (grib.timeRangeIndicator == 125): add_bounded_time_coords(aux_coords_and_dims, grib) cell_methods.append(CellMethod("standard_deviation", coords="time")) if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 0): - aux_coords_and_dims.append((DimCoord(points=Unit(grib._forecastTimeUnit).convert(np.int32(grib._forecastTime), "hours"), standard_name='forecast_period', units="hours"), None)) - aux_coords_and_dims.append((DimCoord(points=grib.phenomenon_points('hours'), standard_name='time', units=Unit('hours since epoch', CALENDAR_GREGORIAN)), None)) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber in (8, 9)): - add_bounded_time_coords(aux_coords_and_dims, grib) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 0): - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 1): - cell_methods.append(CellMethod("sum", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 2): - cell_methods.append(CellMethod("maximum", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 3): - cell_methods.append(CellMethod("minimum", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 4): - cell_methods.append(CellMethod("_difference", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 5): - cell_methods.append(CellMethod("_root_mean_square", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 6): - cell_methods.append(CellMethod("standard_deviation", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 7): - cell_methods.append(CellMethod("_convariance", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 8): - cell_methods.append(CellMethod("_difference", coords="time")) - - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 8) and \ - (grib.typeOfStatisticalProcessing == 9): - cell_methods.append(CellMethod("_ratio", coords="time")) - - if \ - (grib.edition == 1) and \ (grib.levelType == 'pl'): aux_coords_and_dims.append((DimCoord(points=grib.level, long_name="pressure", units="hPa"), None)) if \ - (grib.edition == 1) and \ (grib.levelType == 'sfc'): if (grib._cf_data is not None) and \ @@ -358,7 +250,6 @@ def add_bounded_time_coords(aux_coords_and_dims, grib): aux_coords_and_dims.append((DimCoord(points=grib.level, long_name="height", units="m", attributes={'positive':'up'}), None)) if \ - (grib.edition == 1) and \ (grib.levelType == 'ml') and \ (hasattr(grib, 'pv')): aux_coords_and_dims.append((AuxCoord(grib.level, standard_name='model_level_number', attributes={'positive': 'up'}), None)) @@ -366,66 +257,9 @@ def add_bounded_time_coords(aux_coords_and_dims, grib): aux_coords_and_dims.append((AuxCoord(grib.pv[grib.numberOfCoordinatesValues//2 + grib.level], long_name='sigma'), None)) factories.append(Factory(HybridPressureFactory, [{'long_name': 'level_pressure'}, {'long_name': 'sigma'}, Reference('surface_pressure')])) - if \ - (grib.edition == 2) and \ - (grib.typeOfFirstFixedSurface != grib.typeOfSecondFixedSurface): - warnings.warn("Different vertical bound types not yet handled.") - - if \ - (grib.edition == 2) and \ - (grib.typeOfFirstFixedSurface == 103) and \ - (grib.typeOfSecondFixedSurface == 255): - aux_coords_and_dims.append((DimCoord(points=grib.scaledValueOfFirstFixedSurface/(10.0**grib.scaleFactorOfFirstFixedSurface), standard_name="height", units="m"), None)) - - if \ - (grib.edition == 2) and \ - (grib.typeOfFirstFixedSurface == 103) and \ - (grib.typeOfSecondFixedSurface != 255): - aux_coords_and_dims.append((DimCoord(points=0.5*(grib.scaledValueOfFirstFixedSurface/(10.0**grib.scaleFactorOfFirstFixedSurface) + grib.scaledValueOfSecondFixedSurface/(10.0**grib.scaleFactorOfSecondFixedSurface)), standard_name="height", units="m", bounds=[grib.scaledValueOfFirstFixedSurface/(10.0**grib.scaleFactorOfFirstFixedSurface), grib.scaledValueOfSecondFixedSurface/(10.0**grib.scaleFactorOfSecondFixedSurface)]), None)) - - if \ - (grib.edition == 2) and \ - (grib.typeOfFirstFixedSurface == 100) and \ - (grib.typeOfSecondFixedSurface == 255): - aux_coords_and_dims.append((DimCoord(points=grib.scaledValueOfFirstFixedSurface/(10.0**grib.scaleFactorOfFirstFixedSurface), long_name="pressure", units="Pa"), None)) - - if \ - (grib.edition == 2) and \ - (grib.typeOfFirstFixedSurface == 100) and \ - (grib.typeOfSecondFixedSurface != 255): - aux_coords_and_dims.append((DimCoord(points=0.5*(grib.scaledValueOfFirstFixedSurface/(10.0**grib.scaleFactorOfFirstFixedSurface) + grib.scaledValueOfSecondFixedSurface/(10.0**grib.scaleFactorOfSecondFixedSurface)), long_name="pressure", units="Pa", bounds=[grib.scaledValueOfFirstFixedSurface/(10.0**grib.scaleFactorOfFirstFixedSurface), grib.scaledValueOfSecondFixedSurface/(10.0**grib.scaleFactorOfSecondFixedSurface)]), None)) - - if \ - (grib.edition == 2) and \ - (grib.typeOfFirstFixedSurface in [105, 119]) and \ - (grib.numberOfCoordinatesValues > 0): - aux_coords_and_dims.append((AuxCoord(grib.scaledValueOfFirstFixedSurface, standard_name='model_level_number', attributes={'positive': 'up'}), None)) - aux_coords_and_dims.append((DimCoord(grib.pv[grib.scaledValueOfFirstFixedSurface], long_name='level_pressure', units='Pa'), None)) - aux_coords_and_dims.append((AuxCoord(grib.pv[grib.numberOfCoordinatesValues//2 + grib.scaledValueOfFirstFixedSurface], long_name='sigma'), None)) - factories.append(Factory(HybridPressureFactory, [{'long_name': 'level_pressure'}, {'long_name': 'sigma'}, Reference('surface_air_pressure')])) - if grib._originatingCentre != 'unknown': aux_coords_and_dims.append((AuxCoord(points=grib._originatingCentre, long_name='originating_centre', units='no_unit'), None)) - if \ - (grib.edition == 2) and \ - (grib.productDefinitionTemplateNumber == 1): - aux_coords_and_dims.append((DimCoord(points=grib.perturbationNumber, long_name='ensemble_member', units='no_unit'), None)) - - if \ - (grib.edition == 2) and \ - grib.productDefinitionTemplateNumber not in (0, 8): - attributes["GRIB_LOAD_WARNING"] = ("unsupported GRIB%d ProductDefinitionTemplate: #4.%d" % (grib.edition, grib.productDefinitionTemplateNumber)) - - if \ - (grib.edition == 2) and \ - (grib.centre == 'ecmf') and \ - (grib.discipline == 0) and \ - (grib.parameterCategory == 3) and \ - (grib.parameterNumber == 25) and \ - (grib.typeOfFirstFixedSurface == 105): - references.append(ReferenceTarget('surface_air_pressure', lambda cube: {'standard_name': 'surface_air_pressure', 'units': 'Pa', 'data': np.exp(cube.data)})) - return ConversionMetadata(factories, references, standard_name, long_name, units, attributes, cell_methods, dim_coords_and_dims, aux_coords_and_dims) diff --git a/lib/iris/fileformats/grib/_grib_cf_map.py b/lib/iris/fileformats/grib/_grib_cf_map.py index 71c523e431..63c597c9ba 100644 --- a/lib/iris/fileformats/grib/_grib_cf_map.py +++ b/lib/iris/fileformats/grib/_grib_cf_map.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2016, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -16,11 +16,11 @@ # along with Iris. If not, see . # # DO NOT EDIT: AUTO-GENERATED -# Created on 12 February 2016 17:02 from +# Created on 14 October 2016 15:10 from # http://www.metarelate.net/metOcean -# at commit cf419fba84a70fba5f394f1481cfcdbba28877ff +# at commit 3cde018acc4303203ff006a26f7b96a64e6ed3fb -# https://github.com/metarelate/metOcean/commit/cf419fba84a70fba5f394f1481cfcdbba28877ff +# https://github.com/metarelate/metOcean/commit/3cde018acc4303203ff006a26f7b96a64e6ed3fb """ Provides GRIB/CF phenomenon translations. @@ -88,6 +88,7 @@ G2Param(2, 0, 1, 49): CFName('precipitation_amount', None, 'kg m-2'), G2Param(2, 0, 1, 51): CFName('atmosphere_mass_content_of_water', None, 'kg m-2'), G2Param(2, 0, 1, 53): CFName('snowfall_flux', None, 'kg m-2 s-1'), + G2Param(2, 0, 1, 60): CFName('snowfall_amount', None, 'kg m-2'), G2Param(2, 0, 1, 64): CFName('atmosphere_mass_content_of_water_vapor', None, 'kg m-2'), G2Param(2, 0, 2, 0): CFName('wind_from_direction', None, 'degrees'), G2Param(2, 0, 2, 1): CFName('wind_speed', None, 'm s-1'), @@ -107,8 +108,8 @@ G2Param(2, 0, 4, 7): CFName('surface_downwelling_shortwave_flux_in_air', None, 'W m-2'), G2Param(2, 0, 4, 9): CFName('surface_net_downward_shortwave_flux', None, 'W m-2'), G2Param(2, 0, 5, 3): CFName('surface_downwelling_longwave_flux_in_air', None, 'W m-2'), - G2Param(2, 0, 5, 5): CFName('toa_outgoing_longwave_flux', None, 'W m-2'), - G2Param(2, 0, 6, 1): CFName('cloud_area_fraction', None, '%'), + G2Param(2, 0, 5, 5): CFName('surface_net_downward_longwave_flux', None, 'W m-2'), + G2Param(2, 0, 6, 1): CFName(None, 'cloud_area_fraction_assuming_maximum_random_overlap', '1'), G2Param(2, 0, 6, 3): CFName('low_type_cloud_area_fraction', None, '%'), G2Param(2, 0, 6, 4): CFName('medium_type_cloud_area_fraction', None, '%'), G2Param(2, 0, 6, 5): CFName('high_type_cloud_area_fraction', None, '%'), @@ -119,6 +120,7 @@ G2Param(2, 0, 7, 8): CFName(None, 'storm_relative_helicity', 'J kg-1'), G2Param(2, 0, 14, 0): CFName('atmosphere_mole_content_of_ozone', None, 'Dobson'), G2Param(2, 0, 19, 1): CFName(None, 'grib_physical_atmosphere_albedo', '%'), + G2Param(2, 2, 0, 0): CFName('land_binary_mask', None, '1'), G2Param(2, 2, 0, 0): CFName('land_area_fraction', None, '1'), G2Param(2, 2, 0, 1): CFName('surface_roughness_length', None, 'm'), G2Param(2, 2, 0, 2): CFName('soil_temperature', None, 'K'), @@ -170,6 +172,7 @@ CFName(None, 'storm_relative_helicity', 'J kg-1'): G2Param(2, 0, 7, 8), CFName('air_potential_temperature', None, 'K'): G2Param(2, 0, 0, 2), CFName('air_pressure', None, 'Pa'): G2Param(2, 0, 3, 0), + CFName('air_pressure_at_sea_level', None, 'Pa'): G2Param(2, 0, 3, 0), CFName('air_pressure_at_sea_level', None, 'Pa'): G2Param(2, 0, 3, 1), CFName('air_temperature', None, 'K'): G2Param(2, 0, 0, 0), CFName('altitude', None, 'm'): G2Param(2, 0, 3, 6), @@ -179,7 +182,6 @@ CFName('atmosphere_mass_content_of_water_vapor', None, 'kg m-2'): G2Param(2, 0, 1, 64), CFName('atmosphere_mole_content_of_ozone', None, 'Dobson'): G2Param(2, 0, 14, 0), CFName('atmosphere_specific_convective_available_potential_energy', None, 'J kg-1'): G2Param(2, 0, 7, 6), - CFName('cloud_area_fraction', None, '%'): G2Param(2, 0, 6, 1), CFName('cloud_area_fraction_in_atmosphere_layer', None, '%'): G2Param(2, 0, 6, 7), CFName('dew_point_temperature', None, 'K'): G2Param(2, 0, 0, 6), CFName('geopotential', None, 'm2 s-2'): G2Param(2, 0, 3, 4), @@ -201,6 +203,7 @@ CFName('sea_surface_temperature', None, 'K'): G2Param(2, 10, 3, 0), CFName('sea_water_x_velocity', None, 'm s-1'): G2Param(2, 10, 1, 2), CFName('sea_water_y_velocity', None, 'm s-1'): G2Param(2, 10, 1, 3), + CFName('snowfall_amount', None, 'kg m-2'): G2Param(2, 0, 1, 60), CFName('snowfall_flux', None, 'kg m-2 s-1'): G2Param(2, 0, 1, 53), CFName('soil_temperature', None, 'K'): G2Param(2, 2, 0, 2), CFName('specific_humidity', None, 'kg kg-1'): G2Param(2, 0, 1, 0), @@ -209,6 +212,7 @@ CFName('surface_downwelling_longwave_flux_in_air', None, 'W m-2'): G2Param(2, 0, 5, 3), CFName('surface_downwelling_shortwave_flux_in_air', None, 'W m-2'): G2Param(2, 0, 4, 7), CFName('surface_net_downward_longwave_flux', None, 'W m-2'): G2Param(2, 0, 5, 5), + CFName('surface_net_downward_longwave_flux', None, 'W m-2'): G2Param(2, 0, 5, 5), CFName('surface_net_downward_shortwave_flux', None, 'W m-2'): G2Param(2, 0, 4, 9), CFName('surface_roughness_length', None, 'm'): G2Param(2, 2, 0, 1), CFName('surface_runoff_flux', None, 'kg m-2 s-1'): G2Param(2, 2, 0, 34), @@ -216,7 +220,6 @@ CFName('surface_upward_latent_heat_flux', None, 'W m-2'): G2Param(2, 0, 0, 10), CFName('surface_upward_sensible_heat_flux', None, 'W m-2'): G2Param(2, 0, 0, 11), CFName('thickness_of_snowfall_amount', None, 'm'): G2Param(2, 0, 1, 11), - CFName('toa_outgoing_longwave_flux', None, 'W m-2'): G2Param(2, 0, 5, 5), CFName('wind_from_direction', None, 'degrees'): G2Param(2, 0, 2, 0), CFName('wind_speed', None, 'm s-1'): G2Param(2, 0, 2, 1), CFName('wind_speed_of_gust', None, 'm s-1'): G2Param(2, 0, 2, 22), diff --git a/lib/iris/fileformats/grib/_load_convert.py b/lib/iris/fileformats/grib/_load_convert.py index 54a3cbcd38..d12e6dd168 100644 --- a/lib/iris/fileformats/grib/_load_convert.py +++ b/lib/iris/fileformats/grib/_load_convert.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -38,7 +38,9 @@ import iris.coord_systems as icoord_systems from iris.coords import AuxCoord, DimCoord, CellMethod from iris.exceptions import TranslationError +from iris.fileformats.grib._grib1_load_rules import grib1_convert from iris.fileformats.grib import grib_phenom_translation as itranslation +from iris.fileformats.grib.message import GribMessage from iris.fileformats.rules import ConversionMetadata, Factory, Reference from iris.util import _is_circular @@ -157,7 +159,9 @@ def _unscale(v, f): if isinstance(value, Iterable) or isinstance(factor, Iterable): def _masker(item): - result = ma.masked_equal(item, _MDI) + mdis = [mdi for mdi in _MDIs] + result = ma.masked_equal(item, mdis[0]) + result = ma.masked_equal(result, mdis[1]) if ma.count_masked(result): # Circumvent downstream NumPy "RuntimeWarning" # of "overflow encountered in power" in _unscale @@ -171,13 +175,13 @@ def _masker(item): result = result.data else: result = ma.masked - if value != _MDI and factor != _MDI: + if value not in _MDIs and factor not in _MDIs: result = _unscale(value, factor) return result # Regulations 92.1.4 and 92.1.5. -_MDI = 2 ** 32 - 1 +_MDIs = set((2 ** 31 - 1, 2 ** 32 - 1)) # Note: # 1. Integer "on-disk" values (aka. coded keys) in GRIB messages: # - Are 8-, 16-, or 32-bit. @@ -274,7 +278,7 @@ def reference_time_coord(section): """ # Look-up standard name by significanceOfReferenceTime. - _lookup = {0: 'time', + _lookup = {0: 'forecast_reference_time', 1: 'forecast_reference_time', 2: 'time', 3: 'time'} @@ -622,10 +626,10 @@ def grid_definition_template_4_and_5(section, metadata, y_name, x_name, cs): basicAngleOfTheInitialProductDomain = section[key] subdivisionsOfBasicAngle = section['subdivisionsOfBasicAngle'] - if basicAngleOfTheInitialProductDomain in [0, _MDI]: + if basicAngleOfTheInitialProductDomain in [0] + [m for m in _MDIs]: basicAngleOfTheInitialProductDomain = 1. - if subdivisionsOfBasicAngle in [0, _MDI]: + if subdivisionsOfBasicAngle in [0] + [m for m in _MDIs]: subdivisionsOfBasicAngle = 1. / _GRID_ACCURACY_IN_DEGREES resolution = np.float64(basicAngleOfTheInitialProductDomain) @@ -1007,10 +1011,10 @@ def grid_definition_template_40_regular(section, metadata, cs): # Create lat/lon coordinates. x_coord = DimCoord(x_points, standard_name='longitude', - units='degrees_east', coord_system=cs, + units='degrees', coord_system=cs, circular=circular) y_coord = DimCoord(y_points, standard_name='latitude', - units='degrees_north', coord_system=cs) + units='degrees', coord_system=cs) # Determine the lat/lon dimensions. y_dim, x_dim = 0, 1 @@ -1042,9 +1046,9 @@ def grid_definition_template_40_reduced(section, metadata, cs): # Create lat/lon coordinates. x_coord = AuxCoord(x_points, standard_name='longitude', - units='degrees_east', coord_system=cs) + units='degrees', coord_system=cs) y_coord = AuxCoord(y_points, standard_name='latitude', - units='degrees_north', coord_system=cs) + units='degrees', coord_system=cs) # Add the lat/lon coordinates to the metadata dim coords. metadata['aux_coords_and_dims'].append((y_coord, 0)) @@ -1066,7 +1070,7 @@ def grid_definition_template_90(section, metadata): :class:`collections.OrderedDict` of metadata. """ - if section['Nr'] == _MDI: + if section['Nr'] in _MDIs: raise TranslationError('Unsupported orthographic grid.') elif section['Nr'] == 0: raise TranslationError('Unsupported zero height for space-view.') @@ -1348,7 +1352,7 @@ def hybrid_factories(section, metadata): units='Pa') metadata['aux_coords_and_dims'].append((coord, None)) # Create the sigma scalar coordinate. - offset += NV / 2 + offset += int(NV / 2) coord = AuxCoord(pv[offset], long_name='sigma') metadata['aux_coords_and_dims'].append((coord, None)) # Create the associated factory reference. @@ -1391,7 +1395,7 @@ def vertical_coords(section, metadata): if fixed_surface is None: if typeOfFirstFixedSurface != _TYPE_OF_FIXED_SURFACE_MISSING: - if scaledValueOfFirstFixedSurface == _MDI: + if scaledValueOfFirstFixedSurface in _MDIs: if options.warn_on_unsupported: msg = 'Unable to translate type of first fixed ' \ 'surface with missing scaled value.' @@ -1415,7 +1419,7 @@ def vertical_coords(section, metadata): key = 'scaledValueOfSecondFixedSurface' scaledValueOfSecondFixedSurface = section[key] - if scaledValueOfSecondFixedSurface == _MDI: + if scaledValueOfSecondFixedSurface in _MDIs: msg = 'Product definition section 4 has missing ' \ 'scaled value of second fixed surface' raise TranslationError(msg) @@ -1650,8 +1654,8 @@ def data_cutoff(hoursAfterDataCutoff, minutesAfterDataCutoff): Message section 4, octet 17. """ - if (hoursAfterDataCutoff != _MDI or - minutesAfterDataCutoff != _MDI): + if (hoursAfterDataCutoff not in _MDIs or + minutesAfterDataCutoff not in _MDIs): if options.warn_on_unsupported: warnings.warn('Unable to translate "hours and/or minutes ' 'after data cutoff".') @@ -1847,8 +1851,11 @@ def product_definition_template_8(section, metadata, frt_coord): # Add the forecast cell method to the metadata. metadata['cell_methods'].append(time_statistic_cell_method) - # Add the forecast reference time coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((frt_coord, None)) + # Add the forecast reference time coordinate to the metadata aux coords, + # if it is a forecast reference time, not a time coord, as defined by + # significanceOfReferenceTime. + if frt_coord.name() != 'time': + metadata['aux_coords_and_dims'].append((frt_coord, None)) # Add a bounded forecast period coordinate. fp_coord = statistical_forecast_period_coord(section, frt_coord) @@ -1900,12 +1907,12 @@ def product_definition_template_9(section, metadata, frt_coord): if probability_typecode == 1: # Type is "above upper level". threshold_value = section['scaledValueOfUpperLimit'] - if threshold_value == _MDI: + if threshold_value in _MDIs: msg = 'Product definition section 4 has missing ' \ 'scaled value of upper limit' raise TranslationError(msg) threshold_scaling = section['scaleFactorOfUpperLimit'] - if threshold_scaling == _MDI: + if threshold_scaling in _MDIs: msg = 'Product definition section 4 has missing ' \ 'scale factor of upper limit' raise TranslationError(msg) @@ -1923,6 +1930,35 @@ def product_definition_template_9(section, metadata, frt_coord): return probability_type +def product_definition_template_10(section, metadata, frt_coord): + """ + Translate template representing percentile forecasts at a horizontal level + or in a horizontal layer in a continuous or non-continuous time interval. + + Updates the metadata in-place with the translations. + + Args: + + * section: + Dictionary of coded key/value pairs from section 4 of the message. + + * metadata: + :class:`collections.OrderedDict` of metadata. + + * frt_coord: + The scalar forecast reference time :class:`iris.coords.DimCoord`. + + """ + product_definition_template_8(section, metadata, frt_coord) + + percentile = DimCoord(section['percentileValue'], + long_name='percentile_over_time', + units='no_unit') + + # Add the percentile data info + metadata['aux_coords_and_dims'].append((percentile, None)) + + def product_definition_template_11(section, metadata, frt_coord): """ Translate template representing individual ensemble forecast, control @@ -2035,6 +2071,7 @@ def product_definition_template_40(section, metadata, frt_coord): # Perform identical message processing. product_definition_template_0(section, metadata, frt_coord) + # Reference GRIB2 Code Table 4.230. constituent_type = section['constituentType'] # Add the constituent type as an attribute. @@ -2085,6 +2122,8 @@ def product_definition_section(section, metadata, discipline, tablesVersion, elif template == 9: probability = \ product_definition_template_9(section, metadata, rt_coord) + elif template == 10: + product_definition_template_10(section, metadata, rt_coord) elif template == 11: product_definition_template_11(section, metadata, rt_coord) elif template == 31: @@ -2220,24 +2259,38 @@ def convert(field): A :class:`iris.fileformats.rules.ConversionMetadata` object. """ - editionNumber = field.sections[0]['editionNumber'] - if editionNumber != 2: - msg = 'GRIB edition {} is not supported'.format(editionNumber) - raise TranslationError(msg) + if hasattr(field, 'sections'): + editionNumber = field.sections[0]['editionNumber'] - # Initialise the cube metadata. - metadata = OrderedDict() - metadata['factories'] = [] - metadata['references'] = [] - metadata['standard_name'] = None - metadata['long_name'] = None - metadata['units'] = None - metadata['attributes'] = {} - metadata['cell_methods'] = [] - metadata['dim_coords_and_dims'] = [] - metadata['aux_coords_and_dims'] = [] + if editionNumber != 2: + emsg = 'GRIB edition {} is not supported by {!r}.' + raise TranslationError(emsg.format(editionNumber, + type(field).__name__)) + + # Initialise the cube metadata. + metadata = OrderedDict() + metadata['factories'] = [] + metadata['references'] = [] + metadata['standard_name'] = None + metadata['long_name'] = None + metadata['units'] = None + metadata['attributes'] = {} + metadata['cell_methods'] = [] + metadata['dim_coords_and_dims'] = [] + metadata['aux_coords_and_dims'] = [] - # Convert GRIB2 message to cube metadata. - grib2_convert(field, metadata) + # Convert GRIB2 message to cube metadata. + grib2_convert(field, metadata) + + result = ConversionMetadata._make(metadata.values()) + else: + editionNumber = field.edition - return ConversionMetadata._make(metadata.values()) + if editionNumber != 1: + emsg = 'GRIB edition {} is not supported by {!r}.' + raise TranslationError(emsg.format(editionNumber, + type(field).__name__)) + + result = grib1_convert(field) + + return result diff --git a/lib/iris/fileformats/grib/_save_rules.py b/lib/iris/fileformats/grib/_save_rules.py index b9a224be2b..d9e914f7fb 100644 --- a/lib/iris/fileformats/grib/_save_rules.py +++ b/lib/iris/fileformats/grib/_save_rules.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -17,10 +17,9 @@ """ Grib save implementation. -This module replaces the deprecated -:mod:`iris.fileformats.grib.grib_save_rules`. It is a private module -with no public API. It is invoked from -:meth:`iris.fileformats.grib.save_grib2`. +:mod:`iris.fileformats.grib._save_rules` is a private module with +no public API. +It is invoked from :meth:`iris.fileformats.grib.save_grib2`. """ @@ -573,14 +572,6 @@ def _non_missing_forecast_period(cube): "scaling required.") fp = int(fp) - # Turn negative forecast times into grib negative numbers? - from iris.fileformats.grib import hindcast_workaround - if hindcast_workaround and fp < 0: - msg = "Encoding negative forecast period from {} to ".format(fp) - fp = 2**31 + abs(fp) - msg += "{}".format(np.int32(fp)) - warnings.warn(msg) - return rt, rt_meaning, fp, grib_time_code @@ -808,33 +799,55 @@ def _cube_is_time_statistic(cube): """ Test whether we can identify this cube as a statistic over time. - At present, accept anything whose latest cell method operates over a single - coordinate that "looks like" a time factor (i.e. some specific names). - In particular, we recognise the coordinate names defined in - :py:mod:`iris.coord_categorisation`. + We need to know whether our cube represents a time statistic. This is + almost always captured in the cell methods. The exception is when a + percentage statistic has been calculated (i.e. for PDT10). This is + captured in a `percentage_over_time` scalar coord, which must be handled + here too. """ - # The *only* relevant information is in cell_methods, as coordinates or - # dimensions of aggregation may no longer exist. So it's not possible to - # be definitive, but we handle *some* useful cases. - # In other cases just say "no", which is safe even when not ideal. + result = False + stat_coord_name = 'percentile_over_time' + cube_coord_names = [coord.name() for coord in cube.coords()] + + # Check our cube for time statistic indicators. + has_percentile_statistic = stat_coord_name in cube_coord_names + has_cell_methods = cube.cell_methods + + # Determine whether we have a time statistic. + if has_percentile_statistic: + result = True + elif has_cell_methods: + # Define accepted time names, including from coord_categorisations. + recognised_time_names = ['time', 'year', 'month', 'day', 'weekday', + 'season'] + latest_coordnames = cube.cell_methods[-1].coord_names + if len(latest_coordnames) != 1: + result = False + else: + coord_name = latest_coordnames[0] + result = coord_name in recognised_time_names + else: + result = False - # Identify a single coordinate from the latest cell_method. - if not cube.cell_methods: - return False - latest_coordnames = cube.cell_methods[-1].coord_names - if len(latest_coordnames) != 1: - return False - coord_name = latest_coordnames[0] + return result - # Define accepted time names, including those from coord_categorisations. - recognised_time_names = ['time', 'year', 'month', 'day', 'weekday', - 'season'] - # Accept it if the name is recognised. - # Currently does *not* recognise related names like 'month_number' or - # 'years', as that seems potentially unsafe. - return coord_name in recognised_time_names +def set_ensemble(cube, grib): + """ + Set keys in the provided grib based message relating to ensemble + information. + + """ + if not (cube.coords('realization') and + len(cube.coord('realization').points) == 1): + raise ValueError("A cube 'realization' coordinate with one " + "point is required, but not present") + gribapi.grib_set(grib, "perturbationNumber", + int(cube.coord('realization').points[0])) + # no encoding at present in iris, set to missing + gribapi.grib_set(grib, "numberOfForecastsInEnsemble", 255) + gribapi.grib_set(grib, "typeOfEnsembleForecast", 255) def product_definition_template_common(cube, grib): @@ -870,6 +883,21 @@ def product_definition_template_0(cube, grib): product_definition_template_common(cube, grib) +def product_definition_template_1(cube, grib): + """ + Set keys within the provided grib message based on Product + Definition Template 4.1. + + Template 4.1 is used to represent an individual ensemble forecast, control + and perturbed, at a horizontal level or in a horizontal layer at a point + in time. + + """ + gribapi.grib_set(grib, "productDefinitionTemplateNumber", 1) + product_definition_template_common(cube, grib) + set_ensemble(cube, grib) + + def product_definition_template_8(cube, grib): """ Set keys within the provided grib message based on Product @@ -880,32 +908,43 @@ def product_definition_template_8(cube, grib): """ gribapi.grib_set(grib, "productDefinitionTemplateNumber", 8) - _product_definition_template_8_and_11(cube, grib) + _product_definition_template_8_10_and_11(cube, grib) + + +def product_definition_template_10(cube, grib): + """ + Set keys within the provided grib message based on Product Definition + Template 4.10. + + Template 4.10 is used to represent a percentile forecast over a time + interval. + + """ + gribapi.grib_set(grib, "productDefinitionTemplateNumber", 10) + if not (cube.coords('percentile_over_time') and + len(cube.coord('percentile_over_time').points) == 1): + raise ValueError("A cube 'percentile_over_time' coordinate with one " + "point is required, but not present.") + gribapi.grib_set(grib, "percentileValue", + int(cube.coord('percentile_over_time').points[0])) + _product_definition_template_8_10_and_11(cube, grib) def product_definition_template_11(cube, grib): """ Set keys within the provided grib message based on Product - Definition Template 4.8. + Definition Template 4.11. - Template 4.8 is used to represent an aggregation over a time - interval. + Template 4.11 is used to represent an aggregation over a time + interval for an ensemble member. """ gribapi.grib_set(grib, "productDefinitionTemplateNumber", 11) - if not (cube.coords('realization') and - len(cube.coord('realization').points) == 1): - raise ValueError("A cube 'realization' coordinate with one" - "point is required, but not present") - gribapi.grib_set(grib, "perturbationNumber", - int(cube.coord('realization').points[0])) - # no encoding at present in Iris, set to missing - gribapi.grib_set(grib, "numberOfForecastsInEnsemble", 255) - gribapi.grib_set(grib, "typeOfEnsembleForecast", 255) - _product_definition_template_8_and_11(cube, grib) + set_ensemble(cube, grib) + _product_definition_template_8_10_and_11(cube, grib) -def _product_definition_template_8_and_11(cube, grib): +def _product_definition_template_8_10_and_11(cube, grib): """ Set keys within the provided grib message based on common aspects of Product Definition Templates 4.8 and 4.11. @@ -927,22 +966,6 @@ def _product_definition_template_8_and_11(cube, grib): msg = 'Expected time coordinate with two bounds, got {} bounds' raise ValueError(msg.format(time_coord.nbounds)) - # Check that there is one and only one cell method related to the - # time coord. - time_cell_methods = [cell_method for cell_method in cube.cell_methods if - 'time' in cell_method.coord_names] - if not time_cell_methods: - raise ValueError("Expected a cell method with a coordinate name " - "of 'time'") - if len(time_cell_methods) > 1: - raise ValueError("Cannot handle multiple 'time' cell methods") - cell_method, = time_cell_methods - - if len(cell_method.coord_names) > 1: - raise ValueError("Cannot handle multiple coordinate names in " - "the time related cell method. Expected ('time',), " - "got {!r}".format(cell_method.coord_names)) - # Extract the datetime-like object corresponding to the end of # the overall processing interval. end = time_coord.units.num2date(time_coord.bounds[0, -1]) @@ -962,15 +985,34 @@ def _product_definition_template_8_and_11(cube, grib): gribapi.grib_set(grib, "numberOfTimeRange", 1) gribapi.grib_set(grib, "numberOfMissingInStatisticalProcess", 0) - # Type of statistical process (see code table 4.10) - statistic_type = _STATISTIC_TYPE_NAMES.get(cell_method.method, 255) - gribapi.grib_set(grib, "typeOfStatisticalProcessing", statistic_type) - # Period over which statistical processing is performed. set_time_range(time_coord, grib) - # Time increment i.e. interval of cell method (if any) - set_time_increment(cell_method, grib) + # Check that there is one and only one cell method related to the + # time coord. + if cube.cell_methods: + time_cell_methods = [ + cell_method for cell_method in cube.cell_methods if 'time' in + cell_method.coord_names] + if not time_cell_methods: + raise ValueError("Expected a cell method with a coordinate name " + "of 'time'") + if len(time_cell_methods) > 1: + raise ValueError("Cannot handle multiple 'time' cell methods") + cell_method, = time_cell_methods + + if len(cell_method.coord_names) > 1: + raise ValueError("Cannot handle multiple coordinate names in " + "the time related cell method. Expected " + "('time',), got {!r}".format( + cell_method.coord_names)) + + # Type of statistical process (see code table 4.10) + statistic_type = _STATISTIC_TYPE_NAMES.get(cell_method.method, 255) + gribapi.grib_set(grib, "typeOfStatisticalProcessing", statistic_type) + + # Time increment i.e. interval of cell method (if any) + set_time_increment(cell_method, grib) def product_definition_template_40(cube, grib): @@ -996,7 +1038,10 @@ def product_definition_section(cube, grib): """ if not cube.coord("time").has_bounds(): - if 'WMO_constituent_type' in cube.attributes: + if cube.coords('realization'): + # ensemble forecast (template 4.1) + pdt = product_definition_template_1(cube, grib) + elif 'WMO_constituent_type' in cube.attributes: # forecast for atmospheric chemical constiuent (template 4.40) product_definition_template_40(cube, grib) else: @@ -1006,6 +1051,9 @@ def product_definition_section(cube, grib): if cube.coords('realization'): # time processed (template 4.11) pdt = product_definition_template_11 + elif cube.coords('percentile_over_time'): + # time processed as percentile (template 4.10) + pdt = product_definition_template_10 else: # time processed (template 4.8) pdt = product_definition_template_8 diff --git a/lib/iris/fileformats/grib/grib_phenom_translation.py b/lib/iris/fileformats/grib/grib_phenom_translation.py index 1d2507163f..68c533666b 100644 --- a/lib/iris/fileformats/grib/grib_phenom_translation.py +++ b/lib/iris/fileformats/grib/grib_phenom_translation.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -41,7 +41,7 @@ import iris.std_names -class LookupTable(dict): +class _LookupTable(dict): """ Specialised dictionary object for making lookup tables. @@ -51,7 +51,7 @@ class LookupTable(dict): """ def __init__(self, *args, **kwargs): - self._super = super(LookupTable, self) + self._super = super(_LookupTable, self) self._super.__init__(*args, **kwargs) def __getitem__(self, key): @@ -83,7 +83,7 @@ def __setitem__(self, key, value): def _make_grib1_cf_table(): """ Build the Grib1 to CF phenomenon translation table. """ - table = LookupTable() + table = _LookupTable() def _make_grib1_cf_entry(table2_version, centre_number, param_number, standard_name, long_name, units, set_height=None): @@ -170,7 +170,7 @@ def _make_grib1_cf_entry(table2_version, centre_number, param_number, def _make_grib2_to_cf_table(): """ Build the Grib2 to CF phenomenon translation table. """ - table = LookupTable() + table = _LookupTable() def _make_grib2_cf_entry(param_discipline, param_category, param_number, standard_name, long_name, units): @@ -233,7 +233,7 @@ def _make_grib2_cf_entry(param_discipline, param_category, param_number, def _make_cf_to_grib2_table(): """ Build the Grib1 to CF phenomenon translation table. """ - table = LookupTable() + table = _LookupTable() def _make_cf_grib2_entry(standard_name, long_name, param_discipline, param_category, param_number, diff --git a/lib/iris/fileformats/grib/grib_save_rules.py b/lib/iris/fileformats/grib/grib_save_rules.py deleted file mode 100644 index e6ecc45ef4..0000000000 --- a/lib/iris/fileformats/grib/grib_save_rules.py +++ /dev/null @@ -1,695 +0,0 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office -# -# This file is part of Iris. -# -# Iris is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Iris is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Iris. If not, see . -""" -Grib save implementation. - -..deprecated:: 1.8 - -This module is for legacy requirements only. -It has been superceded by :mod:`iris.fileformats.grib._save_rules', which has -no public API. - -""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -import warnings - -import cf_units -import gribapi -import numpy as np -import numpy.ma as ma - -import iris -import iris.exceptions -from iris.fileformats.rules import is_regular, regular_step -from iris.fileformats.grib import grib_phenom_translation as gptx - - -def gribbability_check(cube): - "We always need the following things for grib saving." - - # GeogCS exists? - cs0 = cube.coord(dimensions=[0]).coord_system - cs1 = cube.coord(dimensions=[1]).coord_system - if cs0 is None or cs1 is None: - raise iris.exceptions.TranslationError("CoordSystem not present") - if cs0 != cs1: - raise iris.exceptions.TranslationError("Inconsistent CoordSystems") - - # Regular? - y_coord = cube.coord(dimensions=[0]) - x_coord = cube.coord(dimensions=[1]) - if not is_regular(x_coord) or not is_regular(y_coord): - raise iris.exceptions.TranslationError( - "Cannot save irregular grids to grib") - - # Time period exists? - if not cube.coords("time"): - raise iris.exceptions.TranslationError("time coord not found") - - -############################################################################### -# -# Identification Section 1 -# -############################################################################### - -def centre(cube, grib): - # TODO: read centre from cube - gribapi.grib_set_long(grib, "centre", 74) # UKMO - gribapi.grib_set_long(grib, "subCentre", 0) # exeter is not in the spec - - -def reference_time(cube, grib): - # Set the reference time. - # (analysis, forecast start, verify time, obs time, etc) - try: - fp_coord = cube.coord("forecast_period") - except iris.exceptions.CoordinateNotFoundError: - fp_coord = None - - if fp_coord is not None: - rt, rt_meaning, _, _ = _non_missing_forecast_period(cube) - else: - rt, rt_meaning, _, _ = _missing_forecast_period(cube) - - gribapi.grib_set_long(grib, "significanceOfReferenceTime", rt_meaning) - gribapi.grib_set_long( - grib, "dataDate", "%04d%02d%02d" % (rt.year, rt.month, rt.day)) - gribapi.grib_set_long( - grib, "dataTime", "%02d%02d" % (rt.hour, rt.minute)) - - # TODO: Set the calendar, when we find out what happened to the proposal! - # http://tinyurl.com/oefqgv6 - # I was sure it was approved for pre-operational use but it's not there. - - -def identification(cube, grib): - centre(cube, grib) - reference_time(cube, grib) - - # operational product, operational test, research product, etc - # (missing for now) - gribapi.grib_set_long(grib, "productionStatusOfProcessedData", 255) - # analysis, forecast, processed satellite, processed radar, - # (analysis and forecast products for now) - gribapi.grib_set_long(grib, "typeOfProcessedData", 2) - - -############################################################################### -# -# Grid Definition Section 3 -# -############################################################################### - -def shape_of_the_earth(cube, grib): - - # assume latlon - cs = cube.coord(dimensions=[0]).coord_system - - # Turn them all missing to start with (255 for byte, -1 for long) - gribapi.grib_set_long(grib, "scaleFactorOfRadiusOfSphericalEarth", 255) - gribapi.grib_set_long(grib, "scaledValueOfRadiusOfSphericalEarth", -1) - gribapi.grib_set_long(grib, "scaleFactorOfEarthMajorAxis", 255) - gribapi.grib_set_long(grib, "scaledValueOfEarthMajorAxis", -1) - gribapi.grib_set_long(grib, "scaleFactorOfEarthMinorAxis", 255) - gribapi.grib_set_long(grib, "scaledValueOfEarthMinorAxis", -1) - - ellipsoid = cs - if isinstance(cs, iris.coord_systems.RotatedGeogCS): - ellipsoid = cs.ellipsoid - - if ellipsoid.inverse_flattening == 0.0: - gribapi.grib_set_long(grib, "shapeOfTheEarth", 1) - gribapi.grib_set_long(grib, "scaleFactorOfRadiusOfSphericalEarth", 0) - gribapi.grib_set_long(grib, "scaledValueOfRadiusOfSphericalEarth", - ellipsoid.semi_major_axis) - else: - gribapi.grib_set_long(grib, "shapeOfTheEarth", 7) - gribapi.grib_set_long(grib, "scaleFactorOfEarthMajorAxis", 0) - gribapi.grib_set_long(grib, "scaledValueOfEarthMajorAxis", - ellipsoid.semi_major_axis) - gribapi.grib_set_long(grib, "scaleFactorOfEarthMinorAxis", 0) - gribapi.grib_set_long(grib, "scaledValueOfEarthMinorAxis", - ellipsoid.semi_minor_axis) - - -def grid_dims(x_coord, y_coord, grib): - gribapi.grib_set_long(grib, "Ni", x_coord.shape[0]) - gribapi.grib_set_long(grib, "Nj", y_coord.shape[0]) - - -def latlon_first_last(x_coord, y_coord, grib): - if x_coord.has_bounds() or y_coord.has_bounds(): - warnings.warn("Ignoring xy bounds") - -# XXX Pending #1125 -# gribapi.grib_set_double(grib, "latitudeOfFirstGridPointInDegrees", -# float(y_coord.points[0])) -# gribapi.grib_set_double(grib, "latitudeOfLastGridPointInDegrees", -# float(y_coord.points[-1])) -# gribapi.grib_set_double(grib, "longitudeOfFirstGridPointInDegrees", -# float(x_coord.points[0])) -# gribapi.grib_set_double(grib, "longitudeOfLastGridPointInDegrees", -# float(x_coord.points[-1])) -# WORKAROUND - gribapi.grib_set_long(grib, "latitudeOfFirstGridPoint", - int(y_coord.points[0]*1000000)) - gribapi.grib_set_long(grib, "latitudeOfLastGridPoint", - int(y_coord.points[-1]*1000000)) - gribapi.grib_set_long(grib, "longitudeOfFirstGridPoint", - int((x_coord.points[0] % 360)*1000000)) - gribapi.grib_set_long(grib, "longitudeOfLastGridPoint", - int((x_coord.points[-1] % 360)*1000000)) - - -def dx_dy(x_coord, y_coord, grib): - x_step = regular_step(x_coord) - y_step = regular_step(y_coord) - # TODO: THIS USED BE "Dx" and "Dy"!!! DID THE API CHANGE AGAIN??? - gribapi.grib_set_double(grib, "DxInDegrees", float(abs(x_step))) - gribapi.grib_set_double(grib, "DyInDegrees", float(abs(y_step))) - - -def scanning_mode_flags(x_coord, y_coord, grib): - gribapi.grib_set_long(grib, "iScansPositively", - int(x_coord.points[1] - x_coord.points[0] > 0)) - gribapi.grib_set_long(grib, "jScansPositively", - int(y_coord.points[1] - y_coord.points[0] > 0)) - - -def latlon_common(cube, grib): - y_coord = cube.coord(dimensions=[0]) - x_coord = cube.coord(dimensions=[1]) - shape_of_the_earth(cube, grib) - grid_dims(x_coord, y_coord, grib) - latlon_first_last(x_coord, y_coord, grib) - dx_dy(x_coord, y_coord, grib) - scanning_mode_flags(x_coord, y_coord, grib) - - -def rotated_pole(cube, grib): - cs = cube.coord(dimensions=[0]).coord_system - -# XXX Pending #1125 -# gribapi.grib_set_double(grib, "latitudeOfSouthernPoleInDegrees", -# float(cs.n_pole.latitude)) -# gribapi.grib_set_double(grib, "longitudeOfSouthernPoleInDegrees", -# float(cs.n_pole.longitude)) -# gribapi.grib_set_double(grib, "angleOfRotationInDegrees", 0) -# WORKAROUND - latitude = -int(cs.grid_north_pole_latitude*1000000) - longitude = int(((cs.grid_north_pole_longitude+180) % 360)*1000000) - gribapi.grib_set_long(grib, "latitudeOfSouthernPole", latitude) - gribapi.grib_set_long(grib, "longitudeOfSouthernPole", longitude) - gribapi.grib_set_long(grib, "angleOfRotation", 0) - - -def grid_template(cube, grib): - cs = cube.coord(dimensions=[0]).coord_system - if isinstance(cs, iris.coord_systems.GeogCS): - # template 3.0 - gribapi.grib_set_long(grib, "gridDefinitionTemplateNumber", 0) - latlon_common(cube, grib) - - # rotated - elif isinstance(cs, iris.coord_systems.RotatedGeogCS): - # template 3.1 - gribapi.grib_set_long(grib, "gridDefinitionTemplateNumber", 1) - latlon_common(cube, grib) - rotated_pole(cube, grib) - else: - raise ValueError("Currently unhandled CoordSystem: %s" % cs) - - -############################################################################### -# -# Product Definition Section 4 -# -############################################################################### - -def param_code(cube, grib): - # NOTE: for now, can match by *either* standard_name or long_name. - # This allows workarounds for data with no identified standard_name. - grib2_info = gptx.cf_phenom_to_grib2_info(cube.standard_name, - cube.long_name) - if grib2_info is not None: - gribapi.grib_set_long(grib, "discipline", - int(grib2_info.discipline)) - gribapi.grib_set_long(grib, "parameterCategory", - int(grib2_info.category)) - gribapi.grib_set_long(grib, "parameterNumber", - int(grib2_info.number)) - else: - gribapi.grib_set_long(grib, "discipline", 255) - gribapi.grib_set_long(grib, "parameterCategory", 255) - gribapi.grib_set_long(grib, "parameterNumber", 255) - warnings.warn('Unable to determine Grib2 parameter code for cube.\n' - 'discipline, parameterCategory and parameterNumber ' - 'have been set to "missing".') - - -def generating_process_type(cube, grib): - # analysis = 0 - # initialisation = 1 - # forecast = 2 - # more... - - # missing - gribapi.grib_set_long(grib, "typeOfGeneratingProcess", 255) - - -def background_process_id(cube, grib): - # locally defined - gribapi.grib_set_long(grib, "backgroundProcess", 255) - - -def generating_process_id(cube, grib): - # locally defined - gribapi.grib_set_long(grib, "generatingProcessIdentifier", 255) - - -def obs_time_after_cutoff(cube, grib): - # nothing stored in iris for this at present - gribapi.grib_set_long(grib, "hoursAfterDataCutoff", 0) - gribapi.grib_set_long(grib, "minutesAfterDataCutoff", 0) - - -def _non_missing_forecast_period(cube): - # Calculate "model start time" to use as the reference time. - fp_coord = cube.coord("forecast_period") - - # Convert fp and t to hours so we can subtract to calculate R. - cf_fp_hrs = fp_coord.units.convert(fp_coord.points[0], 'hours') - t_coord = cube.coord("time").copy() - hours_since = cf_units.Unit("hours since epoch", - calendar=t_coord.units.calendar) - t_coord.convert_units(hours_since) - - rt_num = t_coord.points[0] - cf_fp_hrs - rt = hours_since.num2date(rt_num) - rt_meaning = 1 # "start of forecast" - - # Forecast period - if fp_coord.units == cf_units.Unit("hours"): - grib_time_code = 1 - elif fp_coord.units == cf_units.Unit("minutes"): - grib_time_code = 0 - elif fp_coord.units == cf_units.Unit("seconds"): - grib_time_code = 13 - else: - raise iris.exceptions.TranslationError( - "Unexpected units for 'forecast_period' : %s" % fp_coord.units) - - if not t_coord.has_bounds(): - fp = fp_coord.points[0] - else: - if not fp_coord.has_bounds(): - raise iris.exceptions.TranslationError( - "bounds on 'time' coordinate requires bounds on" - " 'forecast_period'.") - fp = fp_coord.bounds[0][0] - - if fp - int(fp): - warnings.warn("forecast_period encoding problem: " - "scaling required.") - fp = int(fp) - - # Turn negative forecast times into grib negative numbers? - from iris.fileformats.grib import hindcast_workaround - if hindcast_workaround and fp < 0: - msg = "Encoding negative forecast period from {} to ".format(fp) - fp = 2**31 + abs(fp) - msg += "{}".format(np.int32(fp)) - warnings.warn(msg) - - return rt, rt_meaning, fp, grib_time_code - - -def _missing_forecast_period(cube): - # We have no way of knowing the CF forecast reference time. - # Set GRIB reference time to "verifying time of forecast", - # and the forecast period to 0h. - warnings.warn('No CF forecast_period. Setting reference time to mean ' - '"verifying time of forecast", "forecast time" = 0h') - - t_coord = cube.coord("time") - t = t_coord.bounds[0, 0] if t_coord.has_bounds() else t_coord.points[0] - rt = t_coord.units.num2date(t) - rt_meaning = 2 # "verification time of forecast" - - fp = 0 - fp_meaning = 1 # hours - - return rt, rt_meaning, fp, fp_meaning - - -def time_range(cube, grib): - """Grib encoding of forecast_period.""" - try: - fp_coord = cube.coord("forecast_period") - except iris.exceptions.CoordinateNotFoundError: - fp_coord = None - - if fp_coord is not None: - _, _, fp, grib_time_code = _non_missing_forecast_period(cube) - else: - _, _, fp, grib_time_code = _missing_forecast_period(cube) - - gribapi.grib_set_long(grib, "indicatorOfUnitOfTimeRange", grib_time_code) - gribapi.grib_set_long(grib, "forecastTime", fp) - - -def hybrid_surfaces(cube, grib): - is_hybrid = False -# XXX Addressed in #1118 pending #1039 for hybrid levels -# -# # hybrid height? (assume points) -# if cube.coords("model_level") and cube.coords("level_height") and \ -# cube.coords("sigma") and \ -# isinstance(cube.coord("sigma").coord_system, -# iris.coord_systems.HybridHeightCS): -# is_hybrid = True -# gribapi.grib_set_long(grib, "typeOfFirstFixedSurface", 118) -# gribapi.grib_set_long(grib, "scaledValueOfFirstFixedSurface", -# long(cube.coord("model_level").points[0])) -# gribapi.grib_set_long(grib, "PVPresent", 1) -# gribapi.grib_set_long(grib, "numberOfVerticalCoordinateValues", 2) -# level_height = cube.coord("level_height").points[0] -# sigma = cube.coord("sigma").points[0] -# gribapi.grib_set_double_array(grib, "pv", [level_height, sigma]) -# -# # hybrid pressure? -# if XXX: -# pass - return is_hybrid - - -def non_hybrid_surfaces(cube, grib): - - # Look for something we can export - v_coord = grib_v_code = output_unit = None - - # pressure - if cube.coords("air_pressure") or cube.coords("pressure"): - grib_v_code = 100 - output_unit = cf_units.Unit("Pa") - v_coord = (cube.coords("air_pressure") or cube.coords("pressure"))[0] - - # altitude - elif cube.coords("altitude"): - grib_v_code = 102 - output_unit = cf_units.Unit("m") - v_coord = cube.coord("altitude") - - # height - elif cube.coords("height"): - grib_v_code = 103 - output_unit = cf_units.Unit("m") - v_coord = cube.coord("height") - - # unknown / absent - else: - # check for *ANY* height coords at all... - v_coords = cube.coords(axis='z') - if v_coords: - # There are vertical coordinate(s), but we don't understand them... - v_coords_str = ' ,'.join(["'{}'".format(c.name()) - for c in v_coords]) - raise iris.exceptions.TranslationError( - 'The vertical-axis coordinate(s) ({}) ' - 'are not recognised or handled.'.format(v_coords_str)) - - # What did we find? - if v_coord is None: - # No vertical coordinate: record as 'surface' level (levelType=1). - # NOTE: may *not* be truly correct, but seems to be common practice. - # Still under investigation : - # See https://github.com/SciTools/iris/issues/519 - gribapi.grib_set_long(grib, "typeOfFirstFixedSurface", 1) - gribapi.grib_set_long(grib, "scaleFactorOfFirstFixedSurface", 0) - gribapi.grib_set_long(grib, "scaledValueOfFirstFixedSurface", 0) - # Set secondary surface = 'missing'. - gribapi.grib_set_long(grib, "typeOfSecondFixedSurface", -1) - gribapi.grib_set_long(grib, "scaleFactorOfSecondFixedSurface", 255) - gribapi.grib_set_long(grib, "scaledValueOfSecondFixedSurface", -1) - elif not v_coord.has_bounds(): - # No second surface - output_v = v_coord.units.convert(v_coord.points[0], output_unit) - if output_v - abs(output_v): - warnings.warn("Vertical level encoding problem: scaling required.") - output_v = int(output_v) - - gribapi.grib_set_long(grib, "typeOfFirstFixedSurface", grib_v_code) - gribapi.grib_set_long(grib, "scaleFactorOfFirstFixedSurface", 0) - gribapi.grib_set_long(grib, "scaledValueOfFirstFixedSurface", output_v) - gribapi.grib_set_long(grib, "typeOfSecondFixedSurface", -1) - gribapi.grib_set_long(grib, "scaleFactorOfSecondFixedSurface", 255) - gribapi.grib_set_long(grib, "scaledValueOfSecondFixedSurface", -1) - else: - # bounded : set lower+upper surfaces - output_v = v_coord.units.convert(v_coord.bounds[0], output_unit) - if output_v[0] - abs(output_v[0]) or output_v[1] - abs(output_v[1]): - warnings.warn("Vertical level encoding problem: scaling required.") - gribapi.grib_set_long(grib, "typeOfFirstFixedSurface", grib_v_code) - gribapi.grib_set_long(grib, "typeOfSecondFixedSurface", grib_v_code) - gribapi.grib_set_long(grib, "scaleFactorOfFirstFixedSurface", 0) - gribapi.grib_set_long(grib, "scaleFactorOfSecondFixedSurface", 0) - gribapi.grib_set_long(grib, "scaledValueOfFirstFixedSurface", - int(output_v[0])) - gribapi.grib_set_long(grib, "scaledValueOfSecondFixedSurface", - int(output_v[1])) - - -def surfaces(cube, grib): - if not hybrid_surfaces(cube, grib): - non_hybrid_surfaces(cube, grib) - - -def product_common(cube, grib): - param_code(cube, grib) - generating_process_type(cube, grib) - background_process_id(cube, grib) - generating_process_id(cube, grib) - obs_time_after_cutoff(cube, grib) - time_range(cube, grib) - surfaces(cube, grib) - - -def type_of_statistical_processing(cube, grib, coord): - """Search for processing over the given coord.""" - # if the last cell method applies only to the given coord... - cell_method = cube.cell_methods[-1] - coord_names = cell_method.coord_names - if len(coord_names) != 1: - raise ValueError('There are multiple coord names referenced by ' - 'the primary cell method: {!r}. Multiple coordinate ' - 'names are not supported.'.format(coord_names)) - if coord_names[0] != coord.name(): - raise ValueError('The coord name referenced by the primary cell method' - ', {!r}, is not the expected coord name {!r}.' - ''.format(coord_names[0], coord.name())) - stat_codes = {'mean': 0, 'sum': 1, 'maximum': 2, 'minimum': 3, - 'standard_deviation': 6} - # 255 is the code in template 4.8 for 'unknown' statistical method - stat_code = stat_codes.get(cell_method.method, 255) - gribapi.grib_set_long(grib, "typeOfStatisticalProcessing", stat_code) - - -def time_processing_period(cube, grib): - """ - For template 4.8 (time mean, time max, etc). - - The time range is taken from the 'time' coordinate bounds. - If the cell-method coordinate is not 'time' itself, the type of statistic - will not be derived and the save process will be aborted. - - """ - # We could probably split this function up a bit - - # Can safely assume bounded pt. - pt_coord = cube.coord("time") - end = cf_units.num2date(pt_coord.bounds[0, 1], pt_coord.units.name, - pt_coord.units.calendar) - - gribapi.grib_set_long(grib, "yearOfEndOfOverallTimeInterval", end.year) - gribapi.grib_set_long(grib, "monthOfEndOfOverallTimeInterval", end.month) - gribapi.grib_set_long(grib, "dayOfEndOfOverallTimeInterval", end.day) - gribapi.grib_set_long(grib, "hourOfEndOfOverallTimeInterval", end.hour) - gribapi.grib_set_long(grib, "minuteOfEndOfOverallTimeInterval", end.minute) - gribapi.grib_set_long(grib, "secondOfEndOfOverallTimeInterval", end.second) - - gribapi.grib_set_long(grib, "numberOfTimeRange", 1) - gribapi.grib_set_long(grib, "numberOfMissingInStatisticalProcess", 0) - - type_of_statistical_processing(cube, grib, pt_coord) - - # Type of time increment, e.g incrementing fp, incrementing ref - # time, etc. (code table 4.11) - gribapi.grib_set_long(grib, "typeOfTimeIncrement", 255) - # time unit for period over which statistical processing is done (hours) - gribapi.grib_set_long(grib, "indicatorOfUnitForTimeRange", 1) - # period over which statistical processing is done - gribapi.grib_set_long(grib, "lengthOfTimeRange", - float(pt_coord.bounds[0, 1] - pt_coord.bounds[0, 0])) - # time unit between successive source fields (not setting this at present) - gribapi.grib_set_long(grib, "indicatorOfUnitForTimeIncrement", 255) - # between successive source fields (just set to 0 for now) - gribapi.grib_set_long(grib, "timeIncrement", 0) - - -def _cube_is_time_statistic(cube): - """ - Test whether we can identify this cube as a statistic over time. - - At present, accept anything whose latest cell method operates over a single - coordinate that "looks like" a time factor (i.e. some specific names). - In particular, we recognise the coordinate names defined in - :py:mod:`iris.coord_categorisation`. - - """ - # The *only* relevant information is in cell_methods, as coordinates or - # dimensions of aggregation may no longer exist. So it's not possible to - # be definitive, but we handle *some* useful cases. - # In other cases just say "no", which is safe even when not ideal. - - # Identify a single coordinate from the latest cell_method. - if not cube.cell_methods: - return False - latest_coordnames = cube.cell_methods[-1].coord_names - if len(latest_coordnames) != 1: - return False - coord_name = latest_coordnames[0] - - # Define accepted time names, including those from coord_categorisations. - recognised_time_names = ['time', 'year', 'month', 'day', 'weekday', - 'season'] - - # Accept it if the name is recognised. - # Currently does *not* recognise related names like 'month_number' or - # 'years', as that seems potentially unsafe. - return coord_name in recognised_time_names - - -def product_template(cube, grib): - # This will become more complex if we cover more templates, such as 4.15 - - # forecast (template 4.0) - if not cube.coord("time").has_bounds(): - gribapi.grib_set_long(grib, "productDefinitionTemplateNumber", 0) - product_common(cube, grib) - return - - # time processed (template 4.8) - if _cube_is_time_statistic(cube): - gribapi.grib_set_long(grib, "productDefinitionTemplateNumber", 8) - product_common(cube, grib) - try: - time_processing_period(cube, grib) - except ValueError as e: - raise ValueError('Saving to GRIB2 failed: the cube is not suitable' - ' for saving as a time processed statistic GRIB' - ' message. {}'.format(e)) - return - - # Don't know how to handle this kind of data - raise iris.exceptions.TranslationError( - 'A suitable product template could not be deduced') - - -############################################################################### -# -# Data Representation Section 5 -# -############################################################################### - -def data(cube, grib): - # Masked data? - if isinstance(cube.data, ma.core.MaskedArray): - # What missing value shall we use? - if not np.isnan(cube.data.fill_value): - # Use the data's fill value. - fill_value = float(cube.data.fill_value) - else: - # We can't use the data's fill value if it's NaN, - # the GRIB API doesn't like it. - # Calculate an MDI outside the data range. - min, max = cube.data.min(), cube.data.max() - fill_value = min - (max - min) * 0.1 - # Prepare the unmaksed data array, using fill_value as the MDI. - data = cube.data.filled(fill_value) - else: - fill_value = None - data = cube.data - - # units scaling - grib2_info = gptx.cf_phenom_to_grib2_info(cube.standard_name, - cube.long_name) - if grib2_info is None: - # for now, just allow this - warnings.warn('Unable to determine Grib2 parameter code for cube.\n' - 'Message data may not be correctly scaled.') - else: - if cube.units != grib2_info.units: - data = cube.units.convert(data, grib2_info.units) - if fill_value is not None: - fill_value = cube.units.convert(fill_value, grib2_info.units) - - if fill_value is None: - # Disable missing values in the grib message. - gribapi.grib_set(grib, "bitmapPresent", 0) - else: - # Enable missing values in the grib message. - gribapi.grib_set(grib, "bitmapPresent", 1) - gribapi.grib_set_double(grib, "missingValue", fill_value) - gribapi.grib_set_double_array(grib, "values", data.flatten()) - - # todo: check packing accuracy? -# print("packingError", gribapi.getb_get_double(grib, "packingError")) - - -############################################################################### - -def run(cube, grib): - """ - Sets the keys of the grib message based on the contents of the cube. - - Args: - - * cube: - An instance of :class:`iris.cube.Cube`. - - * grib_message_id: - ID of a grib message in memory. This is typically the return value of - :func:`gribapi.grib_new_from_samples`. - - """ - gribbability_check(cube) - - # Section 1 - Identification Section. - identification(cube, grib) - - # Section 3 - Grid Definition Section (Grid Definition Template) - grid_template(cube, grib) - - # Section 4 - Product Definition Section (Product Definition Template) - product_template(cube, grib) - - # Section 5 - Data Representation Section (Data Representation Template) - data(cube, grib) diff --git a/lib/iris/fileformats/grib/message.py b/lib/iris/fileformats/grib/message.py index 5c6cb04009..b70a8dd17a 100644 --- a/lib/iris/fileformats/grib/message.py +++ b/lib/iris/fileformats/grib/message.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/integration/test_grib_load.py b/lib/iris/tests/integration/test_grib_load.py index a66c54a762..bae290cde4 100644 --- a/lib/iris/tests/integration/test_grib_load.py +++ b/lib/iris/tests/integration/test_grib_load.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -56,6 +56,8 @@ skip_irisgrib_fails = skipIf(iris_internal_grib_module is None, 'Test(s) are not currently ussable with ' '"iris_grib".') +skip_irisgrib_fails = skipIf(True, 'Test(s) are not currently ussable with ' + '"iris_grib".') @tests.skip_data diff --git a/lib/iris/tests/results/unit/fileformats/grib/load_cubes/load_cubes/reduced_raw.cml b/lib/iris/tests/results/unit/fileformats/grib/load_cubes/load_cubes/reduced_raw.cml index 896fcaa9bb..1c3ea9062c 100644 --- a/lib/iris/tests/results/unit/fileformats/grib/load_cubes/load_cubes/reduced_raw.cml +++ b/lib/iris/tests/results/unit/fileformats/grib/load_cubes/load_cubes/reduced_raw.cml @@ -1,9 +1,15 @@ + + + - + + + + + - - - - + diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 1985d88206..1f96ff404c 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -85,9 +85,8 @@ class StandardReportWithExclusions(pep8.StandardReport): '*/iris/analysis/_interpolate_private.py', '*/iris/fileformats/cf.py', '*/iris/fileformats/dot.py', - '*/iris/fileformats/grib/__init__.py', '*/iris/fileformats/grib/_grib_cf_map.py', - '*/iris/fileformats/grib/load_rules.py', + '*/iris/fileformats/grib/_grib1_load_rules.py', '*/iris/fileformats/pp_rules.py', '*/iris/fileformats/rules.py', '*/iris/fileformats/um_cf_map.py', diff --git a/lib/iris/tests/test_grib_load_translations.py b/lib/iris/tests/test_grib_load_translations.py index f868d8d70d..25f508dbd6 100644 --- a/lib/iris/tests/test_grib_load_translations.py +++ b/lib/iris/tests/test_grib_load_translations.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -53,6 +53,7 @@ if tests.GRIB_AVAILABLE: import gribapi import iris.fileformats.grib + import iris.fileformats.grib.grib_phenom_translation as gpt def _mock_gribapi_fetch(message, key): @@ -302,15 +303,6 @@ def test_timeunits_grib1_specific(self): ) TestGribTimecodes._run_timetests(self, tests) - def test_timeunits_grib2_specific(self): - tests = ( - (2, 13, None, 1.0, 'seconds'), - # check the extra grib1 keys FAIL - (2, 14, TestGribTimecodes._err_bad_timeunit(14), 0.0, '??'), - (2, 254, TestGribTimecodes._err_bad_timeunit(254), 0.0, '??'), - ) - TestGribTimecodes._run_timetests(self, tests) - def test_timeunits_calendar(self): tests = ( (1, 3, TestGribTimecodes._err_bad_timeunit(3), 0.0, 'months'), @@ -324,55 +316,9 @@ def test_timeunits_calendar(self): def test_timeunits_invalid(self): tests = ( (1, 111, TestGribTimecodes._err_bad_timeunit(111), 1.0, '??'), - (2, 27, TestGribTimecodes._err_bad_timeunit(27), 1.0, '??'), ) TestGribTimecodes._run_timetests(self, tests) - def test_load_probability_forecast(self): - # Test GribWrapper interpretation of PDT 4.9 data. - # NOTE: - # Currently Iris has only partial support for PDT 4.9. - # Though it can load the data, key metadata (thresholds) is lost. - # At present, we are not testing for this. - - # Make a testing grib message in memory, with gribapi. - grib_message = gribapi.grib_new_from_samples('GRIB2') - gribapi.grib_set_long(grib_message, 'productDefinitionTemplateNumber', - 9) - gribapi.grib_set_string(grib_message, 'stepRange', '10-55') - grib_wrapper = iris.fileformats.grib.GribWrapper(grib_message) - - # Define two expected datetimes for _periodEndDateTime as - # gribapi v1.9.16 mis-calculates this. - # See https://software.ecmwf.int/wiki/display/GRIB/\ - # GRIB+API+version+1.9.18+released - try: - # gribapi v1.9.16 has no __version__ attribute. - gribapi_ver = gribapi.__version__ - except AttributeError: - gribapi_ver = gribapi.grib_get_api_version() - - if StrictVersion(gribapi_ver) < StrictVersion('1.9.18'): - exp_end_date = datetime.datetime(year=2007, month=3, day=25, - hour=12, minute=0, second=0) - else: - exp_end_date = datetime.datetime(year=2007, month=3, day=25, - hour=19, minute=0, second=0) - - # Check that it captures the statistics time period info. - # (And for now, nothing else) - self.assertEqual( - grib_wrapper._referenceDateTime, - datetime.datetime(year=2007, month=3, day=23, - hour=12, minute=0, second=0) - ) - self.assertEqual( - grib_wrapper._periodStartDateTime, - datetime.datetime(year=2007, month=3, day=23, - hour=22, minute=0, second=0) - ) - self.assertEqual(grib_wrapper._periodEndDateTime, exp_end_date) - def test_warn_unknown_pdts(self): # Test loading of an unrecognised GRIB Product Definition Template. @@ -389,13 +335,10 @@ def test_warn_unknown_pdts(self): # Load the message from the file as a cube. cube_generator = iris.fileformats.grib.load_cubes( temp_gribfile_path) - cube = next(cube_generator) - - # Check the cube has an extra "warning" attribute. - self.assertEqual( - cube.attributes['GRIB_LOAD_WARNING'], - 'unsupported GRIB2 ProductDefinitionTemplate: #4.5' - ) + with self.assertRaises(iris.exceptions.TranslationError) as te: + cube = next(cube_generator) + self.assertEqual('Product definition template [5]' + ' is not supported', str(te.exception)) @tests.skip_grib @@ -421,7 +364,8 @@ def cube_from_message(self, grib): grib_message = FakeGribMessage(**grib.__dict__) wrapped_msg = iris.fileformats.grib.GribWrapper(grib_message) cube, _, _ = iris.fileformats.rules._make_cube( - wrapped_msg, iris.fileformats.grib.load_rules.convert) + wrapped_msg, + iris.fileformats.grib._grib1_load_rules.grib1_convert) return cube @@ -487,30 +431,15 @@ def mock_grib(self): grib.phenomenon_points = lambda unit: [0.0] return grib - def known_grib2(self, discipline, category, param, - standard_name, long_name, units_str): - grib = self.mock_grib() - grib.discipline = discipline - grib.parameterCategory = category - grib.parameterNumber = param - cube = self.cube_from_message(grib) - try: - _cf_units = cf_units.Unit(units_str) - except ValueError: - _cf_units = cf_units.Unit('???') - self.assertEqual(cube.standard_name, standard_name) - self.assertEqual(cube.long_name, long_name) - self.assertEqual(cube.units, _cf_units) - def test_grib2_unknownparam(self): grib = self.mock_grib() grib.discipline = 999 grib.parameterCategory = 999 grib.parameterNumber = 9999 - cube = self.cube_from_message(grib) - self.assertEqual(cube.standard_name, None) - self.assertEqual(cube.long_name, None) - self.assertEqual(cube.units, cf_units.Unit("???")) + result = gpt.grib2_phenom_to_cf_info(grib.discipline, + grib.parameterCategory, + grib.parameterNumber) + self.assertIsNone(result) def test_grib2_known_standard_params(self): # check we know how to translate at least these params @@ -533,7 +462,8 @@ def test_grib2_known_standard_params(self): (0, 3, 3, None, "icao_standard_atmosphere_reference_height", "m"), (0, 3, 5, "geopotential_height", None, "m"), (0, 3, 9, "geopotential_height_anomaly", None, "m"), - (0, 6, 1, "cloud_area_fraction", None, "%"), + (0, 6, 1, None, + "cloud_area_fraction_assuming_maximum_random_overlap", "1"), (0, 6, 6, "atmosphere_mass_content_of_cloud_liquid_water", None, "kg m-2"), (0, 7, 6, @@ -547,8 +477,12 @@ def test_grib2_known_standard_params(self): for (discipline, category, number, standard_name, long_name, units) in full_set: - self.known_grib2(discipline, category, number, - standard_name, long_name, units) + result = gpt.grib2_phenom_to_cf_info(discipline, + category, + number) + self.assertEqual(result.standard_name, standard_name) + self.assertEqual(result.long_name, long_name) + self.assertEqual(result.units, units) if __name__ == "__main__": diff --git a/lib/iris/tests/test_grib_save.py b/lib/iris/tests/test_grib_save.py index 53c14a169c..95df9c0dfc 100644 --- a/lib/iris/tests/test_grib_save.py +++ b/lib/iris/tests/test_grib_save.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2016, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -34,7 +34,8 @@ if tests.GRIB_AVAILABLE: import gribapi - + from iris.fileformats.grib._load_convert import _MDIs + _mdis = [mdi for mdi in _MDIs] @tests.skip_data @tests.skip_grib @@ -50,10 +51,10 @@ def test_latlon_forecast_plev(self): iris.save(cubes, temp_file_path) expect_diffs = {'totalLength': (4837, 4832), 'productionStatusOfProcessedData': (0, 255), - 'scaleFactorOfRadiusOfSphericalEarth': (4294967295, + 'scaleFactorOfRadiusOfSphericalEarth': (_mdis[1], 0), 'shapeOfTheEarth': (0, 1), - 'scaledValueOfRadiusOfSphericalEarth': (4294967295, + 'scaledValueOfRadiusOfSphericalEarth': (_mdis[1], 6367470), 'typeOfGeneratingProcess': (0, 255), 'generatingProcessIdentifier': (128, 255), @@ -70,10 +71,10 @@ def test_rotated_latlon(self): iris.save(cubes, temp_file_path) expect_diffs = {'totalLength': (648196, 648191), 'productionStatusOfProcessedData': (0, 255), - 'scaleFactorOfRadiusOfSphericalEarth': (4294967295, + 'scaleFactorOfRadiusOfSphericalEarth': (_mdis[1], 0), 'shapeOfTheEarth': (0, 1), - 'scaledValueOfRadiusOfSphericalEarth': (4294967295, + 'scaledValueOfRadiusOfSphericalEarth': (_mdis[1], 6367470), 'longitudeOfLastGridPoint': (392109982, 32106370), 'latitudeOfLastGridPoint': (19419996, 19419285), @@ -91,10 +92,10 @@ def test_time_mean(self): cubes = iris.load(source_grib) expect_diffs = {'totalLength': (21232, 21227), 'productionStatusOfProcessedData': (0, 255), - 'scaleFactorOfRadiusOfSphericalEarth': (4294967295, + 'scaleFactorOfRadiusOfSphericalEarth': (_mdis[1], 0), 'shapeOfTheEarth': (0, 1), - 'scaledValueOfRadiusOfSphericalEarth': (4294967295, + 'scaledValueOfRadiusOfSphericalEarth': (_mdis[1], 6367470), 'longitudeOfLastGridPoint': (356249908, 356249809), 'latitudeOfLastGridPoint': (-89999938, -89999944), diff --git a/lib/iris/tests/unit/fileformats/grib/__init__.py b/lib/iris/tests/unit/fileformats/grib/__init__.py index 970757abcb..6e0dcadb8e 100644 --- a/lib/iris/tests/unit/fileformats/grib/__init__.py +++ b/lib/iris/tests/unit/fileformats/grib/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2016, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -19,6 +19,15 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +# import iris.tests first so that some things can be initialised +# before importing anything else. +import iris.tests as tests + +import gribapi +import numpy as np + +import iris +import iris.fileformats.grib from iris.fileformats.grib.message import GribMessage from iris.tests import mock @@ -27,3 +36,172 @@ def _make_test_message(sections): raw_message = mock.Mock(sections=sections) recreate_raw = mock.Mock(return_value=raw_message) return GribMessage(raw_message, recreate_raw) + + +def _mock_gribapi_fetch(message, key): + """ + Fake the gribapi key-fetch. + + Fetch key-value from the fake message (dictionary). + If the key is not present, raise the diagnostic exception. + + """ + if key in message: + return message[key] + else: + raise _mock_gribapi.GribInternalError + + +def _mock_gribapi__grib_is_missing(grib_message, keyname): + """ + Fake the gribapi key-existence enquiry. + + Return whether the key exists in the fake message (dictionary). + + """ + return (keyname not in grib_message) + + +def _mock_gribapi__grib_get_native_type(grib_message, keyname): + """ + Fake the gribapi type-discovery operation. + + Return type of key-value in the fake message (dictionary). + If the key is not present, raise the diagnostic exception. + + """ + if keyname in grib_message: + return type(grib_message[keyname]) + raise _mock_gribapi.GribInternalError(keyname) + + +# Construct a mock object to mimic the gribapi for GribWrapper testing. +_mock_gribapi = mock.Mock(spec=gribapi) +_mock_gribapi.GribInternalError = Exception + +_mock_gribapi.grib_get_long = mock.Mock(side_effect=_mock_gribapi_fetch) +_mock_gribapi.grib_get_string = mock.Mock(side_effect=_mock_gribapi_fetch) +_mock_gribapi.grib_get_double = mock.Mock(side_effect=_mock_gribapi_fetch) +_mock_gribapi.grib_get_double_array = mock.Mock( + side_effect=_mock_gribapi_fetch) +_mock_gribapi.grib_is_missing = mock.Mock( + side_effect=_mock_gribapi__grib_is_missing) +_mock_gribapi.grib_get_native_type = mock.Mock( + side_effect=_mock_gribapi__grib_get_native_type) + + +class FakeGribMessage(dict): + """ + A 'fake grib message' object, for testing GribWrapper construction. + + Behaves as a dictionary, containing key-values for message keys. + + """ + def __init__(self, **kwargs): + """ + Create a fake message object. + + General keys can be set/add as required via **kwargs. + The 'time_code' key is specially managed. + + """ + # Start with a bare dictionary + dict.__init__(self) + # Extract specially-recognised keys. + time_code = kwargs.pop('time_code', None) + # Set the minimally required keys. + self._init_minimal_message() + # Also set a time-code, if given. + if time_code is not None: + self.set_timeunit_code(time_code) + # Finally, add any remaining passed key-values. + self.update(**kwargs) + + def _init_minimal_message(self): + # Set values for all the required keys. + self.update({ + 'edition': 1, + 'Ni': 1, + 'Nj': 1, + 'numberOfValues': 1, + 'alternativeRowScanning': 0, + 'centre': 'ecmf', + 'year': 2007, + 'month': 3, + 'day': 23, + 'hour': 12, + 'minute': 0, + 'indicatorOfUnitOfTimeRange': 1, + 'shapeOfTheEarth': 6, + 'gridType': 'rotated_ll', + 'angleOfRotation': 0.0, + 'iDirectionIncrementInDegrees': 0.036, + 'jDirectionIncrementInDegrees': 0.036, + 'iScansNegatively': 0, + 'jScansPositively': 1, + 'longitudeOfFirstGridPointInDegrees': -5.70, + 'latitudeOfFirstGridPointInDegrees': -4.452, + 'jPointsAreConsecutive': 0, + 'values': np.array([[1.0]]), + 'indicatorOfParameter': 9999, + 'parameterNumber': 9999, + 'startStep': 24, + 'timeRangeIndicator': 1, + 'P1': 2, 'P2': 0, + # time unit - needed AS WELL as 'indicatorOfUnitOfTimeRange' + 'unitOfTime': 1, + 'table2Version': 9999, + }) + + def set_timeunit_code(self, timecode): + self['indicatorOfUnitOfTimeRange'] = timecode + # for some odd reason, GRIB1 code uses *both* of these + # NOTE kludge -- the 2 keys are really the same thing + self['unitOfTime'] = timecode + + +class TestField(tests.IrisTest): + def _test_for_coord(self, field, convert, coord_predicate, expected_points, + expected_bounds): + (factories, references, standard_name, long_name, units, + attributes, cell_methods, dim_coords_and_dims, + aux_coords_and_dims) = convert(field) + + # Check for one and only one matching coordinate. + coords_and_dims = dim_coords_and_dims + aux_coords_and_dims + matching_coords = [coord for coord, _ in coords_and_dims if + coord_predicate(coord)] + self.assertEqual(len(matching_coords), 1, str(matching_coords)) + coord = matching_coords[0] + + # Check points and bounds. + if expected_points is not None: + self.assertArrayEqual(coord.points, expected_points) + + if expected_bounds is None: + self.assertIsNone(coord.bounds) + else: + self.assertArrayEqual(coord.bounds, expected_bounds) + + def assertCoordsAndDimsListsMatch(self, coords_and_dims_got, + coords_and_dims_expected): + """ + Check that coords_and_dims lists are equivalent. + + The arguments are lists of pairs of (coordinate, dimensions). + The elements are compared one-to-one, by coordinate name (so the order + of the lists is _not_ significant). + It also checks that the coordinate types (DimCoord/AuxCoord) match. + + """ + def sorted_by_coordname(list): + return sorted(list, key=lambda item: item[0].name()) + + coords_and_dims_got = sorted_by_coordname(coords_and_dims_got) + coords_and_dims_expected = sorted_by_coordname( + coords_and_dims_expected) + self.assertEqual(coords_and_dims_got, coords_and_dims_expected) + # Also check coordinate type equivalences (as Coord.__eq__ does not). + self.assertEqual( + [type(coord) for coord, dims in coords_and_dims_got], + [type(coord) for coord, dims in coords_and_dims_expected]) diff --git a/lib/iris/tests/unit/fileformats/grib/load_rules/__init__.py b/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/__init__.py similarity index 85% rename from lib/iris/tests/unit/fileformats/grib/load_rules/__init__.py rename to lib/iris/tests/unit/fileformats/grib/grib1_load_rules/__init__.py index 39a7f0e0a5..946065ac18 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_rules/__init__.py +++ b/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . -"""Unit tests for the :mod:`iris.fileformats.grib.load_rules` module.""" +"""Unit tests for the :mod:`iris.fileformats.grib._grib1_load_rules` module.""" from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa diff --git a/lib/iris/tests/unit/fileformats/grib/load_rules/test_convert.py b/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/test_grib1_convert.py similarity index 65% rename from lib/iris/tests/unit/fileformats/grib/load_rules/test_convert.py rename to lib/iris/tests/unit/fileformats/grib/grib1_load_rules/test_grib1_convert.py index cd53ebf88f..19ea6ab80e 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_rules/test_convert.py +++ b/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/test_grib1_convert.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2016, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -14,12 +14,14 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . -"""Unit tests for :func:`iris.fileformats.grib.load_rules.convert`.""" +""" +Unit tests for :func:`iris.fileformats.grib._grib1_load_rules.grib1_convert`. +""" from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# Import iris tests first so that some things can be initialised before +# Import iris.tests first so that some things can be initialised before # importing anything else import iris.tests as tests @@ -27,57 +29,20 @@ import gribapi import iris +from iris.exceptions import TranslationError +from iris.fileformats.grib import GribWrapper +from iris.fileformats.grib._grib1_load_rules import grib1_convert from iris.fileformats.rules import Reference +from iris.tests.unit.fileformats.grib import TestField from iris.tests import mock -from iris.tests.test_grib_load_translations import TestGribSimple -from iris.tests.unit.fileformats import TestField -from iris.fileformats.grib import GribWrapper -from iris.fileformats.grib.load_rules import convert - - -class Test_GribLevels_Mock(TestGribSimple): - # Unit test levels with mocking. - def test_grib2_height(self): - grib = self.mock_grib() - grib.edition = 2 - grib.typeOfFirstFixedSurface = 103 - grib.scaledValueOfFirstFixedSurface = 12345 - grib.scaleFactorOfFirstFixedSurface = 0 - grib.typeOfSecondFixedSurface = 255 - cube = self.cube_from_message(grib) - self.assertEqual( - cube.coord('height'), - iris.coords.DimCoord(12345, standard_name="height", units="m")) - - def test_grib2_bounded_height(self): - grib = self.mock_grib() - grib.edition = 2 - grib.typeOfFirstFixedSurface = 103 - grib.scaledValueOfFirstFixedSurface = 12345 - grib.scaleFactorOfFirstFixedSurface = 0 - grib.typeOfSecondFixedSurface = 103 - grib.scaledValueOfSecondFixedSurface = 54321 - grib.scaleFactorOfSecondFixedSurface = 0 - cube = self.cube_from_message(grib) - self.assertEqual( - cube.coord('height'), - iris.coords.DimCoord(33333, standard_name="height", units="m", - bounds=[[12345, 54321]])) - - def test_grib2_diff_bound_types(self): - grib = self.mock_grib() - grib.edition = 2 - grib.typeOfFirstFixedSurface = 103 - grib.scaledValueOfFirstFixedSurface = 12345 - grib.scaleFactorOfFirstFixedSurface = 0 - grib.typeOfSecondFixedSurface = 102 - grib.scaledValueOfSecondFixedSurface = 54321 - grib.scaleFactorOfSecondFixedSurface = 0 - with mock.patch('warnings.warn') as warn: - cube = self.cube_from_message(grib) - warn.assert_called_with( - "Different vertical bound types not yet handled.") + +class TestBadEdition(tests.IrisTest): + def test(self): + message = mock.Mock(edition=2) + emsg = 'GRIB edition 2 is not supported' + with self.assertRaisesRegexp(TranslationError, emsg): + grib1_convert(message) class TestBoundedTime(TestField): @@ -100,10 +65,10 @@ def assert_bounded_message(self, **kwargs): 'table2Version': 9999} attributes.update(kwargs) message = mock.Mock(**attributes) - self._test_for_coord(message, convert, self.is_forecast_period, + self._test_for_coord(message, grib1_convert, self.is_forecast_period, expected_points=[35], expected_bounds=[[15, 55]]) - self._test_for_coord(message, convert, self.is_time, + self._test_for_coord(message, grib1_convert, self.is_time, expected_points=[100], expected_bounds=[[80, 120]]) @@ -149,20 +114,12 @@ def test_time_range_indicator_124(self): def test_time_range_indicator_125(self): self.assert_bounded_message(timeRangeIndicator=125) - def test_product_template_8(self): - self.assert_bounded_message(edition=2, - productDefinitionTemplateNumber=8) - - def test_product_template_9(self): - self.assert_bounded_message(edition=2, - productDefinitionTemplateNumber=9) - class Test_GribLevels(tests.IrisTest): def test_grib1_hybrid_height(self): gm = gribapi.grib_new_from_samples('regular_gg_ml_grib1') gw = GribWrapper(gm) - results = convert(gw) + results = grib1_convert(gw) factory, = results[0] self.assertEqual(factory.factory_class, diff --git a/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/__init__.py b/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/__init__.py new file mode 100644 index 0000000000..0bc5a8a92b --- /dev/null +++ b/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/__init__.py @@ -0,0 +1,21 @@ +# (C) British Crown Copyright 2014 - 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the +:mod:`iris.fileformats.grib.grib_phenom_translation` package.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa diff --git a/lib/iris/tests/test_grib_phenomenon_translations.py b/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/test_grib_phenom_translation.py similarity index 94% rename from lib/iris/tests/test_grib_phenomenon_translations.py rename to lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/test_grib_phenom_translation.py index d10e145387..c4b9570e2d 100644 --- a/lib/iris/tests/test_grib_phenomenon_translations.py +++ b/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/test_grib_phenom_translation.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -15,29 +15,24 @@ # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . ''' -Created on Apr 26, 2013 +Unit tests for the mod:`iris.fileformats.grib.grib_phenom_translation` module. -@author: itpp ''' - from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# Import iris tests first so that some things can be initialised before -# importing anything else +# Import iris.tests first so that some things can be initialised before +# importing anything else. import iris.tests as tests import cf_units -if tests.GRIB_AVAILABLE: - import gribapi - import iris.fileformats.grib.grib_phenom_translation as gptx +import iris.fileformats.grib.grib_phenom_translation as gptx -@tests.skip_grib class TestGribLookupTableType(tests.IrisTest): def test_lookuptable_type(self): - ll = gptx.LookupTable([('a', 1), ('b', 2)]) + ll = gptx._LookupTable([('a', 1), ('b', 2)]) assert ll['a'] == 1 assert ll['q'] is None ll['q'] = 15 @@ -51,7 +46,6 @@ def test_lookuptable_type(self): assert ll['q'] == 7 -@tests.skip_grib class TestGribPhenomenonLookup(tests.IrisTest): def test_grib1_cf_lookup(self): def check_grib1_cf(param, diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py b/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py index b6afad2e24..60b499cd66 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -19,7 +19,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test__hindcast_fix.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test__hindcast_fix.py index 186a431074..69dcc9259e 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test__hindcast_fix.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test__hindcast_fix.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -56,8 +56,8 @@ def test_fix(self): def test_fix_warning(self): # Check warning appears when enabled. - self.patch('iris.fileformats.grib._load_convert.options' - '.warn_on_unsupported', True) + pm = 'iris.fileformats.grib._load_convert.options.warn_on_unsupported' + self.patch(pm, True) hindcast_fix(2 * 2**30 + 5) self.assertEqual(self.patch_warn.call_count, 1) self.assertIn('Re-interpreting large grib forecastTime', diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_bitmap_section.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_bitmap_section.py index dbfef702c5..f7fa1b2896 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_bitmap_section.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_bitmap_section.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_convert.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_convert.py index 2ab05af4f4..a5cbc5e248 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_convert.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_convert.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -19,7 +19,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -29,16 +29,15 @@ from iris.tests.unit.fileformats.grib import _make_test_message -class Test(tests.IrisTest): - def test_call(self): +class TestGribMessage(tests.IrisTest): + def test_edition_2(self): + def func(field, metadata): + return metadata['factories'].append(factory) + sections = [{'editionNumber': 2}] field = _make_test_message(sections) this = 'iris.fileformats.grib._load_convert.grib2_convert' factory = mock.sentinel.factory - - def func(field, metadata): - return metadata['factories'].append(factory) - with mock.patch(this, side_effect=func) as grib2_convert: # The call being tested. result = convert(field) @@ -46,13 +45,32 @@ def func(field, metadata): metadata = ([factory], [], None, None, None, {}, [], [], []) self.assertEqual(result, metadata) - def test_edition_1(self): + def test_edition_1_bad(self): sections = [{'editionNumber': 1}] field = _make_test_message(sections) - with self.assertRaisesRegexp(TranslationError, - 'edition 1 is not supported'): + emsg = 'edition 1 is not supported' + with self.assertRaisesRegexp(TranslationError, emsg): convert(field) +class TestGribWrapper(tests.IrisTest): + def test_edition_2_bad(self): + # Test object with no '.sections', and '.edition' ==2. + field = mock.Mock(edition=2, spec=('edition')) + emsg = 'edition 2 is not supported' + with self.assertRaisesRegexp(TranslationError, emsg): + convert(field) + + def test_edition_1(self): + # Test object with no '.sections', and '.edition' ==1. + field = mock.Mock(edition=1, spec=('edition')) + func = 'iris.fileformats.grib._load_convert.grib1_convert' + metadata = mock.sentinel.metadata + with mock.patch(func, return_value=metadata) as grib1_convert: + result = convert(field) + grib1_convert.assert_called_once_with(field) + self.assertEqual(result, metadata) + + if __name__ == '__main__': tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_data_cutoff.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_data_cutoff.py index 729d2b9b30..4090a12535 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_data_cutoff.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_data_cutoff.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,14 +22,16 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests -from iris.fileformats.grib._load_convert import _MDI as MDI +from iris.fileformats.grib._load_convert import _MDIs from iris.fileformats.grib._load_convert import data_cutoff from iris.tests import mock +MDI = [mdi for mdi in _MDIs][1] + class TestDataCutoff(tests.IrisTest): def _check(self, hours, minutes, request_warning, expect_warning=False): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid.py index 3c3ce51026..dde4be0f22 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid_geometry.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid_geometry.py index 25252da835..a14fa3d90b 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid_geometry.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid_geometry.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ensemble_identifier.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_ensemble_identifier.py index 8dea616fc8..cc31ccd638 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ensemble_identifier.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_ensemble_identifier.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_float32_from_int32.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_float32_from_int32.py index 4d98ffdfb9..ee84fd007a 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_float32_from_int32.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_float32_from_int32.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_int32_from_uint32.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_int32_from_uint32.py index f1144f14fc..91ab71cab6 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_int32_from_uint32.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_int32_from_uint32.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_forecast_period_coord.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_forecast_period_coord.py index 3c516a1bff..866c03dbb4 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_forecast_period_coord.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_forecast_period_coord.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_generating_process.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_generating_process.py index fb5942f3ff..ee6ab0972f 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_generating_process.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_generating_process.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grib2_convert.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grib2_convert.py index 1e785eb248..d1590abe31 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grib2_convert.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grib2_convert.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -19,13 +19,13 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests import copy -import iris +import iris.fileformats.grib from iris.fileformats.grib._load_convert import grib2_convert from iris.tests import mock from iris.tests.unit.fileformats.grib import _make_test_message diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_0_and_1.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_0_and_1.py index 0d0e4bc9ac..c22bc8d5b1 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_0_and_1.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_0_and_1.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015, Met Office +# (C) British Crown Copyright 2015 - 2017, Met Office # # This file is part of Iris. # @@ -23,13 +23,13 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# Import iris tests first so that some things can be initialised +# Import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import \ - grid_definition_template_0_and_1 +from iris.fileformats.grib._load_convert \ + import grid_definition_template_0_and_1 class Test(tests.IrisTest): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_12.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_12.py index a9a5346344..4b59e3de44 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_12.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_12.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -32,8 +32,8 @@ import iris.coord_systems import iris.coords import iris.exceptions -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata from iris.fileformats.grib._load_convert import grid_definition_template_12 +from iris.tests.unit.fileformats.grib.load_convert import empty_metadata MDI = 2 ** 32 - 1 diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_20.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_20.py index b22bab70be..1389a42ce3 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_20.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_20.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -31,8 +31,8 @@ import iris.coord_systems import iris.coords -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata from iris.fileformats.grib._load_convert import grid_definition_template_20 +from iris.tests.unit.fileformats.grib.load_convert import empty_metadata MDI = 2 ** 32 - 1 diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_30.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_30.py index 515cf95896..8114fa8f68 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_30.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_30.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -31,8 +31,8 @@ import iris.coord_systems import iris.coords -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata from iris.fileformats.grib._load_convert import grid_definition_template_30 +from iris.tests.unit.fileformats.grib.load_convert import empty_metadata MDI = 2 ** 32 - 1 diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_40.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_40.py index ddddb8190c..c34756ed2c 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_40.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_40.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015 - 2016, Met Office +# (C) British Crown Copyright 2015 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -31,8 +31,8 @@ import iris.coord_systems import iris.coords -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata from iris.fileformats.grib._load_convert import grid_definition_template_40 +from iris.tests.unit.fileformats.grib.load_convert import empty_metadata MDI = 2 ** 32 - 1 diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_4_and_5.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_4_and_5.py index ab78dfd041..ac263721b9 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_4_and_5.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_4_and_5.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -34,21 +34,19 @@ from iris.coords import DimCoord from iris.fileformats.grib._load_convert import \ - grid_definition_template_4_and_5, \ - _MDI as MDI + grid_definition_template_4_and_5, _MDIs from iris.tests import mock +MDI = [mdi for mdi in _MDIs][1] RESOLUTION = 1e6 class Test(tests.IrisTest): def setUp(self): - patch = [] - patch.append(mock.patch('warnings.warn')) - module = 'iris.fileformats.grib._load_convert' - this = '{}._is_circular'.format(module) - patch.append(mock.patch(this, return_value=False)) + self.patch('warnings.warn') + self.patch('iris.fileformats.grib._load_convert._is_circular', + return_value=False) self.metadata = {'factories': [], 'references': [], 'standard_name': None, 'long_name': None, 'units': None, 'attributes': {}, @@ -56,9 +54,6 @@ def setUp(self): 'aux_coords_and_dims': []} self.cs = mock.sentinel.coord_system self.data = np.arange(10, dtype=np.float64) - for p in patch: - p.start() - self.addCleanup(p.stop) def _check(self, section, request_warning, expect_warning=False, y_dim=0, x_dim=1): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_5.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_5.py index e799bf4d72..ae63540cbd 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_5.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_5.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -35,37 +35,38 @@ class Test(tests.IrisTest): def setUp(self): + def func(s, m, y, x, c): + return m['dim_coords_and_dims'].append(item) + module = 'iris.fileformats.grib._load_convert' - patch = [] + self.major = mock.sentinel.major self.minor = mock.sentinel.minor self.radius = mock.sentinel.radius - this = '{}.ellipsoid_geometry'.format(module) + + mfunc = '{}.ellipsoid_geometry'.format(module) return_value = (self.major, self.minor, self.radius) - patch.append(mock.patch(this, return_value=return_value)) - this = '{}.ellipsoid'.format(module) + self.patch(mfunc, return_value=return_value) + + mfunc = '{}.ellipsoid'.format(module) self.ellipsoid = mock.sentinel.ellipsoid - patch.append(mock.patch(this, return_value=self.ellipsoid)) - this = '{}.grid_definition_template_4_and_5'.format(module) + self.patch(mfunc, return_value=self.ellipsoid) + + mfunc = '{}.grid_definition_template_4_and_5'.format(module) self.coord = mock.sentinel.coord self.dim = mock.sentinel.dim item = (self.coord, self.dim) + self.patch(mfunc, side_effect=func) - def func(s, m, y, x, c): - return m['dim_coords_and_dims'].append(item) - patch.append(mock.patch(this, side_effect=func)) - - this = 'iris.coord_systems.RotatedGeogCS' + mclass = 'iris.coord_systems.RotatedGeogCS' self.cs = mock.sentinel.cs - patch.append(mock.patch(this, return_value=self.cs)) + self.patch(mclass, return_value=self.cs) + self.metadata = {'factories': [], 'references': [], 'standard_name': None, 'long_name': None, 'units': None, 'attributes': {}, 'cell_methods': [], 'dim_coords_and_dims': [], 'aux_coords_and_dims': []} - for p in patch: - p.start() - self.addCleanup(p.stop) def test(self): metadata = deepcopy(self.metadata) diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_90.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_90.py index 04883dd990..4501cd028b 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_90.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_90.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -24,7 +24,7 @@ from six.moves import (filter, input, map, range, zip) # noqa import six -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -33,8 +33,8 @@ import iris.coord_systems import iris.coords import iris.exceptions -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata from iris.fileformats.grib._load_convert import grid_definition_template_90 +from iris.tests.unit.fileformats.grib.load_convert import empty_metadata MDI = 2 ** 32 - 1 diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_other_time_coord.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_other_time_coord.py index 4b47d99c4f..1ae013c410 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_other_time_coord.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_other_time_coord.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_0.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_0.py index 649aa3c2b6..ae40e215a6 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_0.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_0.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,15 +23,16 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests import iris.coords -from iris.tests.unit.fileformats.grib.load_convert import (LoadConvertTest, - empty_metadata) +import iris.fileformats.grib from iris.fileformats.grib._load_convert import product_definition_template_0 from iris.tests import mock +from iris.tests.unit.fileformats.grib.load_convert import (LoadConvertTest, + empty_metadata) MDI = 0xffffffff diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_1.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_1.py index d8c8f5344e..012107f146 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_1.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_1.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -37,15 +37,14 @@ class Test(tests.IrisTest): def setUp(self): + def func(s, m, f): + return m['cell_methods'].append(self.cell_method) + module = 'iris.fileformats.grib._load_convert' self.patch('warnings.warn') this = '{}.product_definition_template_0'.format(module) self.cell_method = mock.sentinel.cell_method - - def func(s, m, f): - return m['cell_methods'].append(self.cell_method) self.patch(this, side_effect=func) - self.metadata = {'factories': [], 'references': [], 'standard_name': None, 'long_name': None, 'units': None, 'attributes': {}, diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_10.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_10.py new file mode 100644 index 0000000000..95ce05d461 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_10.py @@ -0,0 +1,82 @@ +# (C) British Crown Copyright 2016 - 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +""" +Test function +:func:`iris.fileformats.grib._load_convert.product_definition_template_10`. + +""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# import iris.tests first so that some things can be initialised +# before importing anything else. +import iris.tests as tests + +from copy import deepcopy + +from iris.coords import DimCoord +from iris.fileformats.grib._load_convert import product_definition_template_10 +from iris.tests import mock +from iris.tests.unit.fileformats.grib.load_convert import empty_metadata + + +class Test(tests.IrisTest): + def setUp(self): + module = 'iris.fileformats.grib._load_convert' + this_module = '{}.product_definition_template_10'.format(module) + self.patch_statistical_fp_coord = self.patch( + module + '.statistical_forecast_period_coord', + return_value=mock.sentinel.dummy_fp_coord) + self.patch_time_coord = self.patch( + module + '.validity_time_coord', + return_value=mock.sentinel.dummy_time_coord) + self.patch_vertical_coords = self.patch(module + '.vertical_coords') + + def test_percentile_coord(self): + metadata = empty_metadata() + percentileValue = 75 + section = {'percentileValue': percentileValue, + 'hoursAfterDataCutoff': 1, + 'minutesAfterDataCutoff': 1, + 'numberOfTimeRange': 1, + 'typeOfStatisticalProcessing': 1, + 'typeOfTimeIncrement': 2, + 'timeIncrement': 0, + 'yearOfEndOfOverallTimeInterval': 2000, + 'monthOfEndOfOverallTimeInterval': 1, + 'dayOfEndOfOverallTimeInterval': 1, + 'hourOfEndOfOverallTimeInterval': 1, + 'minuteOfEndOfOverallTimeInterval': 0, + 'secondOfEndOfOverallTimeInterval': 1} + forecast_reference_time = mock.Mock() + # The called being tested. + product_definition_template_10(section, metadata, + forecast_reference_time) + + expected = {'aux_coords_and_dims': []} + percentile = DimCoord(percentileValue, + long_name='percentile_over_time', + units='no_unit') + expected['aux_coords_and_dims'].append((percentile, None)) + + self.assertEqual(metadata['aux_coords_and_dims'][-1], + expected['aux_coords_and_dims'][0]) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_11.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_11.py index a20cb4cf75..beebb9632d 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_11.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_11.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -23,7 +23,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -37,15 +37,14 @@ class Test(tests.IrisTest): def setUp(self): + def func(s, m, f): + return m['cell_methods'].append(self.cell_method) + module = 'iris.fileformats.grib._load_convert' self.patch('warnings.warn') this_module = '{}.product_definition_template_11'.format(module) self.cell_method = mock.sentinel.cell_method - - def func(s, m, f): - return m['cell_methods'].append(self.cell_method) self.patch(this_module, side_effect=func) - self.patch_statistical_fp_coord = self.patch( module + '.statistical_forecast_period_coord', return_value=mock.sentinel.dummy_fp_coord) @@ -77,7 +76,7 @@ def _check(self, request_warning): 'hourOfEndOfOverallTimeInterval': 1, 'minuteOfEndOfOverallTimeInterval': 0, 'secondOfEndOfOverallTimeInterval': 1} - forecast_reference_time = mock.sentinel.forecast_reference_time + forecast_reference_time = mock.Mock() # The called being tested. product_definition_template_11(section, metadata, forecast_reference_time) diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_31.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_31.py index 59f6f327f8..fc0c117bfd 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_31.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_31.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -38,9 +38,7 @@ class Test(tests.IrisTest): def setUp(self): - patch = mock.patch('warnings.warn') - patch.start() - self.addCleanup(patch.stop) + self.patch('warnings.warn') self.metadata = {'factories': [], 'references': [], 'standard_name': None, 'long_name': None, 'units': None, 'attributes': None, @@ -49,6 +47,9 @@ def setUp(self): def _check(self, request_warning=False, value=10, factor=1): # Prepare the arguments. + def unscale(v, f): + return v / 10.0 ** f + series = mock.sentinel.satelliteSeries number = mock.sentinel.satelliteNumber instrument = mock.sentinel.instrumentType @@ -64,11 +65,7 @@ def _check(self, request_warning=False, value=10, factor=1): with mock.patch(this, warn_on_unsupported=request_warning): # The call being tested. product_definition_template_31(section, metadata, rt_coord) - # Check the result. - def unscale(v, f): - return v / 10.0 ** f - expected = deepcopy(self.metadata) coord = AuxCoord(series, long_name='satellite_series') expected['aux_coords_and_dims'].append((coord, None)) diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_40.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_40.py index 5625be870e..fa9805ac4a 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_40.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_40.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -23,37 +23,36 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests import iris.coords -from iris.fileformats.grib._load_convert import product_definition_template_40 +from iris.fileformats.grib._load_convert import \ + product_definition_template_40, _MDIs from iris.tests.unit.fileformats.grib.load_convert import empty_metadata - -MDI = 0xffffffff - - -def section_4(): - return {'hoursAfterDataCutoff': MDI, - 'minutesAfterDataCutoff': MDI, - 'constituentType': 1, - 'indicatorOfUnitOfTimeRange': 0, # minutes - 'startStep': 360, - 'NV': 0, - 'typeOfFirstFixedSurface': 103, - 'scaleFactorOfFirstFixedSurface': 0, - 'scaledValueOfFirstFixedSurface': 9999, - 'typeOfSecondFixedSurface': 255} +MDI = [mdi for mdi in _MDIs][1] class Test(tests.IrisTest): + def setUp(self): + self.section_4 = {'hoursAfterDataCutoff': MDI, + 'minutesAfterDataCutoff': MDI, + 'constituentType': 1, + 'indicatorOfUnitOfTimeRange': 0, # minutes + 'startStep': 360, + 'NV': 0, + 'typeOfFirstFixedSurface': 103, + 'scaleFactorOfFirstFixedSurface': 0, + 'scaledValueOfFirstFixedSurface': 9999, + 'typeOfSecondFixedSurface': 255} + def test_constituent_type(self): metadata = empty_metadata() rt_coord = iris.coords.DimCoord(24, 'forecast_reference_time', units='hours since epoch') - product_definition_template_40(section_4(), metadata, rt_coord) + product_definition_template_40(self.section_4, metadata, rt_coord) expected = empty_metadata() expected['attributes']['WMO_constituent_type'] = 1 self.assertEqual(metadata['attributes'], expected['attributes']) diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_8.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_8.py index fb0d550813..eeb203aed9 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_8.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_8.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -24,7 +24,7 @@ from six.moves import (filter, input, map, range, zip) # noqa import six -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -54,7 +54,7 @@ def setUp(self): self.section = {} self.section['hoursAfterDataCutoff'] = mock.sentinel.cutoff_hours self.section['minutesAfterDataCutoff'] = mock.sentinel.cutoff_mins - self.frt_coord = mock.sentinel.frt_coord + self.frt_coord = mock.Mock() self.metadata = {'cell_methods': [], 'aux_coords_and_dims': []} def test_basic(self): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_9.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_9.py index e1aede2dbf..c00e527334 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_9.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_9.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,15 +23,17 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests from iris.exceptions import TranslationError from iris.fileformats.grib._load_convert import product_definition_template_9 -from iris.fileformats.grib._load_convert import Probability, _MDI +from iris.fileformats.grib._load_convert import Probability, _MDIs from iris.tests import mock +MDI = [mdi for mdi in _MDIs][1] + class Test(tests.IrisTest): def setUp(self): @@ -69,14 +71,14 @@ def test_fail_bad_probability_type(self): self.section, self.metadata, self.frt_coord) def test_fail_bad_threshold_value(self): - self.section['scaledValueOfUpperLimit'] = _MDI + self.section['scaledValueOfUpperLimit'] = MDI with self.assertRaisesRegexp(TranslationError, 'missing scaled value'): product_definition_template_9( self.section, self.metadata, self.frt_coord) def test_fail_bad_threshold_scalefactor(self): - self.section['scaleFactorOfUpperLimit'] = _MDI + self.section['scaleFactorOfUpperLimit'] = MDI with self.assertRaisesRegexp(TranslationError, 'missing scale factor'): product_definition_template_9( diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_projection_centre.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_projection_centre.py index 8426655fab..780ffe8868 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_projection_centre.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_projection_centre.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_reference_time_coord.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_reference_time_coord.py index d8dcdd7a03..05e30c2a36 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_reference_time_coord.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_reference_time_coord.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -24,7 +24,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -59,7 +59,12 @@ def _check(self, section, standard_name=None): coord = reference_time_coord(section) self.assertEqual(coord, expected) - def test_start_of_forecast(self): + def test_start_of_forecast0(self): + section = deepcopy(self.section) + section['significanceOfReferenceTime'] = 0 + self._check(section, 'forecast_reference_time') + + def test_start_of_forecast1(self): section = deepcopy(self.section) section['significanceOfReferenceTime'] = 1 self._check(section, 'forecast_reference_time') diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_resolution_flags.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_resolution_flags.py index 29713cffc5..22681a24e1 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_resolution_flags.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_resolution_flags.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,12 +22,12 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests -from iris.fileformats.grib._load_convert import \ - resolution_flags, ResolutionFlags +from iris.fileformats.grib._load_convert import (resolution_flags, + ResolutionFlags) class Test(tests.IrisTest): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_scanning_mode.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_scanning_mode.py index 97cbe69d4e..54724adb19 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_scanning_mode.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_scanning_mode.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_cell_method.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_cell_method.py index e89f998ed8..8ed2a26fc4 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_cell_method.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_cell_method.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,12 +23,11 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests from iris.exceptions import TranslationError - from iris.fileformats.grib._load_convert import statistical_cell_method diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_forecast_period_coord.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_forecast_period_coord.py index 4bf3fb4a61..bc9e71a265 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_forecast_period_coord.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_forecast_period_coord.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -23,14 +23,14 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests import datetime from iris.fileformats.grib._load_convert import \ - statistical_forecast_period_coord + statistical_forecast_period_coord from iris.tests import mock diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_time_range_unit.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_time_range_unit.py index 648a68d587..a708e68460 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_time_range_unit.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_time_range_unit.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_translate_phenomenon.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_translate_phenomenon.py index ea8cee55db..8fd2e32911 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_translate_phenomenon.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_translate_phenomenon.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -15,15 +15,15 @@ # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . """ -Tests for function -:func:`iris.fileformats.grib._load_convert.translate_phenomenon`. +Tests for +function :func:`iris.fileformats.grib._load_convert.translate_phenomenon`. """ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -32,11 +32,10 @@ from cf_units import Unit from iris.coords import DimCoord -from iris.fileformats.grib._load_convert import Probability +from iris.fileformats.grib._load_convert import (Probability, + translate_phenomenon) from iris.fileformats.grib.grib_phenom_translation import _GribToCfDataClass -from iris.fileformats.grib._load_convert import translate_phenomenon - class Test_probability(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_unscale.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_unscale.py index 1f219477f4..c0005a2410 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_unscale.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_unscale.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,16 +22,17 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests import numpy as np import numpy.ma as ma -from iris.fileformats.grib._load_convert import unscale, _MDI as MDI +from iris.fileformats.grib._load_convert import unscale, _MDIs # Reference GRIB2 Regulation 92.1.12. +MDI = [mdi for mdi in _MDIs][1] class Test(tests.IrisTest): diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_validity_time_coord.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_validity_time_coord.py index 9d9cc9858f..1e2de6af88 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_validity_time_coord.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_validity_time_coord.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_vertical_coords.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_vertical_coords.py index 51f5c68407..dd82dbfd65 100644 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_vertical_coords.py +++ b/lib/iris/tests/unit/fileformats/grib/load_convert/test_vertical_coords.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -22,7 +22,7 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised +# import iris.tests first so that some things can be initialised # before importing anything else. import iris.tests as tests @@ -33,9 +33,11 @@ from iris.fileformats.grib._load_convert import vertical_coords from iris.fileformats.grib._load_convert import \ _TYPE_OF_FIXED_SURFACE_MISSING as MISSING_SURFACE, \ - _MDI as MISSING_LEVEL + _MDIs from iris.tests import mock +MISSING_LEVEL = [mdi for mdi in _MDIs][1] + class Test(tests.IrisTest): def setUp(self): @@ -46,14 +48,13 @@ def setUp(self): 'aux_coords_and_dims': []} def test_hybrid_factories(self): + def func(section, metadata): + return metadata['factories'].append(factory) + metadata = deepcopy(self.metadata) section = {'NV': 1} this = 'iris.fileformats.grib._load_convert.hybrid_factories' factory = mock.sentinel.factory - - def func(section, metadata): - return metadata['factories'].append(factory) - with mock.patch(this, side_effect=func) as hybrid_factories: vertical_coords(section, metadata) self.assertTrue(hybrid_factories.called) diff --git a/lib/iris/tests/unit/fileformats/grib/message/__init__.py b/lib/iris/tests/unit/fileformats/grib/message/__init__.py index 3162608b42..800584bf5e 100644 --- a/lib/iris/tests/unit/fileformats/grib/message/__init__.py +++ b/lib/iris/tests/unit/fileformats/grib/message/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/message/test_Section.py b/lib/iris/tests/unit/fileformats/grib/message/test_Section.py index 07cee31dff..0de759f61b 100644 --- a/lib/iris/tests/unit/fileformats/grib/message/test_Section.py +++ b/lib/iris/tests/unit/fileformats/grib/message/test_Section.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py b/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py index 29dcd72120..4181dfc3fd 100644 --- a/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py +++ b/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py b/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py index b7e05de87b..05aff8c43d 100644 --- a/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py +++ b/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -34,10 +34,9 @@ class Test(tests.IrisTest): def test(self): message_location = _MessageLocation(mock.sentinel.filename, mock.sentinel.location) - patch_target = 'iris.fileformats.grib.message._RawGribMessage.' \ - 'from_file_offset' + pt = 'iris.fileformats.grib.message._RawGribMessage.from_file_offset' expected = mock.sentinel.message - with mock.patch(patch_target, return_value=expected) as rgm: + with mock.patch(pt, return_value=expected) as rgm: result = message_location() rgm.assert_called_once_with(mock.sentinel.filename, mock.sentinel.location) diff --git a/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py b/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py index c192ee48d9..7cfae668c1 100644 --- a/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py +++ b/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py b/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py index 0fbe2245ad..5ede464eac 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -19,12 +19,15 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +# Import iris.tests first so that some things can be initialised before +# importing anything else. import iris.tests as tests +import numpy as np + import iris from iris.fileformats.pp import EARTH_RADIUS as PP_DEFAULT_EARTH_RADIUS from iris.tests import mock -import numpy as np class GdtTestMixin(object): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test__missing_forecast_period.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test__missing_forecast_period.py index 5bd2c47127..9bc193d26b 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test__missing_forecast_period.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test__missing_forecast_period.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -27,8 +27,6 @@ # importing anything else. import iris.tests as tests -import datetime - from iris.cube import Cube from iris.coords import DimCoord from iris.fileformats.grib._save_rules import _missing_forecast_period @@ -41,7 +39,7 @@ def test_no_bounds(self): cube.add_aux_coord(t_coord) res = _missing_forecast_period(cube) - expected_rt = datetime.datetime(1970, 1, 1, 15, 0) + expected_rt = t_coord.units.num2date(15) expected_rt_type = 3 expected_fp = 0 expected_fp_type = 1 @@ -58,7 +56,7 @@ def test_with_bounds(self): cube.add_aux_coord(t_coord) res = _missing_forecast_period(cube) - expected_rt = datetime.datetime(1970, 1, 1, 14, 0) + expected_rt = t_coord.units.num2date(14) expected_rt_type = 3 expected_fp = 0 expected_fp_type = 1 @@ -79,7 +77,7 @@ def test_no_bounds(self): cube.add_aux_coord(frt_coord) res = _missing_forecast_period(cube) - expected_rt = datetime.datetime(1970, 1, 1, 8, 0) + expected_rt = frt_coord.units.num2date(8) expected_rt_type = 1 expected_fp = 3 * 24 - 8 expected_fp_type = 1 @@ -98,7 +96,7 @@ def test_with_bounds(self): cube.add_aux_coord(frt_coord) res = _missing_forecast_period(cube) - expected_rt = datetime.datetime(1970, 1, 1, 8, 0) + expected_rt = frt_coord.units.num2date(8) expected_rt_type = 1 expected_fp = 2 * 24 - 8 expected_fp_type = 1 diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test__non_missing_forecast_period.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test__non_missing_forecast_period.py index 25ac6690d8..4dd3d88b79 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test__non_missing_forecast_period.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test__non_missing_forecast_period.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_and_11.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_10_and_11.py similarity index 87% rename from lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_and_11.py rename to lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_10_and_11.py index 4cabb3be19..c4a7b70466 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_and_11.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_10_and_11.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -16,7 +16,8 @@ # along with Iris. If not, see . """ Unit tests for -:func:`iris.fileformats.grib._save_rules._product_definition_template_8_and_11` +:func: +`iris.fileformats.grib._save_rules._product_definition_template_8_10_and_11` """ @@ -34,7 +35,7 @@ from iris.tests import mock import iris.tests.stock as stock from iris.fileformats.grib._save_rules import \ - _product_definition_template_8_and_11 + _product_definition_template_8_10_and_11 class TestTypeOfStatisticalProcessing(tests.IrisTest): @@ -52,7 +53,7 @@ def test_sum(self, mock_set): cell_method = CellMethod(method='sum', coords=['time']) cube.add_cell_method(cell_method) - _product_definition_template_8_and_11(cube, mock.sentinel.grib) + _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) mock_set.assert_any_call(mock.sentinel.grib, "typeOfStatisticalProcessing", 1) @@ -62,7 +63,7 @@ def test_unrecognised(self, mock_set): cell_method = CellMethod(method='95th percentile', coords=['time']) cube.add_cell_method(cell_method) - _product_definition_template_8_and_11(cube, mock.sentinel.grib) + _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) mock_set.assert_any_call(mock.sentinel.grib, "typeOfStatisticalProcessing", 255) @@ -74,7 +75,7 @@ def test_multiple_cell_method_coords(self, mock_set): cube.add_cell_method(cell_method) with self.assertRaisesRegexp(ValueError, 'Cannot handle multiple coordinate name'): - _product_definition_template_8_and_11(cube, mock.sentinel.grib) + _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) @mock.patch.object(gribapi, 'grib_set') def test_cell_method_coord_name_fail(self, mock_set): @@ -84,7 +85,7 @@ def test_cell_method_coord_name_fail(self, mock_set): with self.assertRaisesRegexp( ValueError, "Expected a cell method with a coordinate " "name of 'time'"): - _product_definition_template_8_and_11(cube, mock.sentinel.grib) + _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) class TestTimeCoordPrerequisites(tests.IrisTest): @@ -102,8 +103,8 @@ def test_multiple_points(self, mock_set): self.cube.add_aux_coord(coord, 0) with self.assertRaisesRegexp( ValueError, 'Expected length one time coordinate'): - _product_definition_template_8_and_11(self.cube, - mock.sentinel.grib) + _product_definition_template_8_10_and_11(self.cube, + mock.sentinel.grib) @mock.patch.object(gribapi, 'grib_set') def test_no_bounds(self, mock_set): @@ -114,8 +115,8 @@ def test_no_bounds(self, mock_set): with self.assertRaisesRegexp( ValueError, 'Expected time coordinate with two bounds, ' 'got 0 bounds'): - _product_definition_template_8_and_11(self.cube, - mock.sentinel.grib) + _product_definition_template_8_10_and_11(self.cube, + mock.sentinel.grib) @mock.patch.object(gribapi, 'grib_set') def test_more_than_two_bounds(self, mock_set): @@ -126,8 +127,8 @@ def test_more_than_two_bounds(self, mock_set): with self.assertRaisesRegexp( ValueError, 'Expected time coordinate with two bounds, ' 'got 3 bounds'): - _product_definition_template_8_and_11(self.cube, - mock.sentinel.grib) + _product_definition_template_8_10_and_11(self.cube, + mock.sentinel.grib) class TestEndOfOverallTimeInterval(tests.IrisTest): @@ -147,7 +148,7 @@ def test_default_calendar(self, mock_set): cube.add_aux_coord(coord) grib = mock.sentinel.grib - _product_definition_template_8_and_11(cube, grib) + _product_definition_template_8_10_and_11(cube, grib) mock_set.assert_any_call( grib, "yearOfEndOfOverallTimeInterval", 1972) @@ -171,7 +172,7 @@ def test_360_day_calendar(self, mock_set): cube.add_aux_coord(coord) grib = mock.sentinel.grib - _product_definition_template_8_and_11(cube, grib) + _product_definition_template_8_10_and_11(cube, grib) mock_set.assert_any_call( grib, "yearOfEndOfOverallTimeInterval", 1972) @@ -202,7 +203,7 @@ def test_other_cell_methods(self, mock_set): cell_method = CellMethod(method='sum', coords=['time']) cube.add_cell_method(cell_method) - _product_definition_template_8_and_11(cube, mock.sentinel.grib) + _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) mock_set.assert_any_call(mock.sentinel.grib, 'numberOfTimeRange', 1) diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_data_section.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_data_section.py index 88999fd4d5..ff40af8a2a 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_data_section.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_data_section.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -15,22 +15,20 @@ # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . """ -Unit tests for -:func:`iris.fileformats.grib._save_rules.data_section`. +Unit tests for :func:`iris.fileformats.grib._save_rules.data_section`. """ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa -# import iris tests first so that some things can be initialised before +# import iris.tests first so that some things can be initialised before # importing anything else import iris.tests as tests import numpy as np import iris.cube - from iris.fileformats.grib._save_rules import data_section from iris.tests import mock diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_float32_as_int32.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_float32_as_int32.py index 0721ecaaa7..ff816ae7e9 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_float32_as_int32.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_float32_as_int32.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_int32_as_uint32.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_int32_as_uint32.py index 01e5d7dea9..6941b43284 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_int32_as_uint32.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_int32_as_uint32.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_section.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_section.py index 38225d030b..087dd897bd 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_section.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_section.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -29,9 +29,8 @@ from iris.coord_systems import LambertConformal from iris.exceptions import TranslationError -from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin - from iris.fileformats.grib._save_rules import grid_definition_section +from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin class Test(tests.IrisTest, GdtTestMixin): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_0.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_0.py index d73e9b72ce..a4f581b78e 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_0.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_0.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -30,9 +30,8 @@ import numpy as np from iris.coord_systems import GeogCS -from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin - from iris.fileformats.grib._save_rules import grid_definition_template_0 +from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin class Test(tests.IrisTest, GdtTestMixin): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_1.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_1.py index 419cd20d77..f8cdf181d4 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_1.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_1.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -31,11 +31,10 @@ from iris.coord_systems import GeogCS, RotatedGeogCS from iris.exceptions import TranslationError +from iris.fileformats.grib._save_rules import grid_definition_template_1 from iris.fileformats.pp import EARTH_RADIUS as PP_DEFAULT_EARTH_RADIUS from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin -from iris.fileformats.grib._save_rules import grid_definition_template_1 - class Test(tests.IrisTest, GdtTestMixin): def setUp(self): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_12.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_12.py index 8e27fe71cb..fb59a1f9e0 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_12.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_12.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -31,11 +31,10 @@ import iris.coords from iris.coord_systems import GeogCS, TransverseMercator +from iris.fileformats.grib._save_rules import grid_definition_template_12 from iris.exceptions import TranslationError from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin -from iris.fileformats.grib._save_rules import grid_definition_template_12 - class FakeGribError(Exception): pass diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_5.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_5.py index e65ad41107..5b55f1f4d7 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_5.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_5.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -31,11 +31,10 @@ from iris.coord_systems import GeogCS, RotatedGeogCS from iris.exceptions import TranslationError +from iris.fileformats.grib._save_rules import grid_definition_template_5 from iris.fileformats.pp import EARTH_RADIUS as PP_DEFAULT_EARTH_RADIUS from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin -from iris.fileformats.grib._save_rules import grid_definition_template_5 - class Test(tests.IrisTest, GdtTestMixin): def setUp(self): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_identification.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_identification.py index 728c82d157..695ec45639 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_identification.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_identification.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -25,13 +25,12 @@ import gribapi -import iris.fileformats.grib +import iris from iris.fileformats.grib._save_rules import identification from iris.tests import mock import iris.tests.stock as stock from iris.tests.test_grib_load_translations import TestGribSimple - GRIB_API = 'iris.fileformats.grib._save_rules.gribapi' diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_1.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_1.py new file mode 100644 index 0000000000..114305d56d --- /dev/null +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_1.py @@ -0,0 +1,76 @@ +# (C) British Crown Copyright 2016 - 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +""" +Unit tests for +:func:`iris.fileformats.grib._save_rules.product_definition_template_1` + +""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from cf_units import Unit +import gribapi + +from iris.coords import CellMethod, DimCoord +from iris.fileformats.grib._save_rules import product_definition_template_1 +from iris.tests import mock +import iris.tests.stock as stock + + +class TestRealizationIdentifier(tests.IrisTest): + def setUp(self): + self.cube = stock.lat_lon_cube() + # Rename cube to avoid warning about unknown discipline/parameter. + self.cube.rename('air_temperature') + coord = DimCoord([45], 'time', + units=Unit('days since epoch', calendar='standard')) + self.cube.add_aux_coord(coord) + + @mock.patch.object(gribapi, 'grib_set') + def test_realization(self, mock_set): + cube = self.cube + coord = DimCoord(10, 'realization', units='1') + cube.add_aux_coord(coord) + + product_definition_template_1(cube, mock.sentinel.grib) + mock_set.assert_any_call(mock.sentinel.grib, + "productDefinitionTemplateNumber", 1) + mock_set.assert_any_call(mock.sentinel.grib, + "perturbationNumber", 10) + mock_set.assert_any_call(mock.sentinel.grib, + "numberOfForecastsInEnsemble", 255) + mock_set.assert_any_call(mock.sentinel.grib, + "typeOfEnsembleForecast", 255) + + @mock.patch.object(gribapi, 'grib_set') + def test_multiple_realization_values(self, mock_set): + cube = self.cube + coord = DimCoord([8, 9, 10], 'realization', units='1') + cube.add_aux_coord(coord, 0) + + msg = "'realization' coordinate with one point is required" + with self.assertRaisesRegexp(ValueError, msg): + product_definition_template_1(cube, mock.sentinel.grib) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_10.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_10.py new file mode 100644 index 0000000000..f2d0c2b84c --- /dev/null +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_10.py @@ -0,0 +1,74 @@ +# (C) British Crown Copyright 2013 - 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +""" +Unit tests for +:func:`iris.fileformats.grib._save_rules.product_definition_template_10` + +""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from cf_units import Unit +import gribapi + +from iris.coords import DimCoord +from iris.fileformats.grib._save_rules import product_definition_template_10 +from iris.tests import mock +import iris.tests.stock as stock + + +class TestPercentileValueIdentifier(tests.IrisTest): + def setUp(self): + self.cube = stock.lat_lon_cube() + # Rename cube to avoid warning about unknown discipline/parameter. + self.cube.rename('y_wind') + time_coord = DimCoord( + 20, 'time', bounds=[0, 40], + units=Unit('days since epoch', calendar='julian')) + self.cube.add_aux_coord(time_coord) + + @mock.patch.object(gribapi, 'grib_set') + def test_percentile_value(self, mock_set): + cube = self.cube + percentile_coord = DimCoord(95, long_name='percentile_over_time') + cube.add_aux_coord(percentile_coord) + + product_definition_template_10(cube, mock.sentinel.grib) + mock_set.assert_any_call(mock.sentinel.grib, + "productDefinitionTemplateNumber", 10) + mock_set.assert_any_call(mock.sentinel.grib, + "percentileValue", 95) + + @mock.patch.object(gribapi, 'grib_set') + def test_multiple_percentile_value(self, mock_set): + cube = self.cube + percentile_coord = DimCoord([5, 10, 15], + long_name='percentile_over_time') + cube.add_aux_coord(percentile_coord, 0) + err_msg = "A cube 'percentile_over_time' coordinate with one point "\ + "is required" + with self.assertRaisesRegexp(ValueError, err_msg): + product_definition_template_10(cube, mock.sentinel.grib) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_11.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_11.py index f390504207..d40eeb9676 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_11.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_11.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -31,9 +31,9 @@ import gribapi from iris.coords import CellMethod, DimCoord +from iris.fileformats.grib._save_rules import product_definition_template_11 from iris.tests import mock import iris.tests.stock as stock -from iris.fileformats.grib._save_rules import product_definition_template_11 class TestRealizationIdentifier(tests.IrisTest): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_40.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_40.py index c039f67c21..9eb56b4133 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_40.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_40.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2016, Met Office +# (C) British Crown Copyright 2016 - 2017, Met Office # # This file is part of Iris. # @@ -31,9 +31,9 @@ import gribapi from iris.coords import DimCoord +from iris.fileformats.grib._save_rules import product_definition_template_40 from iris.tests import mock import iris.tests.stock as stock -from iris.fileformats.grib._save_rules import product_definition_template_40 class TestChemicalConstituentIdentifier(tests.IrisTest): @@ -52,10 +52,10 @@ def test_constituent_type(self, mock_set): product_definition_template_40(cube, mock.sentinel.grib) mock_set.assert_any_call(mock.sentinel.grib, - "productDefinitionTemplateNumber", 40) + 'productDefinitionTemplateNumber', 40) mock_set.assert_any_call(mock.sentinel.grib, - "constituentType", 0) + 'constituentType', 0) -if __name__ == "__main__": +if __name__ == '__main__': tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_8.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_8.py index 24c8a82b13..3fbe619489 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_8.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_8.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -31,9 +31,9 @@ import gribapi from iris.coords import CellMethod, DimCoord +from iris.fileformats.grib._save_rules import product_definition_template_8 from iris.tests import mock import iris.tests.stock as stock -from iris.fileformats.grib._save_rules import product_definition_template_8 class TestProductDefinitionIdentifier(tests.IrisTest): diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_reference_time.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_reference_time.py index b9a334a241..4b97a337bd 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_reference_time.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_reference_time.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2016, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -25,26 +25,17 @@ import gribapi -import iris.fileformats.grib +from iris.fileformats.grib import load_cubes from iris.fileformats.grib._save_rules import reference_time from iris.tests import mock -import iris.tests.stock as stock -from iris.tests.test_grib_load_translations import TestGribSimple -GRIB_API = 'iris.fileformats.grib._save_rules.gribapi' - - -class Test(TestGribSimple): - @tests.skip_data - def test_forecast_period(self): - # The stock cube has a non-compliant forecast_period. - iris.fileformats.grib.hindcast_workaround = True - cube = stock.global_grib2() - +class Test(tests.IrisTest): + def _test(self, cube): grib = mock.Mock() mock_gribapi = mock.Mock(spec=gribapi) - with mock.patch(GRIB_API, mock_gribapi): + with mock.patch('iris.fileformats.grib._save_rules.gribapi', + mock_gribapi): reference_time(cube, grib) mock_gribapi.assert_has_calls( @@ -52,25 +43,20 @@ def test_forecast_period(self): mock.call.grib_set_long(grib, "dataDate", '19980306'), mock.call.grib_set_long(grib, "dataTime", '0300')]) + @tests.skip_data + def test_forecast_period(self): + # The stock cube has a non-compliant forecast_period. + fname = tests.get_data_path(('GRIB', 'global_t', 'global.grib2')) + [cube] = load_cubes(fname) + self._test(cube) + @tests.skip_data def test_no_forecast_period(self): # The stock cube has a non-compliant forecast_period. - iris.fileformats.grib.hindcast_workaround = True - cube = stock.global_grib2() + fname = tests.get_data_path(('GRIB', 'global_t', 'global.grib2')) + [cube] = load_cubes(fname) cube.remove_coord("forecast_period") - frt_coords = cube.coords('forecast_reference_time') - if frt_coords: - cube.remove_coord(frt_coords[0]) - - grib = mock.Mock() - mock_gribapi = mock.Mock(spec=gribapi) - with mock.patch(GRIB_API, mock_gribapi): - reference_time(cube, grib) - - mock_gribapi.assert_has_calls( - [mock.call.grib_set_long(grib, "significanceOfReferenceTime", 3), - mock.call.grib_set_long(grib, "dataDate", '19941201'), - mock.call.grib_set_long(grib, "dataTime", '0000')]) + self._test(cube) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_fixed_surfaces.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_fixed_surfaces.py index 9489be5455..f4f50b27a9 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_fixed_surfaces.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_fixed_surfaces.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2013 - 2015, Met Office +# (C) British Crown Copyright 2013 - 2017, Met Office # # This file is part of Iris. # @@ -15,8 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . """ -Unit tests for -:func:`iris.fileformats.grib._save_rules.set_fixed_surfaces`. +Unit tests for :func:`iris.fileformats.grib._save_rules.set_fixed_surfaces`. """ diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_increment.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_increment.py index a94df1572a..bddf9930b6 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_increment.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_increment.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -15,8 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . """ -Unit tests for -:func:`iris.fileformats.grib._save_rules.set_time_increment` +Unit tests for :func:`iris.fileformats.grib._save_rules.set_time_increment` """ diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_range.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_range.py index e1a9d8f1a5..d6ff83351e 100644 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_range.py +++ b/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_range.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -15,8 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Iris. If not, see . """ -Unit tests for -:func:`iris.fileformats.grib._save_rules.set_time_range` +Unit tests for :func:`iris.fileformats.grib._save_rules.set_time_range` """ diff --git a/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py b/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py index 6f741a7372..6f620e7d8f 100644 --- a/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py +++ b/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2015, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -29,9 +29,11 @@ from biggus import NumpyArrayAdapter import numpy as np +from iris.exceptions import TranslationError from iris.fileformats.grib import GribWrapper, GribDataProxy from iris.tests import mock + _message_length = 1000 @@ -40,7 +42,8 @@ def _mock_grib_get_long(grib_message, key): numberOfValues=200, jPointsAreConsecutive=0, Ni=20, - Nj=10) + Nj=10, + edition=1) try: result = lookup[key] except KeyError: @@ -60,6 +63,31 @@ def _mock_grib_get_native_type(grib_message, key): return result +class Test_edition(tests.IrisTest): + def setUp(self): + self.patch('iris.fileformats.grib.GribWrapper._confirm_in_scope') + self.patch('iris.fileformats.grib.GribWrapper._compute_extra_keys') + self.patch('gribapi.grib_get_long', _mock_grib_get_long) + self.patch('gribapi.grib_get_string', _mock_grib_get_string) + self.patch('gribapi.grib_get_native_type', _mock_grib_get_native_type) + self.tell = mock.Mock(side_effect=[_message_length]) + + def test_not_edition_1(self): + def func(grib_message, key): + return 2 + + emsg = "GRIB edition 2 is not supported by 'GribWrapper'" + with mock.patch('gribapi.grib_get_long', func): + with self.assertRaisesRegexp(TranslationError, emsg): + GribWrapper(None) + + def test_edition_1(self): + grib_message = 'regular_ll' + grib_fh = mock.Mock(tell=self.tell) + wrapper = GribWrapper(grib_message, grib_fh) + self.assertEqual(wrapper.grib_message, grib_message) + + class Test_deferred(tests.IrisTest): def setUp(self): confirm_patch = mock.patch( @@ -85,10 +113,9 @@ def setUp(self): def test_regular_sequential(self): tell_tale = np.arange(1, 5) * _message_length grib_fh = mock.Mock(tell=mock.Mock(side_effect=tell_tale)) - auto_regularise = False grib_message = 'regular_ll' for i, _ in enumerate(tell_tale): - gw = GribWrapper(grib_message, grib_fh, auto_regularise) + gw = GribWrapper(grib_message, grib_fh) self.assertIsInstance(gw._data, NumpyArrayAdapter) proxy = gw._data.concrete self.assertIsInstance(proxy, GribDataProxy) @@ -97,16 +124,14 @@ def test_regular_sequential(self): self.assertIs(proxy.fill_value, np.nan) self.assertEqual(proxy.path, grib_fh.name) self.assertEqual(proxy.offset, _message_length * i) - self.assertEqual(proxy.regularise, auto_regularise) def test_regular_mixed(self): tell_tale = np.arange(1, 5) * _message_length expected = tell_tale - _message_length grib_fh = mock.Mock(tell=mock.Mock(side_effect=tell_tale)) - auto_regularise = False grib_message = 'regular_ll' for offset in expected: - gw = GribWrapper(grib_message, grib_fh, auto_regularise) + gw = GribWrapper(grib_message, grib_fh) self.assertIsInstance(gw._data, NumpyArrayAdapter) proxy = gw._data.concrete self.assertIsInstance(proxy, GribDataProxy) @@ -115,15 +140,13 @@ def test_regular_mixed(self): self.assertIs(proxy.fill_value, np.nan) self.assertEqual(proxy.path, grib_fh.name) self.assertEqual(proxy.offset, offset) - self.assertEqual(proxy.regularise, auto_regularise) def test_reduced_sequential(self): tell_tale = np.arange(1, 5) * _message_length grib_fh = mock.Mock(tell=mock.Mock(side_effect=tell_tale)) - auto_regularise = False grib_message = 'reduced_gg' for i, _ in enumerate(tell_tale): - gw = GribWrapper(grib_message, grib_fh, auto_regularise) + gw = GribWrapper(grib_message, grib_fh) self.assertIsInstance(gw._data, NumpyArrayAdapter) proxy = gw._data.concrete self.assertIsInstance(proxy, GribDataProxy) @@ -132,16 +155,14 @@ def test_reduced_sequential(self): self.assertIs(proxy.fill_value, np.nan) self.assertEqual(proxy.path, grib_fh.name) self.assertEqual(proxy.offset, _message_length * i) - self.assertEqual(proxy.regularise, auto_regularise) def test_reduced_mixed(self): tell_tale = np.arange(1, 5) * _message_length expected = tell_tale - _message_length grib_fh = mock.Mock(tell=mock.Mock(side_effect=tell_tale)) - auto_regularise = False grib_message = 'reduced_gg' for offset in expected: - gw = GribWrapper(grib_message, grib_fh, auto_regularise) + gw = GribWrapper(grib_message, grib_fh) self.assertIsInstance(gw._data, NumpyArrayAdapter) proxy = gw._data.concrete self.assertIsInstance(proxy, GribDataProxy) @@ -150,7 +171,6 @@ def test_reduced_mixed(self): self.assertIs(proxy.fill_value, np.nan) self.assertEqual(proxy.path, grib_fh.name) self.assertEqual(proxy.offset, offset) - self.assertEqual(proxy.regularise, auto_regularise) if __name__ == '__main__': diff --git a/lib/iris/tests/unit/fileformats/grib/test__load_generate.py b/lib/iris/tests/unit/fileformats/grib/test__load_generate.py new file mode 100644 index 0000000000..dc0cdabb16 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/grib/test__load_generate.py @@ -0,0 +1,80 @@ +# (C) British Crown Copyright 2016 - 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the `iris.fileformats.grib._load_generate` function.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +import iris +from iris.exceptions import TranslationError +import iris.fileformats.grib +from iris.fileformats.grib import GribWrapper +from iris.fileformats.grib import _load_generate +from iris.fileformats.grib.message import GribMessage +from iris.fileformats.rules import Loader +from iris.tests import mock + + +class Test(tests.IrisTest): + def setUp(self): + self.fname = mock.sentinel.fname + self.message_id = mock.sentinel.message_id + self.grib_fh = mock.sentinel.grib_fh + + def _make_test_message(self, sections): + raw_message = mock.Mock(sections=sections, _message_id=self.message_id) + file_ref = mock.Mock(open_file=self.grib_fh) + return GribMessage(raw_message, None, file_ref=file_ref) + + def test_grib1(self): + sections = [{'editionNumber': 1}] + message = self._make_test_message(sections) + mfunc = 'iris.fileformats.grib.GribMessage.messages_from_filename' + mclass = 'iris.fileformats.grib.GribWrapper' + with mock.patch(mfunc, return_value=[message]) as mock_func: + with mock.patch(mclass, spec=GribWrapper) as mock_wrapper: + field = next(_load_generate(self.fname)) + mock_func.assert_called_once_with(self.fname) + self.assertIsInstance(field, GribWrapper) + mock_wrapper.assert_called_once_with(self.message_id, + grib_fh=self.grib_fh) + + def test_grib2(self): + sections = [{'editionNumber': 2}] + message = self._make_test_message(sections) + mfunc = 'iris.fileformats.grib.GribMessage.messages_from_filename' + with mock.patch(mfunc, return_value=[message]) as mock_func: + field = next(_load_generate(self.fname)) + mock_func.assert_called_once_with(self.fname) + self.assertEqual(field, message) + + def test_grib_unknown(self): + sections = [{'editionNumber': 0}] + message = self._make_test_message(sections) + mfunc = 'iris.fileformats.grib.GribMessage.messages_from_filename' + emsg = 'GRIB edition 0 is not supported' + with mock.patch(mfunc, return_value=[message]): + with self.assertRaisesRegexp(TranslationError, emsg): + next(_load_generate(self.fname)) + + +if __name__ == '__main__': + tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/test_as_messages.py b/lib/iris/tests/unit/fileformats/grib/test_as_messages.py deleted file mode 100644 index bc2fc7a7ac..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test_as_messages.py +++ /dev/null @@ -1,50 +0,0 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office -# -# This file is part of Iris. -# -# Iris is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Iris is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Iris. If not, see . -"""Unit tests for the `iris.fileformats.grib.as_messages` function.""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -import iris.tests as tests - -import gribapi - -import iris -from iris.coords import DimCoord -import iris.fileformats.grib as grib -from iris.tests import mock -import iris.tests.stock as stock - - -class TestAsMessages(tests.IrisTest): - def setUp(self): - self.cube = stock.realistic_3d() - - def test_as_messages(self): - realization = 2 - type_of_process = 4 - coord = DimCoord(realization, standard_name='realization', units='1') - self.cube.add_aux_coord(coord) - messages = grib.as_messages(self.cube) - for message in messages: - self.assertEqual(gribapi.grib_get_long(message, - 'typeOfProcessedData'), - type_of_process) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/test_as_pairs.py b/lib/iris/tests/unit/fileformats/grib/test_as_pairs.py deleted file mode 100644 index 3aa76097ba..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test_as_pairs.py +++ /dev/null @@ -1,51 +0,0 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office -# -# This file is part of Iris. -# -# Iris is free software: you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by the -# Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Iris is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Iris. If not, see . -"""Unit tests for the `iris.fileformats.grib.as_pairs` function.""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -import iris.tests as tests - -import gribapi - -import iris -from iris.coords import DimCoord -import iris.fileformats.grib as grib -from iris.tests import mock -import iris.tests.stock as stock - - -class TestAsPairs(tests.IrisTest): - def setUp(self): - self.cube = stock.realistic_3d() - - def test_as_pairs(self): - realization = 2 - type_of_process = 4 - coord = DimCoord(realization, standard_name='realization', units='1') - self.cube.add_aux_coord(coord) - slices_and_messages = grib.as_pairs(self.cube) - for aslice, message in slices_and_messages: - self.assertEqual(aslice.shape, (9, 11)) - self.assertEqual(gribapi.grib_get_long(message, - 'typeOfProcessedData'), - type_of_process) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/test_load_cubes.py b/lib/iris/tests/unit/fileformats/grib/test_load_cubes.py index f3559a1676..659b57371d 100644 --- a/lib/iris/tests/unit/fileformats/grib/test_load_cubes.py +++ b/lib/iris/tests/unit/fileformats/grib/test_load_cubes.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2014 - 2016, Met Office +# (C) British Crown Copyright 2014 - 2017, Met Office # # This file is part of Iris. # @@ -19,55 +19,31 @@ from __future__ import (absolute_import, division, print_function) from six.moves import (filter, input, map, range, zip) # noqa +# Import iris.tests first so that some things can be initialised before +# importing anything else. import iris.tests as tests import iris import iris.fileformats.grib -import iris.fileformats.grib.load_rules -import iris.fileformats.rules - from iris.fileformats.grib import load_cubes +from iris.fileformats.rules import Loader from iris.tests import mock -class TestToggle(tests.IrisTest): - def _test(self, mode, generator, converter): - # Ensure that `load_cubes` defers to - # `iris.fileformats.rules.load_cubes`, passing a correctly - # configured `Loader` instance. - with iris.FUTURE.context(strict_grib_load=mode): - with mock.patch('iris.fileformats.rules.load_cubes') as rules_load: - rules_load.return_value = mock.sentinel.RESULT - result = load_cubes(mock.sentinel.FILES, - mock.sentinel.CALLBACK, - mock.sentinel.REGULARISE) - if mode: - kw_args = {} - else: - kw_args = {'auto_regularise': mock.sentinel.REGULARISE} - loader = iris.fileformats.rules.Loader( - generator, kw_args, - converter, None) - rules_load.assert_called_once_with(mock.sentinel.FILES, - mock.sentinel.CALLBACK, - loader) - self.assertIs(result, mock.sentinel.RESULT) - - def test_sloppy_mode(self): - # Ensure that `load_cubes` uses: - # iris.fileformats.grib.grib_generator - # iris.fileformats.grib.load_rules.convert - self._test(False, iris.fileformats.grib.grib_generator, - iris.fileformats.grib.load_rules.convert) - - def test_strict_mode(self): - # Ensure that `load_cubes` uses: - # iris.fileformats.grib.message.GribMessage.messages_from_filename - # iris.fileformats.grib._load_convert.convert - self._test( - True, - iris.fileformats.grib.message.GribMessage.messages_from_filename, - iris.fileformats.grib._load_convert.convert) +class Test(tests.IrisTest): + def test(self): + generator = iris.fileformats.grib._load_generate + converter = iris.fileformats.grib._load_convert.convert + files = mock.sentinel.FILES + callback = mock.sentinel.CALLBACK + expected_result = mock.sentinel.RESULT + with mock.patch('iris.fileformats.rules.load_cubes') as rules_load: + rules_load.return_value = expected_result + result = load_cubes(files, callback) + kwargs = {} + loader = Loader(generator, kwargs, converter, None) + rules_load.assert_called_once_with(files, callback, loader) + self.assertIs(result, expected_result) @tests.skip_data @@ -78,7 +54,7 @@ def test_reduced_raw(self): # interpolating to a regular grid. gribfile = tests.get_data_path( ("GRIB", "reduced", "reduced_gg.grib2")) - grib_generator = load_cubes(gribfile, auto_regularise=False) + grib_generator = load_cubes(gribfile) self.assertCML(next(grib_generator)) diff --git a/lib/iris/tests/unit/fileformats/grib/test_save_grib2.py b/lib/iris/tests/unit/fileformats/grib/test_save_grib2.py new file mode 100644 index 0000000000..7572dbdd61 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/grib/test_save_grib2.py @@ -0,0 +1,61 @@ +# (C) British Crown Copyright 2016 - 2017, Met Office +# +# This file is part of Iris. +# +# Iris is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Iris is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Iris. If not, see . +"""Unit tests for the `iris.fileformats.grib.save_grib2` function.""" + +from __future__ import (absolute_import, division, print_function) +from six.moves import (filter, input, map, range, zip) # noqa +import six + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +import iris.fileformats.grib +from iris.tests import mock + + +class TestSaveGrib2(tests.IrisTest): + def setUp(self): + self.cube = mock.sentinel.cube + self.target = mock.sentinel.target + func = 'iris.fileformats.grib.save_pairs_from_cube' + self.messages = list(range(10)) + slices = self.messages + side_effect = [zip(slices, self.messages)] + self.save_pairs_from_cube = self.patch(func, side_effect=side_effect) + func = 'iris.fileformats.grib.save_messages' + self.save_messages = self.patch(func) + + def _check(self, append=False): + iris.fileformats.grib.save_grib2(self.cube, self.target, append=append) + self.save_pairs_from_cube.called_once_with(self.cube) + args, kwargs = self.save_messages.call_args + self.assertEqual(len(args), 2) + messages, target = args + self.assertEqual(list(messages), self.messages) + self.assertEqual(target, self.target) + self.assertEqual(kwargs, dict(append=append)) + + def test_save_no_append(self): + self._check() + + def test_save_append(self): + self._check(append=True) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/test_save_messages.py b/lib/iris/tests/unit/fileformats/grib/test_save_messages.py index 33694e2b6d..0968b3fb3b 100644 --- a/lib/iris/tests/unit/fileformats/grib/test_save_messages.py +++ b/lib/iris/tests/unit/fileformats/grib/test_save_messages.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2015, Met Office +# (C) British Crown Copyright 2015 - 2017, Met Office # # This file is part of Iris. # @@ -27,7 +27,7 @@ import gribapi import numpy as np -import iris.fileformats.grib as grib +import iris.fileformats.grib from iris.tests import mock @@ -47,7 +47,8 @@ def test_save(self): # as the gribapi code does a type check # this is deemed acceptable within the scope of this unit test with self.assertRaises((AssertionError, TypeError)): - grib.save_messages([self.grib_message], 'foo.grib2') + iris.fileformats.grib.save_messages([self.grib_message], + 'foo.grib2') self.assertTrue(mock.call('foo.grib2', 'wb') in m.mock_calls) def test_save_append(self): @@ -61,8 +62,9 @@ def test_save_append(self): # as the gribapi code does a type check # this is deemed acceptable within the scope of this unit test with self.assertRaises((AssertionError, TypeError)): - grib.save_messages([self.grib_message], 'foo.grib2', - append=True) + iris.fileformats.grib.save_messages([self.grib_message], + 'foo.grib2', + append=True) self.assertTrue(mock.call('foo.grib2', 'ab') in m.mock_calls)