diff --git a/.travis.yml b/.travis.yml index bc87375fba..3193e71027 100644 --- a/.travis.yml +++ b/.travis.yml @@ -65,6 +65,16 @@ install: fi fi +# JUST FOR NOW : Install latest master version of iris-grib. + - if [[ "$TEST_MINIMAL" != true ]]; then + INSTALL_DIR=$(pwd) ; + wget https://github.com/SciTools/iris-grib/archive/master.zip ; + unzip -q master.zip ; + cd iris-grib-master ; + python setup.py install ; + cd - ; + fi + - PREFIX=$HOME/miniconda/envs/$ENV_NAME # Output debug info diff --git a/INSTALL b/INSTALL index 36c2b8d964..8928318577 100644 --- a/INSTALL +++ b/INSTALL @@ -122,11 +122,9 @@ gdal 1.9.1 or later (https://pypi.python.org/pypi/GDAL/) graphviz 2.18 or later (http://www.graphviz.org/) Graph visualisation software. -grib-api 1.9.16 or later - (https://software.ecmwf.int/wiki/display/GRIB/Releases) - API for the encoding and decoding WMO FM-92 GRIB edition 1 and - edition 2 messages. A compression library such as Jasper is required - to read JPEG2000 compressed GRIB2 files. +iris-grib 0.11 or later + (https://github.com/scitools/iris-grib) + Iris interface to ECMWF's GRIB API matplotlib 1.2.0 (http://matplotlib.sourceforge.net/) Python package for 2D plotting. diff --git a/conda-requirements.txt b/conda-requirements.txt index e0206a539d..0ba33e337b 100644 --- a/conda-requirements.txt +++ b/conda-requirements.txt @@ -34,3 +34,6 @@ nc_time_axis pandas python-stratify pyugrid + +# Iris extensions (i.e. key tools that depend on Iris) +# iris_grib diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 5578086c48..506e6d006c 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -155,11 +155,12 @@ modindex_common_prefix = ['iris'] intersphinx_mapping = { - 'python': ('http://docs.python.org/2.7', None), - 'numpy': ('http://docs.scipy.org/doc/numpy/', None), - 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), - 'matplotlib': ('http://matplotlib.org/', None), - 'cartopy': ('http://scitools.org.uk/cartopy/docs/latest/', None), + 'cartopy': ('http://scitools.org.uk/cartopy/docs/latest/', None), + 'iris-grib': ('http://iris-grib.readthedocs.io/en/latest/', None), + 'matplotlib': ('http://matplotlib.org/', None), + 'numpy': ('http://docs.scipy.org/doc/numpy/', None), + 'python': ('http://docs.python.org/2.7', None), + 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), } diff --git a/lib/iris/fileformats/__init__.py b/lib/iris/fileformats/__init__.py index d1bfe5d102..c869fe07bd 100644 --- a/lib/iris/fileformats/__init__.py +++ b/lib/iris/fileformats/__init__.py @@ -27,11 +27,6 @@ UriProtocol, LeadingLine) from . import abf from . import um -try: - from . import grib as igrib -except ImportError: - igrib = None - from . import name from . import netcdf from . import nimrod @@ -71,10 +66,13 @@ # GRIB files. # def _load_grib(*args, **kwargs): - if igrib is None: - raise RuntimeError('Unable to load GRIB file - the ECMWF ' - '`gribapi` package is not installed.') - return igrib.load_cubes(*args, **kwargs) + try: + from iris_grib import load_cubes + except ImportError: + raise RuntimeError('Unable to load GRIB file - ' + '"iris_grib" package is not installed.') + + return load_cubes(*args, **kwargs) # NB. Because this is such a "fuzzy" check, we give this a very low diff --git a/lib/iris/fileformats/grib/__init__.py b/lib/iris/fileformats/grib/__init__.py deleted file mode 100644 index cfe342a2d6..0000000000 --- a/lib/iris/fileformats/grib/__init__.py +++ /dev/null @@ -1,877 +0,0 @@ -# (C) British Crown Copyright 2010 - 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 . -""" -Conversion of cubes to/from GRIB. - -See: `ECMWF GRIB API `_. - -""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa -import six - -import datetime -import math # for fmod - -import cartopy.crs as ccrs -import cf_units -import gribapi -import numpy as np -import numpy.ma as ma - -from iris._lazy_data import as_lazy_data -import iris.coord_systems as coord_systems -from iris.exceptions import TranslationError, NotYetImplementedError -# NOTE: careful here, to avoid circular imports (as iris imports grib) -from iris.fileformats.grib import grib_phenom_translation as gptx -from iris.fileformats.grib import _save_rules -from iris.fileformats.grib._load_convert import convert as load_convert -from iris.fileformats.grib.message import GribMessage - - -__all__ = ['load_cubes', 'save_grib2', 'load_pairs_from_fields', - '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 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), - 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: ('15 minutes', 15*60), - 14: ('30 minutes', 30*60), - 254: ('seconds', 1), -} - -unknown_string = "???" - - -class GribDataProxy(object): - """A reference to the data payload of a single Grib message.""" - - __slots__ = ('shape', 'dtype', 'path', 'offset') - - def __init__(self, shape, dtype, path, offset): - self.shape = shape - self.dtype = dtype - self.path = path - self.offset = offset - - @property - def ndim(self): - return len(self.shape) - - 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) - data = _message_values(grib_message, self.shape) - gribapi.grib_release(grib_message) - - return data.__getitem__(keys) - - def __repr__(self): - msg = '<{self.__class__.__name__} shape={self.shape} ' \ - 'dtype={self.dtype!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__} - - def __setstate__(self, state): - for key, value in six.iteritems(state): - setattr(self, key, value) - - -class GribWrapper(object): - """ - Contains a pygrib object plus some extra keys of our own. - - 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): - """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 - # advanced the file pointer to the end of the message. - offset = grib_fh.tell() - message_length = gribapi.grib_get_long(grib_message, 'totalLength') - - # Initialise the key-extension dictionary. - # NOTE: this attribute *must* exist, or the the __getattr__ overload - # can hit an infinite loop. - self.extra_keys = {} - self._confirm_in_scope() - self._compute_extra_keys() - - # Calculate the data payload shape. - shape = (gribapi.grib_get_long(grib_message, 'numberOfValues'),) - - if not self.gridType.startswith('reduced'): - ni, nj = self.Ni, self.Nj - j_fast = gribapi.grib_get_long(grib_message, - 'jPointsAreConsecutive') - shape = (nj, ni) if j_fast == 0 else (ni, nj) - - if deferred: - # 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 the current message due to the grib-api reading the message. - proxy = GribDataProxy(shape, np.array([0.]).dtype, grib_fh.name, - offset - message_length) - self._data = as_lazy_data(proxy) - else: - self.data = _message_values(grib_message, shape) - - 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) - if self.alternativeRowScanning == 1: - raise ValueError("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... - if key in ["values", "pv", "latitudes", "longitudes"]: - res = gribapi.grib_get_double_array(self.grib_message, key) - 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)) - 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)) - elif key_type == str: - res = gribapi.grib_get_string(self.grib_message, key) - else: - 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? - 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 - raise AttributeError("Cannot find GRIB key %s" % key) - - return res - - def _timeunit_detail(self): - """Return the (string, seconds) describing the message time unit.""" - unit_code = self.indicatorOfUnitOfTimeRange - if unit_code not in TIME_CODES_EDITION1: - message = 'Unhandled time unit for forecast ' \ - 'indicatorOfUnitOfTimeRange : ' + str(unit_code) - raise NotYetImplementedError(message) - return TIME_CODES_EDITION1[unit_code] - - def _timeunit_string(self): - """Get the udunits string for the message time unit.""" - return self._timeunit_detail()[0] - - def _timeunit_seconds(self): - """Get the number of seconds in the message time unit.""" - return self._timeunit_detail()[1] - - def _compute_extra_keys(self): - """Compute our extra keys.""" - global unknown_string - - self.extra_keys = {} - forecastTime = self.startStep - - # regular or rotated grid? - try: - 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} - - # cf phenomenon translation - # 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)) - - # forecast time with workarounds - self.extra_keys['_forecastTime'] = forecastTime - - # verification date - processingDone = self._get_processing_done() - # time processed? - if processingDone.startswith("time"): - 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) - else: - self.extra_keys['_phenomenonDateTime'] = \ - self._get_verification_date() - - # originating centre - # TODO #574 Expand to include sub-centre - self.extra_keys['_originatingCentre'] = CENTRE_TITLES.get( - centre, "unknown centre %s" % centre) - - # 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 - - # pre-defined sphere - if self.shapeOfTheEarth == 0: - geoid = coord_systems.GeogCS(semi_major_axis=6367470) - - # custom sphere - elif self.shapeOfTheEarth == 1: - geoid = coord_systems.GeogCS( - self.scaledValueOfRadiusOfSphericalEarth * - 10 ** -self.scaleFactorOfRadiusOfSphericalEarth) - - # IAU65 oblate sphere - elif self.shapeOfTheEarth == 2: - geoid = coord_systems.GeogCS(6378160, inverse_flattening=297.0) - - # custom oblate spheroid (km) - elif self.shapeOfTheEarth == 3: - geoid = coord_systems.GeogCS( - semi_major_axis=self.scaledValueOfEarthMajorAxis * - 10 ** -self.scaleFactorOfEarthMajorAxis * 1000., - semi_minor_axis=self.scaledValueOfEarthMinorAxis * - 10 ** -self.scaleFactorOfEarthMinorAxis * 1000.) - - # IAG-GRS80 oblate spheroid - elif self.shapeOfTheEarth == 4: - geoid = coord_systems.GeogCS(6378137, None, 298.257222101) - - # WGS84 - elif self.shapeOfTheEarth == 5: - geoid = \ - coord_systems.GeogCS(6378137, inverse_flattening=298.257223563) - - # pre-defined sphere - elif self.shapeOfTheEarth == 6: - geoid = coord_systems.GeogCS(6371229) - - # custom oblate spheroid (m) - elif self.shapeOfTheEarth == 7: - geoid = coord_systems.GeogCS( - semi_major_axis=self.scaledValueOfEarthMajorAxis * - 10 ** -self.scaleFactorOfEarthMajorAxis, - semi_minor_axis=self.scaledValueOfEarthMinorAxis * - 10 ** -self.scaleFactorOfEarthMinorAxis) - - elif self.shapeOfTheEarth == 8: - raise ValueError("unhandled shape of earth : grib earth shape = 8") - - else: - raise ValueError("undefined shape of earth") - - gridType = gribapi.grib_get_string(self.grib_message, "gridType") - - 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 - elif gridType == 'rotated_ll': - # TODO: Confirm the translation from angleOfRotation to - # north_pole_lon (usually 0 for both) - self.extra_keys['_x_coord_name'] = "grid_longitude" - self.extra_keys['_y_coord_name'] = "grid_latitude" - southPoleLon = longitudeOfSouthernPoleInDegrees - southPoleLat = latitudeOfSouthernPoleInDegrees - self.extra_keys['_coord_system'] = \ - 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" - - if self.projectionCentreFlag == 0: - pole_lat = 90 - elif self.projectionCentreFlag == 1: - pole_lat = -90 - else: - raise TranslationError("Unhandled projectionCentreFlag") - - # Note: I think the grib api defaults LaDInDegrees to 60 for grib1. - self.extra_keys['_coord_system'] = \ - coord_systems.Stereographic( - pole_lat, self.orientationOfTheGridInDegrees, 0, 0, - self.LaDInDegrees, ellipsoid=geoid) - - elif gridType == 'lambert': - self.extra_keys['_x_coord_name'] = "projection_x_coordinate" - self.extra_keys['_y_coord_name'] = "projection_y_coordinate" - - flag_name = "projectionCenterFlag" - - if getattr(self, flag_name) == 0: - pole_lat = 90 - elif getattr(self, flag_name) == 1: - pole_lat = -90 - else: - raise TranslationError("Unhandled projectionCentreFlag") - - LambertConformal = coord_systems.LambertConformal - self.extra_keys['_coord_system'] = LambertConformal( - self.LaDInDegrees, self.LoVInDegrees, 0, 0, - secant_latitudes=(self.Latin1InDegrees, self.Latin2InDegrees), - ellipsoid=geoid) - else: - raise TranslationError("unhandled grid type: {}".format(gridType)) - - if gridType in ["regular_ll", "rotated_ll"]: - self._regular_longitude_common() - j_step = self.jDirectionIncrementInDegrees - if not self.jScansPositively: - j_step = -j_step - self._y_points = (np.arange(self.Nj, dtype=np.float64) * j_step + - self.latitudeOfFirstGridPointInDegrees) - - elif gridType in ['regular_gg']: - # longitude coordinate is straight-forward - self._regular_longitude_common() - # get the distinct latitudes, and make sure they are sorted - # (south-to-north) and then put them in the right direction - # depending on the scan direction - latitude_points = gribapi.grib_get_double_array( - self.grib_message, 'distinctLatitudes').astype(np.float64) - latitude_points.sort() - if not self.jScansPositively: - # we require latitudes north-to-south - self._y_points = latitude_points[::-1] - else: - self._y_points = latitude_points - - elif gridType in ["polar_stereographic", "lambert"]: - # 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()) - - if not np.all(np.isfinite([x1, y1])): - raise TranslationError("Could not determine the first latitude" - " and/or longitude grid point.") - - self._x_points = x1 + self.DxInMetres * np.arange(self.Nx, - dtype=np.float64) - self._y_points = y1 + self.DyInMetres * np.arange(self.Ny, - dtype=np.float64) - - elif gridType in ["reduced_ll", "reduced_gg"]: - self._x_points = self.longitudes - self._y_points = self.latitudes - - else: - raise TranslationError("unhandled grid type") - - def _regular_longitude_common(self): - """Define a regular longitude dimension.""" - i_step = self.iDirectionIncrementInDegrees - if self.iScansNegatively: - i_step = -i_step - self._x_points = (np.arange(self.Ni, dtype=np.float64) * i_step + - self.longitudeOfFirstGridPointInDegrees) - if "longitude" in self.extra_keys['_x_coord_name'] and self.Ni > 1: - if _longitude_is_cyclic(self._x_points): - self.extra_keys['_x_circular'] = True - - def _get_processing_done(self): - """Determine the type of processing that was done on the data.""" - - processingDone = 'unknown' - 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 - 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 grib1 timeRangeIndicator " - "= %i" % time_range_indicator) - - # Get the timeunit interval. - interval_secs = self._timeunit_seconds() - # Multiply by start-offset and convert to a timedelta. - # NOTE: a 'float' conversion is required here, as time_diff may be - # a numpy scalar, which timedelta will not accept. - interval_delta = datetime.timedelta( - seconds=float(time_diff * interval_secs)) - # Return validity_time = (reference_time + start_offset*time_unit). - return reference_date_time + interval_delta - - @property - def bmdi(self): - # Not sure of any cases where GRIB provides a fill value. - # Default for fill value is None. - return None - - def core_data(self): - try: - data = self._data - except AttributeError: - data = self.data - return data - - def phenomenon_points(self, time_unit): - """ - Return the phenomenon time point offset from the epoch time reference - measured in the appropriate time units. - - """ - time_reference = '%s since epoch' % time_unit - return cf_units.date2num(self._phenomenonDateTime, time_reference, - cf_units.CALENDAR_GREGORIAN) - - def phenomenon_bounds(self, time_unit): - """ - Return the phenomenon time bound offsets from the epoch time reference - measured in the appropriate time units. - - """ - # TODO #576 Investigate when it's valid to get phenomenon_bounds - time_reference = '%s since epoch' % time_unit - unit = cf_units.Unit(time_reference, cf_units.CALENDAR_GREGORIAN) - return [unit.date2num(self._periodStartDateTime), - unit.date2num(self._periodEndDateTime)] - - -def _longitude_is_cyclic(points): - """Work out if a set of longitude points is cyclic.""" - # Is the gap from end to start smaller, or about equal to the max step? - gap = 360.0 - abs(points[-1] - points[0]) - max_step = abs(np.diff(points)).max() - cyclic = False - if gap <= max_step: - cyclic = True - else: - delta = 0.001 - if abs(1.0 - gap / max_step) < delta: - cyclic = True - return cyclic - - -def _message_values(grib_message, shape): - gribapi.grib_set_double(grib_message, 'missingValue', np.nan) - data = gribapi.grib_get_double_array(grib_message, 'values') - data = data.reshape(shape) - - # Handle missing values in a sensible way. - mask = np.isnan(data) - if mask.any(): - data = ma.array(data, mask=mask, fill_value=np.nan) - return data - - -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 - - -def load_cubes(filenames, callback=None): - """ - Returns a generator of cubes from the given list of filenames. - - Args: - - * filenames: - One or more GRIB filenames to load from. - - Kwargs: - - * callback: - Function which can be passed on to :func:`iris.io.run_callback`. - - Returns: - A generator containing Iris cubes loaded from the GRIB files. - - """ - 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): - """ - Convert an iterable of GRIB messages into an iterable of - (Cube, Grib message) tuples. - - 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 - removes a significant number of fields, the speed up to load can be - significant: - - >>> import iris - >>> from iris.fileformats.grib import load_pairs_from_fields - >>> from iris.fileformats.grib.message import GribMessage - >>> filename = iris.sample_data_path('polar_stereo.grib2') - >>> filtered_messages = [] - >>> for message in GribMessage.messages_from_filename(filename): - ... if message.sections[1]['productionStatusOfProcessedData'] == 0: - ... filtered_messages.append(message) - >>> cubes_messages = load_pairs_from_fields(filtered_messages) - >>> for cube, msg in cubes_messages: - ... prod_stat = msg.sections[1]['productionStatusOfProcessedData'] - ... cube.attributes['productionStatusOfProcessedData'] = prod_stat - >>> print(cube.attributes['productionStatusOfProcessedData']) - 0 - - This capability can also be used to alter fields before they are passed to - the load pipeline. Fields with out of specification header elements can - be cleaned up this way and cubes created: - - >>> from iris.fileformats.grib import load_pairs_from_fields - >>> cleaned_messages = GribMessage.messages_from_filename(filename) - >>> for message in cleaned_messages: - ... if message.sections[1]['productionStatusOfProcessedData'] == 0: - ... 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`). - - """ - 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): - """ - Save a cube or iterable of cubes to a GRIB2 file. - - Args: - - * 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. - - """ - messages = (message for _, message in save_pairs_from_cube(cube)) - save_messages(messages, target, append=append) - - -def save_pairs_from_cube(cube): - """ - Convert one or more cubes to (2D cube, GRIB message) pairs. - Returns an iterable of tuples each consisting of one 2D cube and - one GRIB message ID, the result of the 2D cube being processed by the GRIB - save rules. - - Args: - - * cube: - A :class:`iris.cube.Cube`, :class:`iris.cube.CubeList` or - list of cubes. - - """ - x_coords = cube.coords(axis='x', dim_coords=True) - y_coords = cube.coords(axis='y', dim_coords=True) - if len(x_coords) != 1 or len(y_coords) != 1: - raise TranslationError("Did not find one (and only one) x or y coord") - - # Save each latlon slice2D in the cube - for slice2D in cube.slices([y_coords[0], x_coords[0]]): - grib_message = gribapi.grib_new_from_samples("GRIB2") - _save_rules.run(slice2D, grib_message) - yield (slice2D, grib_message) - - -def save_messages(messages, target, append=False): - """ - Save messages to a GRIB2 file. - The messages will be released as part of the save. - - Args: - - * 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. - - """ - # grib file (this bit is common to the pp and grib savers...) - if isinstance(target, six.string_types): - grib_file = open(target, "ab" if append else "wb") - elif hasattr(target, "write"): - if hasattr(target, "mode") and "b" not in target.mode: - raise ValueError("Target not binary") - grib_file = target - else: - raise ValueError("Can only save grib to filename or writable") - - try: - for message in messages: - gribapi.grib_write(message, grib_file) - gribapi.grib_release(message) - finally: - # (this bit is common to the pp and grib savers...) - if isinstance(target, six.string_types): - grib_file.close() diff --git a/lib/iris/fileformats/grib/_grib1_load_rules.py b/lib/iris/fileformats/grib/_grib1_load_rules.py deleted file mode 100644 index 238ba1f3fc..0000000000 --- a/lib/iris/fileformats/grib/_grib1_load_rules.py +++ /dev/null @@ -1,266 +0,0 @@ -# (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 . -""" -Module to support the loading and conversion of a GRIB1 message into -cube metadata. - -# Historically this was auto-generated from -# SciTools/iris-code-generators:tools/gen_rules.py - -""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -from cf_units import CALENDAR_GREGORIAN, Unit - -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 - - -def grib1_convert(grib): - """ - Converts a GRIB1 message into the corresponding items of Cube metadata. - - Args: - - * grib: - A :class:`~iris.fileformats.grib.GribWrapper` object. - - Returns: - 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 - long_name = None - units = None - attributes = {} - cell_methods = [] - dim_coords_and_dims = [] - aux_coords_and_dims = [] - - 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)) - aux_coords_and_dims.append((AuxCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system), 0)) - - if \ - (grib.gridType=="regular_ll") and \ - (grib.jPointsAreConsecutive == 0): - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 0)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system, circular=grib._x_circular), 1)) - - if \ - (grib.gridType=="regular_ll") and \ - (grib.jPointsAreConsecutive == 1): - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 1)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system, circular=grib._x_circular), 0)) - - if \ - (grib.gridType=="regular_gg") and \ - (grib.jPointsAreConsecutive == 0): - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 0)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system, circular=grib._x_circular), 1)) - - if \ - (grib.gridType=="regular_gg") and \ - (grib.jPointsAreConsecutive == 1): - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 1)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system, circular=grib._x_circular), 0)) - - if \ - (grib.gridType=="rotated_ll") and \ - (grib.jPointsAreConsecutive == 0): - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 0)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system, circular=grib._x_circular), 1)) - - if \ - (grib.gridType=="rotated_ll") and \ - (grib.jPointsAreConsecutive == 1): - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units='degrees', coord_system=grib._coord_system), 1)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units='degrees', coord_system=grib._coord_system, circular=grib._x_circular), 0)) - - if grib.gridType in ["polar_stereographic", "lambert"]: - dim_coords_and_dims.append((DimCoord(grib._y_points, grib._y_coord_name, units="m", coord_system=grib._coord_system), 0)) - dim_coords_and_dims.append((DimCoord(grib._x_points, grib._x_coord_name, units="m", coord_system=grib._coord_system), 1)) - - if \ - (grib.table2Version < 128) and \ - (grib.indicatorOfParameter == 11) and \ - (grib._cf_data is None): - standard_name = "air_temperature" - units = "kelvin" - - if \ - (grib.table2Version < 128) and \ - (grib.indicatorOfParameter == 33) and \ - (grib._cf_data is None): - standard_name = "x_wind" - units = "m s-1" - - if \ - (grib.table2Version < 128) and \ - (grib.indicatorOfParameter == 34) and \ - (grib._cf_data is None): - standard_name = "y_wind" - units = "m s-1" - - if \ - (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.table2Version >= 128) and \ - (grib._cf_data is None): - long_name = "UNKNOWN LOCAL PARAM " + str(grib.indicatorOfParameter) + "." + str(grib.table2Version) - units = "???" - - if \ - (grib.table2Version == 1) and \ - (grib.indicatorOfParameter >= 128): - long_name = "UNKNOWN LOCAL PARAM " + str(grib.indicatorOfParameter) + "." + str(grib.table2Version) - units = "???" - - if \ - (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)) - - def add_bounded_time_coords(aux_coords_and_dims, grib): - t_bounds = grib.phenomenon_bounds('hours') - period = Unit('hours').convert(t_bounds[1] - t_bounds[0], - grib._forecastTimeUnit) - aux_coords_and_dims.append(( - DimCoord(standard_name='forecast_period', - units=grib._forecastTimeUnit, - points=grib._forecastTime + 0.5 * period, - bounds=[grib._forecastTime, grib._forecastTime + period]), - None)) - aux_coords_and_dims.append(( - DimCoord(standard_name='time', - units=Unit('hours since epoch', CALENDAR_GREGORIAN), - points=0.5 * (t_bounds[0] + t_bounds[1]), - bounds=t_bounds), - None)) - - if \ - (grib.timeRangeIndicator == 2): - add_bounded_time_coords(aux_coords_and_dims, grib) - - if \ - (grib.timeRangeIndicator == 3): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.timeRangeIndicator == 4): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("sum", coords="time")) - - if \ - (grib.timeRangeIndicator == 5): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("_difference", coords="time")) - - if \ - (grib.timeRangeIndicator == 51): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.timeRangeIndicator == 113): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.timeRangeIndicator == 114): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("sum", coords="time")) - - if \ - (grib.timeRangeIndicator == 115): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.timeRangeIndicator == 116): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("sum", coords="time")) - - if \ - (grib.timeRangeIndicator == 117): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.timeRangeIndicator == 118): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("_covariance", coords="time")) - - if \ - (grib.timeRangeIndicator == 123): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("mean", coords="time")) - - if \ - (grib.timeRangeIndicator == 124): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("sum", coords="time")) - - if \ - (grib.timeRangeIndicator == 125): - add_bounded_time_coords(aux_coords_and_dims, grib) - cell_methods.append(CellMethod("standard_deviation", coords="time")) - - if \ - (grib.levelType == 'pl'): - aux_coords_and_dims.append((DimCoord(points=grib.level, long_name="pressure", units="hPa"), None)) - - if \ - (grib.levelType == 'sfc'): - - if (grib._cf_data is not None) and \ - (grib._cf_data.set_height is not None): - aux_coords_and_dims.append((DimCoord(points=grib._cf_data.set_height, long_name="height", units="m", attributes={'positive':'up'}), None)) - elif grib.typeOfLevel == 'heightAboveGround': # required for NCAR - aux_coords_and_dims.append((DimCoord(points=grib.level, long_name="height", units="m", attributes={'positive':'up'}), None)) - - if \ - (grib.levelType == 'ml') and \ - (hasattr(grib, 'pv')): - aux_coords_and_dims.append((AuxCoord(grib.level, standard_name='model_level_number', attributes={'positive': 'up'}), None)) - aux_coords_and_dims.append((DimCoord(grib.pv[grib.level], long_name='level_pressure', units='Pa'), None)) - 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._originatingCentre != 'unknown': - aux_coords_and_dims.append((AuxCoord(points=grib._originatingCentre, long_name='originating_centre', units='no_unit'), None)) - - 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 deleted file mode 100644 index 372b4b8fd4..0000000000 --- a/lib/iris/fileformats/grib/_grib_cf_map.py +++ /dev/null @@ -1,228 +0,0 @@ -# (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 . -# -# DO NOT EDIT: AUTO-GENERATED -# Created on 14 October 2016 15:10 from -# http://www.metarelate.net/metOcean -# at commit 3cde018acc4303203ff006a26f7b96a64e6ed3fb - -# https://github.com/metarelate/metOcean/commit/3cde018acc4303203ff006a26f7b96a64e6ed3fb - -""" -Provides GRIB/CF phenomenon translations. - -""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -from collections import namedtuple - - -CFName = namedtuple('CFName', 'standard_name long_name units') - -DimensionCoordinate = namedtuple('DimensionCoordinate', - 'standard_name units points') - -G1LocalParam = namedtuple('G1LocalParam', 'edition t2version centre iParam') -G2Param = namedtuple('G2Param', 'edition discipline category number') - - -GRIB1_LOCAL_TO_CF_CONSTRAINED = { - G1LocalParam(1, 128, 98, 165): (CFName('x_wind', None, 'm s-1'), DimensionCoordinate('height', 'm', (10,))), - G1LocalParam(1, 128, 98, 166): (CFName('y_wind', None, 'm s-1'), DimensionCoordinate('height', 'm', (10,))), - G1LocalParam(1, 128, 98, 167): (CFName('air_temperature', None, 'K'), DimensionCoordinate('height', 'm', (2,))), - G1LocalParam(1, 128, 98, 168): (CFName('dew_point_temperature', None, 'K'), DimensionCoordinate('height', 'm', (2,))), - } - -GRIB1_LOCAL_TO_CF = { - G1LocalParam(1, 128, 98, 31): CFName('sea_ice_area_fraction', None, '1'), - G1LocalParam(1, 128, 98, 34): CFName('sea_surface_temperature', None, 'K'), - G1LocalParam(1, 128, 98, 59): CFName('atmosphere_specific_convective_available_potential_energy', None, 'J kg-1'), - G1LocalParam(1, 128, 98, 129): CFName('geopotential', None, 'm2 s-2'), - G1LocalParam(1, 128, 98, 130): CFName('air_temperature', None, 'K'), - G1LocalParam(1, 128, 98, 131): CFName('x_wind', None, 'm s-1'), - G1LocalParam(1, 128, 98, 132): CFName('y_wind', None, 'm s-1'), - G1LocalParam(1, 128, 98, 135): CFName('lagrangian_tendency_of_air_pressure', None, 'Pa s-1'), - G1LocalParam(1, 128, 98, 141): CFName('thickness_of_snowfall_amount', None, 'm'), - G1LocalParam(1, 128, 98, 151): CFName('air_pressure_at_sea_level', None, 'Pa'), - G1LocalParam(1, 128, 98, 157): CFName('relative_humidity', None, '%'), - G1LocalParam(1, 128, 98, 164): CFName('cloud_area_fraction', None, '1'), - G1LocalParam(1, 128, 98, 173): CFName('surface_roughness_length', None, 'm'), - G1LocalParam(1, 128, 98, 174): CFName(None, 'grib_physical_atmosphere_albedo', '1'), - G1LocalParam(1, 128, 98, 186): CFName('low_type_cloud_area_fraction', None, '1'), - G1LocalParam(1, 128, 98, 187): CFName('medium_type_cloud_area_fraction', None, '1'), - G1LocalParam(1, 128, 98, 188): CFName('high_type_cloud_area_fraction', None, '1'), - G1LocalParam(1, 128, 98, 235): CFName(None, 'grib_skin_temperature', 'K'), - } - -GRIB2_TO_CF = { - G2Param(2, 0, 0, 0): CFName('air_temperature', None, 'K'), - G2Param(2, 0, 0, 2): CFName('air_potential_temperature', None, 'K'), - G2Param(2, 0, 0, 6): CFName('dew_point_temperature', None, 'K'), - G2Param(2, 0, 0, 10): CFName('surface_upward_latent_heat_flux', None, 'W m-2'), - G2Param(2, 0, 0, 11): CFName('surface_upward_sensible_heat_flux', None, 'W m-2'), - G2Param(2, 0, 0, 17): CFName('surface_temperature', None, 'K'), - G2Param(2, 0, 1, 0): CFName('specific_humidity', None, 'kg kg-1'), - G2Param(2, 0, 1, 1): CFName('relative_humidity', None, '%'), - G2Param(2, 0, 1, 2): CFName('humidity_mixing_ratio', None, 'kg kg-1'), - G2Param(2, 0, 1, 3): CFName(None, 'precipitable_water', 'kg m-2'), - G2Param(2, 0, 1, 7): CFName('precipitation_flux', None, 'kg m-2 s-1'), - G2Param(2, 0, 1, 11): CFName('thickness_of_snowfall_amount', None, 'm'), - G2Param(2, 0, 1, 13): CFName('liquid_water_content_of_surface_snow', None, 'kg m-2'), - G2Param(2, 0, 1, 22): CFName(None, 'cloud_mixing_ratio', 'kg kg-1'), - 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'), - G2Param(2, 0, 2, 2): CFName('x_wind', None, 'm s-1'), - G2Param(2, 0, 2, 3): CFName('y_wind', None, 'm s-1'), - G2Param(2, 0, 2, 8): CFName('lagrangian_tendency_of_air_pressure', None, 'Pa s-1'), - G2Param(2, 0, 2, 10): CFName('atmosphere_absolute_vorticity', None, 's-1'), - G2Param(2, 0, 2, 14): CFName(None, 'ertel_potential_velocity', 'K m2 kg-1 s-1'), - G2Param(2, 0, 2, 22): CFName('wind_speed_of_gust', None, 'm s-1'), - G2Param(2, 0, 3, 0): CFName('air_pressure', None, 'Pa'), - G2Param(2, 0, 3, 1): CFName('air_pressure_at_sea_level', None, 'Pa'), - G2Param(2, 0, 3, 3): CFName(None, 'icao_standard_atmosphere_reference_height', 'm'), - G2Param(2, 0, 3, 4): CFName('geopotential', None, 'm2 s-2'), - G2Param(2, 0, 3, 5): CFName('geopotential_height', None, 'm'), - G2Param(2, 0, 3, 6): CFName('altitude', None, 'm'), - G2Param(2, 0, 3, 9): CFName('geopotential_height_anomaly', None, 'm'), - 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('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, '%'), - G2Param(2, 0, 6, 6): CFName('atmosphere_mass_content_of_cloud_liquid_water', None, 'kg m-2'), - G2Param(2, 0, 6, 7): CFName('cloud_area_fraction_in_atmosphere_layer', None, '%'), - G2Param(2, 0, 7, 6): CFName('atmosphere_specific_convective_available_potential_energy', None, 'J kg-1'), - G2Param(2, 0, 7, 7): CFName(None, 'convective_inhibition', 'J kg-1'), - 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'), - G2Param(2, 2, 0, 7): CFName('surface_altitude', None, 'm'), - G2Param(2, 2, 0, 22): CFName('moisture_content_of_soil_layer', None, 'kg m-2'), - G2Param(2, 2, 0, 34): CFName('surface_runoff_flux', None, 'kg m-2 s-1'), - G2Param(2, 10, 1, 2): CFName('sea_water_x_velocity', None, 'm s-1'), - G2Param(2, 10, 1, 3): CFName('sea_water_y_velocity', None, 'm s-1'), - G2Param(2, 10, 2, 0): CFName('sea_ice_area_fraction', None, '1'), - G2Param(2, 10, 3, 0): CFName('sea_surface_temperature', None, 'K'), - } - -CF_CONSTRAINED_TO_GRIB1_LOCAL = { - (CFName('air_temperature', None, 'K'), DimensionCoordinate('height', 'm', (2,))): G1LocalParam(1, 128, 98, 167), - (CFName('dew_point_temperature', None, 'K'), DimensionCoordinate('height', 'm', (2,))): G1LocalParam(1, 128, 98, 168), - (CFName('x_wind', None, 'm s-1'), DimensionCoordinate('height', 'm', (10,))): G1LocalParam(1, 128, 98, 165), - (CFName('y_wind', None, 'm s-1'), DimensionCoordinate('height', 'm', (10,))): G1LocalParam(1, 128, 98, 166), - } - -CF_TO_GRIB1_LOCAL = { - CFName(None, 'grib_physical_atmosphere_albedo', '1'): G1LocalParam(1, 128, 98, 174), - CFName(None, 'grib_skin_temperature', 'K'): G1LocalParam(1, 128, 98, 235), - CFName('air_pressure_at_sea_level', None, 'Pa'): G1LocalParam(1, 128, 98, 151), - CFName('air_temperature', None, 'K'): G1LocalParam(1, 128, 98, 130), - CFName('atmosphere_specific_convective_available_potential_energy', None, 'J kg-1'): G1LocalParam(1, 128, 98, 59), - CFName('cloud_area_fraction', None, '1'): G1LocalParam(1, 128, 98, 164), - CFName('geopotential', None, 'm2 s-2'): G1LocalParam(1, 128, 98, 129), - CFName('high_type_cloud_area_fraction', None, '1'): G1LocalParam(1, 128, 98, 188), - CFName('lagrangian_tendency_of_air_pressure', None, 'Pa s-1'): G1LocalParam(1, 128, 98, 135), - CFName('low_type_cloud_area_fraction', None, '1'): G1LocalParam(1, 128, 98, 186), - CFName('medium_type_cloud_area_fraction', None, '1'): G1LocalParam(1, 128, 98, 187), - CFName('relative_humidity', None, '%'): G1LocalParam(1, 128, 98, 157), - CFName('sea_ice_area_fraction', None, '1'): G1LocalParam(1, 128, 98, 31), - CFName('sea_surface_temperature', None, 'K'): G1LocalParam(1, 128, 98, 34), - CFName('surface_roughness_length', None, 'm'): G1LocalParam(1, 128, 98, 173), - CFName('thickness_of_snowfall_amount', None, 'm'): G1LocalParam(1, 128, 98, 141), - CFName('x_wind', None, 'm s-1'): G1LocalParam(1, 128, 98, 131), - CFName('y_wind', None, 'm s-1'): G1LocalParam(1, 128, 98, 132), - } - -CF_TO_GRIB2 = { - CFName(None, 'cloud_area_fraction_assuming_maximum_random_overlap', '1'): G2Param(2, 0, 6, 1), - CFName(None, 'cloud_mixing_ratio', 'kg kg-1'): G2Param(2, 0, 1, 22), - CFName(None, 'convective_inhibition', 'J kg-1'): G2Param(2, 0, 7, 7), - CFName(None, 'ertel_potential_velocity', 'K m2 kg-1 s-1'): G2Param(2, 0, 2, 14), - CFName(None, 'grib_physical_atmosphere_albedo', '%'): G2Param(2, 0, 19, 1), - CFName(None, 'icao_standard_atmosphere_reference_height', 'm'): G2Param(2, 0, 3, 3), - CFName(None, 'precipitable_water', 'kg m-2'): G2Param(2, 0, 1, 3), - 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), - CFName('atmosphere_absolute_vorticity', None, 's-1'): G2Param(2, 0, 2, 10), - CFName('atmosphere_mass_content_of_cloud_liquid_water', None, 'kg m-2'): G2Param(2, 0, 6, 6), - CFName('atmosphere_mass_content_of_water', None, 'kg m-2'): G2Param(2, 0, 1, 51), - 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_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), - CFName('geopotential_height', None, 'm'): G2Param(2, 0, 3, 5), - CFName('geopotential_height_anomaly', None, 'm'): G2Param(2, 0, 3, 9), - CFName('high_type_cloud_area_fraction', None, '%'): G2Param(2, 0, 6, 5), - CFName('humidity_mixing_ratio', None, 'kg kg-1'): G2Param(2, 0, 1, 2), - CFName('lagrangian_tendency_of_air_pressure', None, 'Pa s-1'): G2Param(2, 0, 2, 8), - CFName('land_area_fraction', None, '1'): G2Param(2, 2, 0, 0), - CFName('land_binary_mask', None, '1'): G2Param(2, 2, 0, 0), - CFName('liquid_water_content_of_surface_snow', None, 'kg m-2'): G2Param(2, 0, 1, 13), - CFName('low_type_cloud_area_fraction', None, '%'): G2Param(2, 0, 6, 3), - CFName('medium_type_cloud_area_fraction', None, '%'): G2Param(2, 0, 6, 4), - CFName('moisture_content_of_soil_layer', None, 'kg m-2'): G2Param(2, 2, 0, 22), - CFName('precipitation_amount', None, 'kg m-2'): G2Param(2, 0, 1, 49), - CFName('precipitation_flux', None, 'kg m-2 s-1'): G2Param(2, 0, 1, 7), - CFName('relative_humidity', None, '%'): G2Param(2, 0, 1, 1), - CFName('sea_ice_area_fraction', None, '1'): G2Param(2, 10, 2, 0), - 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), - CFName('surface_air_pressure', None, 'Pa'): G2Param(2, 0, 3, 0), - CFName('surface_altitude', None, 'm'): G2Param(2, 2, 0, 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), - CFName('surface_temperature', None, 'K'): G2Param(2, 0, 0, 17), - 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('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), - CFName('x_wind', None, 'm s-1'): G2Param(2, 0, 2, 2), - CFName('y_wind', None, 'm s-1'): G2Param(2, 0, 2, 3), - } diff --git a/lib/iris/fileformats/grib/_load_convert.py b/lib/iris/fileformats/grib/_load_convert.py deleted file mode 100644 index 9ce3df9137..0000000000 --- a/lib/iris/fileformats/grib/_load_convert.py +++ /dev/null @@ -1,2406 +0,0 @@ -# (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 . -""" -Module to support the loading and conversion of a GRIB2 message into -cube metadata. - -""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa - -from argparse import Namespace -from collections import namedtuple, Iterable, OrderedDict -from datetime import datetime, timedelta -import math -import warnings - -import cartopy.crs as ccrs -from cf_units import CALENDAR_GREGORIAN, date2num, Unit -import numpy as np -import numpy.ma as ma - -from iris.aux_factory import HybridPressureFactory -import iris.coord_systems as icoord_systems -from iris.coords import AuxCoord, DimCoord, CellMethod -from iris.exceptions import TranslationError -from iris.fileformats.grib import grib_phenom_translation as itranslation -from iris.fileformats.grib._grib1_load_rules import grib1_convert -from iris.fileformats.rules import ConversionMetadata, Factory, Reference -from iris.util import _is_circular - - -# Restrict the names imported from this namespace. -__all__ = ['convert'] - - -options = Namespace(warn_on_unsupported=False, - support_hindcast_values=True) - -ScanningMode = namedtuple('ScanningMode', ['i_negative', - 'j_positive', - 'j_consecutive', - 'i_alternative']) - -ProjectionCentre = namedtuple('ProjectionCentre', - ['south_pole_on_projection_plane', - 'bipolar_and_symmetric']) - -ResolutionFlags = namedtuple('ResolutionFlags', - ['i_increments_given', - 'j_increments_given', - 'uv_resolved']) - -FixedSurface = namedtuple('FixedSurface', ['standard_name', - 'long_name', - 'units']) - -# Regulations 92.1.6. -_GRID_ACCURACY_IN_DEGREES = 1e-6 # 1/1,000,000 of a degree - -# Reference Common Code Table C-1. -_CENTRES = { - 'ecmf': 'European Centre for Medium Range Weather Forecasts' -} - -# Reference Code Table 1.0 -_CODE_TABLES_MISSING = 255 - -# UDUNITS-2 units time string. Reference GRIB2 Code Table 4.4. -_TIME_RANGE_UNITS = { - 0: 'minutes', - 1: 'hours', - 2: 'days', - # 3: 'months', Unsupported - # 4: 'years', Unsupported - # 5: '10 years', Unsupported - # 6: '30 years', Unsupported - # 7: '100 years', Unsupported - # 8-9 Reserved - 10: '3 hours', - 11: '6 hours', - 12: '12 hours', - 13: 'seconds' -} - -# Reference Code Table 4.5. -_FIXED_SURFACE = { - 100: FixedSurface(None, 'pressure', 'Pa'), # Isobaric surface - 103: FixedSurface(None, 'height', 'm') # Height level above ground -} -_TYPE_OF_FIXED_SURFACE_MISSING = 255 - -# Reference Code Table 6.0 -_BITMAP_CODE_PRESENT = 0 -_BITMAP_CODE_NONE = 255 - -# Reference Code Table 4.10. -_STATISTIC_TYPE_NAMES = { - 0: 'mean', - 1: 'sum', - 2: 'maximum', - 3: 'minimum', - 6: 'standard_deviation' -} - -# Reference Code Table 4.11. -_STATISTIC_TYPE_OF_TIME_INTERVAL = { - 2: 'same start time of forecast, forecast time is incremented' -} -# NOTE: Our test data contains the value 2, which is all we currently support. -# The exact interpretation of this is still unclear. - -# Class containing details of a probability analysis. -Probability = namedtuple('Probability', - ('probability_type_name', 'threshold')) - - -# Regulation 92.1.12 -def unscale(value, factor): - """ - Implements Regulation 92.1.12. - - Args: - - * value: - Scaled value or sequence of scaled values. - - * factor: - Scale factor or sequence of scale factors. - - Returns: - For scalar value and factor, the unscaled floating point - result is returned. If either value and/or factor are - MDI, then :data:`numpy.ma.masked` is returned. - - For sequence value and factor, the unscaled floating point - :class:`numpy.ndarray` is returned. If either value and/or - factor contain MDI, then :class:`numpy.ma.core.MaskedArray` - is returned. - - """ - def _unscale(v, f): - return v / 10.0 ** f - - if isinstance(value, Iterable) or isinstance(factor, Iterable): - def _masker(item): - result = ma.masked_equal(item, _MDI) - if ma.count_masked(result): - # Circumvent downstream NumPy "RuntimeWarning" - # of "overflow encountered in power" in _unscale - # for data containing _MDI. - result.data[result.mask] = 0 - return result - value = _masker(value) - factor = _masker(factor) - result = _unscale(value, factor) - if ma.count_masked(result) == 0: - result = result.data - else: - result = ma.masked - if value != _MDI and factor != _MDI: - result = _unscale(value, factor) - return result - - -# Regulations 92.1.4 and 92.1.5. -_MDI = 2 ** 32 - 1 -# Note: -# 1. Integer "on-disk" values (aka. coded keys) in GRIB messages: -# - Are 8-, 16-, or 32-bit. -# - Are either signed or unsigned, with signed values stored as -# sign-and-magnitude (*not* twos-complement). -# - Use all bits set to indicate a missing value (MDI). -# 2. Irrespective of the on-disk form, the ECMWF GRIB API *always*: -# - Returns values as 64-bit signed integers, either as native -# Python 'int' or numpy 'int64'. -# - Returns missing values as 2**32 - 1, but not all keys are -# defined as supporting missing values. -# NB. For keys which support missing values, the MDI value is reliably -# distinct from the valid range of either signed or unsigned 8-, 16-, -# or 32-bit values. For example: -# unsigned 32-bit: -# min = 0b000...000 = 0 -# max = 0b111...110 = 2**32 - 2 -# MDI = 0b111...111 = 2**32 - 1 -# signed 32-bit: -# MDI = 0b111...111 = 2**32 - 1 -# min = 0b111...110 = -(2**31 - 2) -# max = 0b011...111 = 2**31 - 1 - - -# Non-standardised usage for negative forecast times. -def _hindcast_fix(forecast_time): - """Return a forecast time interpreted as a possibly negative value.""" - uft = np.uint32(forecast_time) - HIGHBIT = 2**30 - - # Workaround grib api's assumption that forecast time is positive. - # Handles correctly encoded -ve forecast times up to one -1 billion. - if 2 * HIGHBIT < uft < 3 * HIGHBIT: - original_forecast_time = forecast_time - forecast_time = -(uft - 2 * HIGHBIT) - if options.warn_on_unsupported: - msg = ('Re-interpreting large grib forecastTime ' - 'from {} to {}.'.format(original_forecast_time, - forecast_time)) - warnings.warn(msg) - - return forecast_time - - -def fixup_float32_from_int32(value): - """ - Workaround for use when reading an IEEE 32-bit floating-point value - which the ECMWF GRIB API has erroneously treated as a 4-byte signed - integer. - - """ - # Convert from two's complement to sign-and-magnitude. - # NB. The bit patterns 0x00000000 and 0x80000000 will both be - # returned by the ECMWF GRIB API as an integer 0. Because they - # correspond to positive and negative zero respectively it is safe - # to treat an integer 0 as a positive zero. - if value < 0: - value = 0x80000000 - value - value_as_uint32 = np.array(value, dtype='u4') - value_as_float32 = value_as_uint32.view(dtype='f4') - return float(value_as_float32) - - -def fixup_int32_from_uint32(value): - """ - Workaround for use when reading a signed, 4-byte integer which the - ECMWF GRIB API has erroneously treated as an unsigned, 4-byte - integer. - - NB. This workaround is safe to use with values which are already - treated as signed, 4-byte integers. - - """ - if value >= 0x80000000: - value = 0x80000000 - value - return value - - -############################################################################### -# -# Identification Section 1 -# -############################################################################### - -def reference_time_coord(section): - """ - Translate section 1 reference time according to its significance. - - Reference section 1, year octets 13-14, month octet 15, day octet 16, - hour octet 17, minute octet 18, second octet 19. - - Returns: - The scalar reference time :class:`iris.coords.DimCoord`. - - """ - # Look-up standard name by significanceOfReferenceTime. - _lookup = {0: 'forecast_reference_time', - 1: 'forecast_reference_time', - 2: 'time', - 3: 'time'} - - # Calculate the reference time and units. - dt = datetime(section['year'], section['month'], section['day'], - section['hour'], section['minute'], section['second']) - # XXX Defaulting to a Gregorian calendar. - # Current GRIBAPI does not cover GRIB Section 1 - Octets 22-nn (optional) - # which are part of GRIB spec v12. - unit = Unit('hours since epoch', calendar=CALENDAR_GREGORIAN) - point = unit.date2num(dt) - - # Reference Code Table 1.2. - significanceOfReferenceTime = section['significanceOfReferenceTime'] - standard_name = _lookup.get(significanceOfReferenceTime) - - if standard_name is None: - msg = 'Identificaton section 1 contains an unsupported significance ' \ - 'of reference time [{}]'.format(significanceOfReferenceTime) - raise TranslationError(msg) - - # Create the associated reference time of data coordinate. - coord = DimCoord(point, standard_name=standard_name, units=unit) - - return coord - - -############################################################################### -# -# Grid Definition Section 3 -# -############################################################################### - -def projection_centre(projectionCentreFlag): - """ - Translate the projection centre flag bitmask. - - Reference GRIB2 Flag Table 3.5. - - Args: - - * projectionCentreFlag - Message section 3, coded key value. - - Returns: - A :class:`collections.namedtuple` representation. - - """ - south_pole_on_projection_plane = bool(projectionCentreFlag & 0x80) - bipolar_and_symmetric = bool(projectionCentreFlag & 0x40) - return ProjectionCentre(south_pole_on_projection_plane, - bipolar_and_symmetric) - - -def scanning_mode(scanningMode): - """ - Translate the scanning mode bitmask. - - Reference GRIB2 Flag Table 3.4. - - Args: - - * scanningMode: - Message section 3, coded key value. - - Returns: - A :class:`collections.namedtuple` representation. - - """ - i_negative = bool(scanningMode & 0x80) - j_positive = bool(scanningMode & 0x40) - j_consecutive = bool(scanningMode & 0x20) - i_alternative = bool(scanningMode & 0x10) - - if i_alternative: - msg = 'Grid definition section 3 contains unsupported ' \ - 'alternative row scanning mode' - raise TranslationError(msg) - - return ScanningMode(i_negative, j_positive, - j_consecutive, i_alternative) - - -def resolution_flags(resolutionAndComponentFlags): - """ - Translate the resolution and component bitmask. - - Reference GRIB2 Flag Table 3.3. - - Args: - - * resolutionAndComponentFlags: - Message section 3, coded key value. - - Returns: - A :class:`collections.namedtuple` representation. - - """ - i_inc_given = bool(resolutionAndComponentFlags & 0x20) - j_inc_given = bool(resolutionAndComponentFlags & 0x10) - uv_resolved = bool(resolutionAndComponentFlags & 0x08) - - return ResolutionFlags(i_inc_given, j_inc_given, uv_resolved) - - -def ellipsoid(shapeOfTheEarth, major, minor, radius): - """ - Translate the shape of the earth to an appropriate coordinate - reference system. - - For MDI set either major and minor or radius to :data:`numpy.ma.masked` - - Reference GRIB2 Code Table 3.2. - - Args: - - * shapeOfTheEarth: - Message section 3, octet 15. - - * major: - Semi-major axis of the oblate spheroid in units determined by - the shapeOfTheEarth. - - * minor: - Semi-minor axis of the oblate spheroid in units determined by - the shapeOfTheEarth. - - * radius: - Radius of sphere (in m). - - Returns: - :class:`iris.coord_systems.CoordSystem` - - """ - # Supported shapeOfTheEarth values. - if shapeOfTheEarth not in (0, 1, 2, 3, 4, 5, 6, 7): - msg = 'Grid definition section 3 contains an unsupported ' \ - 'shape of the earth [{}]'.format(shapeOfTheEarth) - raise TranslationError(msg) - - if shapeOfTheEarth == 0: - # Earth assumed spherical with radius of 6 367 470.0m - result = icoord_systems.GeogCS(6367470) - elif shapeOfTheEarth == 1: - # Earth assumed spherical with radius specified (in m) by - # data producer. - if ma.is_masked(radius): - msg = 'Ellipsoid for shape of the earth {} requires a' \ - 'radius to be specified.'.format(shapeOfTheEarth) - raise ValueError(msg) - result = icoord_systems.GeogCS(radius) - if shapeOfTheEarth == 2: - # Earth assumed oblate spheroid with size as determined by IAU in 1965. - result = icoord_systems.GeogCS(6378160, inverse_flattening=297.0) - elif shapeOfTheEarth in [3, 7]: - # Earth assumed oblate spheroid with major and minor axes - # specified (in km)/(in m) by data producer. - emsg_oblate = 'Ellipsoid for shape of the earth [{}] requires a' \ - 'semi-{} axis to be specified.' - if ma.is_masked(major): - raise ValueError(emsg_oblate.format(shapeOfTheEarth, 'major')) - if ma.is_masked(minor): - raise ValueError(emsg_oblate.format(shapeOfTheEarth, 'minor')) - # Check whether to convert from km to m. - if shapeOfTheEarth == 3: - major *= 1000 - minor *= 1000 - result = icoord_systems.GeogCS(major, minor) - if shapeOfTheEarth == 4: - # Earth assumed oblate spheroid as defined in IAG-GRS80 model. - result = icoord_systems.GeogCS(6378137, - inverse_flattening=298.257222101) - if shapeOfTheEarth == 5: - # Earth assumed represented by WGS84 (as used by ICAO since 1998). - result = icoord_systems.GeogCS(6378137, - inverse_flattening=298.257223563) - elif shapeOfTheEarth == 6: - # Earth assumed spherical with radius of 6 371 229.0m - result = icoord_systems.GeogCS(6371229) - - return result - - -def ellipsoid_geometry(section): - """ - Calculated the unscaled ellipsoid major-axis, minor-axis and radius. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - Returns: - Tuple containing the major-axis, minor-axis and radius. - - """ - major = unscale(section['scaledValueOfEarthMajorAxis'], - section['scaleFactorOfEarthMajorAxis']) - minor = unscale(section['scaledValueOfEarthMinorAxis'], - section['scaleFactorOfEarthMinorAxis']) - radius = unscale(section['scaledValueOfRadiusOfSphericalEarth'], - section['scaleFactorOfRadiusOfSphericalEarth']) - return major, minor, radius - - -def grid_definition_template_0_and_1(section, metadata, y_name, x_name, cs): - """ - Translate templates representing regularly spaced latitude/longitude - on either a standard or rotated grid. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message - - * metadata: - :class:`collections.OrderedDict` of metadata. - - * y_name: - Name of the Y coordinate, e.g. latitude or grid_latitude. - - * x_name: - Name of the X coordinate, e.g. longitude or grid_longitude. - - * cs: - The :class:`iris.coord_systems.CoordSystem` to use when creating - the X and Y coordinates. - - """ - # Abort if this is a reduced grid, that case isn't handled yet. - if section['numberOfOctectsForNumberOfPoints'] != 0 or \ - section['interpretationOfNumberOfPoints'] != 0: - msg = 'Grid definition section 3 contains unsupported ' \ - 'quasi-regular grid' - raise TranslationError(msg) - - scan = scanning_mode(section['scanningMode']) - - # Calculate longitude points. - x_inc = section['iDirectionIncrement'] * _GRID_ACCURACY_IN_DEGREES - x_offset = section['longitudeOfFirstGridPoint'] * _GRID_ACCURACY_IN_DEGREES - x_direction = -1 if scan.i_negative else 1 - Ni = section['Ni'] - x_points = np.arange(Ni, dtype=np.float64) * x_inc * x_direction + x_offset - - # Determine whether the x-points (in degrees) are circular. - circular = _is_circular(x_points, 360.0) - - # Calculate latitude points. - y_inc = section['jDirectionIncrement'] * _GRID_ACCURACY_IN_DEGREES - y_offset = section['latitudeOfFirstGridPoint'] * _GRID_ACCURACY_IN_DEGREES - y_direction = 1 if scan.j_positive else -1 - Nj = section['Nj'] - y_points = np.arange(Nj, dtype=np.float64) * y_inc * y_direction + y_offset - - # Create the lat/lon coordinates. - y_coord = DimCoord(y_points, standard_name=y_name, units='degrees', - coord_system=cs) - x_coord = DimCoord(x_points, standard_name=x_name, units='degrees', - coord_system=cs, circular=circular) - - # Determine the lat/lon dimensions. - y_dim, x_dim = 0, 1 - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the lat/lon coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def grid_definition_template_0(section, metadata): - """ - Translate template representing regular latitude/longitude - grid (regular_ll). - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - # Determine the coordinate system. - major, minor, radius = ellipsoid_geometry(section) - cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - grid_definition_template_0_and_1(section, metadata, - 'latitude', 'longitude', cs) - - -def grid_definition_template_1(section, metadata): - """ - Translate template representing rotated latitude/longitude grid. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - # Determine the coordinate system. - major, minor, radius = ellipsoid_geometry(section) - south_pole_lat = (section['latitudeOfSouthernPole'] * - _GRID_ACCURACY_IN_DEGREES) - south_pole_lon = (section['longitudeOfSouthernPole'] * - _GRID_ACCURACY_IN_DEGREES) - cs = icoord_systems.RotatedGeogCS(-south_pole_lat, - math.fmod(south_pole_lon + 180, 360), - section['angleOfRotation'], - ellipsoid(section['shapeOfTheEarth'], - major, minor, radius)) - grid_definition_template_0_and_1(section, metadata, - 'grid_latitude', 'grid_longitude', cs) - - -def grid_definition_template_4_and_5(section, metadata, y_name, x_name, cs): - """ - Translate template representing variable resolution latitude/longitude - and common variable resolution rotated latitude/longitude. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - * y_name: - Name of the Y coordinate, e.g. 'latitude' or 'grid_latitude'. - - * x_name: - Name of the X coordinate, e.g. 'longitude' or 'grid_longitude'. - - * cs: - The :class:`iris.coord_systems.CoordSystem` to use when createing - the X and Y coordinates. - - """ - # Determine the (variable) units of resolution. - key = 'basicAngleOfTheInitialProductionDomain' - basicAngleOfTheInitialProductDomain = section[key] - subdivisionsOfBasicAngle = section['subdivisionsOfBasicAngle'] - - if basicAngleOfTheInitialProductDomain in [0, _MDI]: - basicAngleOfTheInitialProductDomain = 1. - - if subdivisionsOfBasicAngle in [0, _MDI]: - subdivisionsOfBasicAngle = 1. / _GRID_ACCURACY_IN_DEGREES - - resolution = np.float64(basicAngleOfTheInitialProductDomain) - resolution /= subdivisionsOfBasicAngle - flags = resolution_flags(section['resolutionAndComponentFlags']) - - # Grid Definition Template 3.4. Notes (2). - # Flag bits 3-4 are not applicable for this template. - if flags.uv_resolved and options.warn_on_unsupported: - msg = 'Unable to translate resolution and component flags.' - warnings.warn(msg) - - # Calculate the latitude and longitude points. - x_points = np.array(section['longitudes'], dtype=np.float64) * resolution - y_points = np.array(section['latitudes'], dtype=np.float64) * resolution - - # Determine whether the x-points (in degrees) are circular. - circular = _is_circular(x_points, 360.0) - - # Create the lat/lon coordinates. - y_coord = DimCoord(y_points, standard_name=y_name, units='degrees', - coord_system=cs) - x_coord = DimCoord(x_points, standard_name=x_name, units='degrees', - coord_system=cs, circular=circular) - - scan = scanning_mode(section['scanningMode']) - - # Determine the lat/lon dimensions. - y_dim, x_dim = 0, 1 - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the lat/lon coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def grid_definition_template_4(section, metadata): - """ - Translate template representing variable resolution latitude/longitude. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - # Determine the coordinate system. - major, minor, radius = ellipsoid_geometry(section) - cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - grid_definition_template_4_and_5(section, metadata, - 'latitude', 'longitude', cs) - - -def grid_definition_template_5(section, metadata): - """ - Translate template representing variable resolution rotated - latitude/longitude. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - # Determine the coordinate system. - major, minor, radius = ellipsoid_geometry(section) - south_pole_lat = (section['latitudeOfSouthernPole'] * - _GRID_ACCURACY_IN_DEGREES) - south_pole_lon = (section['longitudeOfSouthernPole'] * - _GRID_ACCURACY_IN_DEGREES) - cs = icoord_systems.RotatedGeogCS(-south_pole_lat, - math.fmod(south_pole_lon + 180, 360), - section['angleOfRotation'], - ellipsoid(section['shapeOfTheEarth'], - major, minor, radius)) - grid_definition_template_4_and_5(section, metadata, - 'grid_latitude', 'grid_longitude', cs) - - -def grid_definition_template_12(section, metadata): - """ - Translate template representing transverse Mercator. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - major, minor, radius = ellipsoid_geometry(section) - geog_cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - - lat = section['latitudeOfReferencePoint'] * _GRID_ACCURACY_IN_DEGREES - lon = section['longitudeOfReferencePoint'] * _GRID_ACCURACY_IN_DEGREES - scale = section['scaleFactorAtReferencePoint'] - # Catch bug in ECMWF GRIB API (present at 1.12.1) where the scale - # is treated as a signed, 4-byte integer. - if isinstance(scale, int): - scale = fixup_float32_from_int32(scale) - CM_TO_M = 0.01 - easting = section['XR'] * CM_TO_M - northing = section['YR'] * CM_TO_M - cs = icoord_systems.TransverseMercator(lat, lon, easting, northing, - scale, geog_cs) - - # Deal with bug in ECMWF GRIB API (present at 1.12.1) where these - # values are treated as unsigned, 4-byte integers. - x1 = fixup_int32_from_uint32(section['X1']) - y1 = fixup_int32_from_uint32(section['Y1']) - x2 = fixup_int32_from_uint32(section['X2']) - y2 = fixup_int32_from_uint32(section['Y2']) - - # Rather unhelpfully this grid definition template seems to be - # overspecified, and thus open to inconsistency. But for determining - # the extents the X1, Y1, X2, and Y2 points have the highest - # precision, as opposed to using Di and Dj. - # Check whether Di and Dj are as consistent as possible with that - # interpretation - i.e. they are within 1cm. - def check_range(v1, v2, n, d): - min_last = v1 + (n - 1) * (d - 1) - max_last = v1 + (n - 1) * (d + 1) - if not (min_last < v2 < max_last): - raise TranslationError('Inconsistent grid definition') - check_range(x1, x2, section['Ni'], section['Di']) - check_range(y1, y2, section['Nj'], section['Dj']) - - x_points = np.linspace(x1 * CM_TO_M, x2 * CM_TO_M, section['Ni']) - y_points = np.linspace(y1 * CM_TO_M, y2 * CM_TO_M, section['Nj']) - - # This has only been tested with +x/+y scanning, so raise an error - # for other permutations. - scan = scanning_mode(section['scanningMode']) - if scan.i_negative: - raise TranslationError('Unsupported -x scanning') - if not scan.j_positive: - raise TranslationError('Unsupported -y scanning') - - # Create the X and Y coordinates. - y_coord = DimCoord(y_points, 'projection_y_coordinate', units='m', - coord_system=cs) - x_coord = DimCoord(x_points, 'projection_x_coordinate', units='m', - coord_system=cs) - - # Determine the lat/lon dimensions. - y_dim, x_dim = 0, 1 - scan = scanning_mode(section['scanningMode']) - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the X and Y coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def grid_definition_template_20(section, metadata): - """ - Translate template representing a Polar Stereographic grid. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - major, minor, radius = ellipsoid_geometry(section) - geog_cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - - proj_centre = projection_centre(section['projectionCentreFlag']) - if proj_centre.bipolar_and_symmetric: - raise TranslationError('Bipolar and symmetric polar stereo projections' - ' are not supported by the ' - 'grid_definition_template_20 translation.') - if proj_centre.south_pole_on_projection_plane: - central_lat = -90. - else: - central_lat = 90. - central_lon = section['orientationOfTheGrid'] * _GRID_ACCURACY_IN_DEGREES - true_scale_lat = section['LaD'] * _GRID_ACCURACY_IN_DEGREES - cs = icoord_systems.Stereographic(central_lat=central_lat, - central_lon=central_lon, - true_scale_lat=true_scale_lat, - ellipsoid=geog_cs) - x_coord, y_coord, scan = _calculate_proj_coords_from_lon_lat(section, cs) - - # Determine the order of the dimensions. - y_dim, x_dim = 0, 1 - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the projection coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def _calculate_proj_coords_from_lon_lat(section, cs): - # Construct the coordinate points, the start point is given in millidegrees - # but the distance measurement is in 10-3 m, so a conversion is necessary - # to find the origin in m. - - scan = scanning_mode(section['scanningMode']) - lon_0 = section['longitudeOfFirstGridPoint'] * _GRID_ACCURACY_IN_DEGREES - lat_0 = section['latitudeOfFirstGridPoint'] * _GRID_ACCURACY_IN_DEGREES - x0_m, y0_m = cs.as_cartopy_crs().transform_point( - lon_0, lat_0, ccrs.Geodetic()) - dx_m = section['Dx'] * 1e-3 - dy_m = section['Dy'] * 1e-3 - x_dir = -1 if scan.i_negative else 1 - y_dir = 1 if scan.j_positive else -1 - x_points = x0_m + dx_m * x_dir * np.arange(section['Nx'], dtype=np.float64) - y_points = y0_m + dy_m * y_dir * np.arange(section['Ny'], dtype=np.float64) - - # Create the dimension coordinates. - x_coord = DimCoord(x_points, standard_name='projection_x_coordinate', - units='m', coord_system=cs) - y_coord = DimCoord(y_points, standard_name='projection_y_coordinate', - units='m', coord_system=cs) - return x_coord, y_coord, scan - - -def grid_definition_template_30(section, metadata): - """ - Translate template representing a Lambert Conformal grid. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - major, minor, radius = ellipsoid_geometry(section) - geog_cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - - central_latitude = section['LaD'] * _GRID_ACCURACY_IN_DEGREES - central_longitude = section['LoV'] * _GRID_ACCURACY_IN_DEGREES - false_easting = 0 - false_northing = 0 - secant_latitudes = (section['Latin1'] * _GRID_ACCURACY_IN_DEGREES, - section['Latin2'] * _GRID_ACCURACY_IN_DEGREES) - - cs = icoord_systems.LambertConformal(central_latitude, - central_longitude, - false_easting, - false_northing, - secant_latitudes=secant_latitudes, - ellipsoid=geog_cs) - - # A projection centre flag is defined for GDT30. However, we don't need to - # know which pole is in the projection plane as Cartopy handles that. The - # Other component of the projection centre flag determines if there are - # multiple projection centres. There is no support for this in Proj4 or - # Cartopy so a translation error is raised if this flag is set. - proj_centre = projection_centre(section['projectionCentreFlag']) - if proj_centre.bipolar_and_symmetric: - msg = 'Unsupported projection centre: Bipolar and symmetric.' - raise TranslationError(msg) - - res_flags = resolution_flags(section['resolutionAndComponentFlags']) - if not res_flags.uv_resolved and options.warn_on_unsupported: - # Vector components are given as relative to east an north, rather than - # relative to the projection coordinates, issue a warning in this case. - # (ideally we need a way to add this information to a cube) - msg = 'Unable to translate resolution and component flags.' - warnings.warn(msg) - - x_coord, y_coord, scan = _calculate_proj_coords_from_lon_lat(section, cs) - - # Determine the order of the dimensions. - y_dim, x_dim = 0, 1 - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the projection coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def grid_definition_template_40(section, metadata): - """ - Translate template representing a Gaussian grid. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - major, minor, radius = ellipsoid_geometry(section) - cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - - if section['numberOfOctectsForNumberOfPoints'] != 0 or \ - section['interpretationOfNumberOfPoints'] != 0: - grid_definition_template_40_reduced(section, metadata, cs) - else: - grid_definition_template_40_regular(section, metadata, cs) - - -def grid_definition_template_40_regular(section, metadata, cs): - """ - Translate template representing a regular Gaussian grid. - - """ - scan = scanning_mode(section['scanningMode']) - - # Calculate longitude points. - x_inc = section['iDirectionIncrement'] * _GRID_ACCURACY_IN_DEGREES - x_offset = section['longitudeOfFirstGridPoint'] * _GRID_ACCURACY_IN_DEGREES - x_direction = -1 if scan.i_negative else 1 - Ni = section['Ni'] - x_points = np.arange(Ni, dtype=np.float64) * x_inc * x_direction + x_offset - - # Determine whether the x-points (in degrees) are circular. - circular = _is_circular(x_points, 360.0) - - # Get the latitude points. - # - # Gaussian latitudes are defined by Gauss-Legendre quadrature and the Gauss - # quadrature rule (http://en.wikipedia.org/wiki/Gaussian_quadrature). The - # latitudes of a particular Gaussian grid are uniquely defined by the - # number of latitudes between the equator and the pole, N. The latitudes - # are calculated from the roots of a Legendre series which must be - # calculated numerically. This process involves forming a (possibly large) - # companion matrix, computing its eigenvalues, and usually at least one - # application of Newton's method to achieve best results - # (http://en.wikipedia.org/wiki/Newton%27s_method). The latitudes are given - # by the arcsine of the roots converted to degrees. This computation can be - # time-consuming, especially for large grid sizes. - # - # A direct computation would require: - # 1. Reading the coded key 'N' representing the number of latitudes - # between the equator and pole. - # 2. Computing the set of global Gaussian latitudes associated with the - # value of N. - # 3. Determining the direction of the latitude points from the scanning - # mode. - # 4. Producing a subset of the latitudes based on the given first and - # last latitude points, given by the coded keys La1 and La2. - # - # Given the complexity and potential for poor performance of calculating - # the Gaussian latitudes directly, the GRIB-API computed key - # 'distinctLatitudes' is utilised to obtain the latitude points from the - # GRIB2 message. This computed key provides a rapid calculation of the - # monotonic latitude points that form the Gaussian grid, accounting for - # the coverage of the grid. - y_points = section.get_computed_key('distinctLatitudes') - y_points.sort() - if not scan.j_positive: - y_points = y_points[::-1] - - # Create lat/lon coordinates. - x_coord = DimCoord(x_points, standard_name='longitude', - units='degrees', coord_system=cs, - circular=circular) - y_coord = DimCoord(y_points, standard_name='latitude', - units='degrees', coord_system=cs) - - # Determine the lat/lon dimensions. - y_dim, x_dim = 0, 1 - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the lat/lon coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def grid_definition_template_40_reduced(section, metadata, cs): - """ - Translate template representing a reduced Gaussian grid. - - """ - # Get the latitude and longitude points. - # - # The same comments made in grid_definition_template_40_regular regarding - # computation of Gaussian lattiudes applies here too. Further to this the - # reduced Gaussian grid is not rectangular, the number of points along - # each latitude circle vary with latitude. Whilst it is possible to - # compute the latitudes and longitudes individually for each grid point - # from coded keys, it would be complex and time-consuming compared to - # loading the latitude and longitude arrays directly using the computed - # keys 'latitudes' and 'longitudes'. - x_points = section.get_computed_key('longitudes') - y_points = section.get_computed_key('latitudes') - - # Create lat/lon coordinates. - x_coord = AuxCoord(x_points, standard_name='longitude', - units='degrees', coord_system=cs) - y_coord = AuxCoord(y_points, standard_name='latitude', - units='degrees', coord_system=cs) - - # Add the lat/lon coordinates to the metadata dim coords. - metadata['aux_coords_and_dims'].append((y_coord, 0)) - metadata['aux_coords_and_dims'].append((x_coord, 0)) - - -def grid_definition_template_90(section, metadata): - """ - Translate template representing space view. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - if section['Nr'] == _MDI: - raise TranslationError('Unsupported orthographic grid.') - elif section['Nr'] == 0: - raise TranslationError('Unsupported zero height for space-view.') - if section['orientationOfTheGrid'] != 0: - raise TranslationError('Unsupported space-view orientation.') - - # Determine the coordinate system. - sub_satellite_lat = (section['latitudeOfSubSatellitePoint'] * - _GRID_ACCURACY_IN_DEGREES) - # The subsequent calculations to determine the apparent Earth - # diameters rely on the satellite being over the equator. - if sub_satellite_lat != 0: - raise TranslationError('Unsupported non-zero latitude for ' - 'space-view perspective.') - sub_satellite_lon = (section['longitudeOfSubSatellitePoint'] * - _GRID_ACCURACY_IN_DEGREES) - major, minor, radius = ellipsoid_geometry(section) - geog_cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius) - height_above_centre = geog_cs.semi_major_axis * section['Nr'] / 1e6 - height_above_ellipsoid = height_above_centre - geog_cs.semi_major_axis - cs = icoord_systems.VerticalPerspective(sub_satellite_lat, - sub_satellite_lon, - height_above_ellipsoid, - ellipsoid=geog_cs) - - # Figure out how large the Earth would appear in projection coodinates. - # For both the apparent equatorial and polar diameters this is a - # two-step process: - # 1) Determine the angle subtended by the visible surface. - # 2) Convert that angle into projection coordinates. - # NB. The solutions given below assume the satellite is over the - # equator. - # The apparent equatorial angle uses simple, circular geometry. - # But to derive the apparent polar angle we use the auxiliary circle - # parametric form of the ellipse. In this form, the equation for the - # tangent line is given by: - # x cos(psi) y sin(psi) - # ---------- + ---------- = 1 - # a b - # By considering the cases when x=0 and y=0, the apparent polar - # angle (theta) is given by: - # tan(theta) = b / sin(psi) - # ------------ - # a / cos(psi) - # This can be simplified using: cos(psi) = a / height_above_centre - half_apparent_equatorial_angle = math.asin(geog_cs.semi_major_axis / - height_above_centre) - x_apparent_diameter = (2 * half_apparent_equatorial_angle * - height_above_ellipsoid) - parametric_angle = math.acos(geog_cs.semi_major_axis / height_above_centre) - half_apparent_polar_angle = math.atan(geog_cs.semi_minor_axis / - (height_above_centre * - math.sin(parametric_angle))) - y_apparent_diameter = (2 * half_apparent_polar_angle * - height_above_ellipsoid) - - y_step = y_apparent_diameter / section['dy'] - x_step = x_apparent_diameter / section['dx'] - y_start = y_step * (section['Yo'] - section['Yp'] / 1000) - x_start = x_step * (section['Xo'] - section['Xp'] / 1000) - y_points = y_start + np.arange(section['Ny']) * y_step - x_points = x_start + np.arange(section['Nx']) * x_step - - # This has only been tested with -x/+y scanning, so raise an error - # for other permutations. - scan = scanning_mode(section['scanningMode']) - if scan.i_negative: - x_points = -x_points - else: - raise TranslationError('Unsupported +x scanning') - if not scan.j_positive: - raise TranslationError('Unsupported -y scanning') - - # Create the X and Y coordinates. - y_coord = DimCoord(y_points, 'projection_y_coordinate', units='m', - coord_system=cs) - x_coord = DimCoord(x_points, 'projection_x_coordinate', units='m', - coord_system=cs) - - # Determine the lat/lon dimensions. - y_dim, x_dim = 0, 1 - if scan.j_consecutive: - y_dim, x_dim = 1, 0 - - # Add the X and Y coordinates to the metadata dim coords. - metadata['dim_coords_and_dims'].append((y_coord, y_dim)) - metadata['dim_coords_and_dims'].append((x_coord, x_dim)) - - -def grid_definition_section(section, metadata): - """ - Translate section 3 from the GRIB2 message. - - Update the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 3 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - # Reference GRIB2 Code Table 3.0. - value = section['sourceOfGridDefinition'] - if value != 0: - msg = 'Grid definition section 3 contains unsupported ' \ - 'source of grid definition [{}]'.format(value) - raise TranslationError(msg) - - # Reference GRIB2 Code Table 3.1. - template = section['gridDefinitionTemplateNumber'] - - if template == 0: - # Process regular latitude/longitude grid (regular_ll) - grid_definition_template_0(section, metadata) - elif template == 1: - # Process rotated latitude/longitude grid. - grid_definition_template_1(section, metadata) - elif template == 4: - # Process variable resolution latitude/longitude. - grid_definition_template_4(section, metadata) - elif template == 5: - # Process variable resolution rotated latitude/longitude. - grid_definition_template_5(section, metadata) - elif template == 12: - # Process transverse Mercator. - grid_definition_template_12(section, metadata) - elif template == 20: - # Polar stereographic. - grid_definition_template_20(section, metadata) - elif template == 30: - # Process Lambert conformal: - grid_definition_template_30(section, metadata) - elif template == 40: - grid_definition_template_40(section, metadata) - elif template == 90: - # Process space view. - grid_definition_template_90(section, metadata) - else: - msg = 'Grid definition template [{}] is not supported'.format(template) - raise TranslationError(msg) - - -############################################################################### -# -# Product Definition Section 4 -# -############################################################################### - -def translate_phenomenon(metadata, discipline, parameterCategory, - parameterNumber, probability=None): - """ - Translate GRIB2 phenomenon to CF phenomenon. - - Updates the metadata in-place with the translations. - - Args: - - * metadata: - :class:`collections.OrderedDict` of metadata. - - * discipline: - Message section 0, octet 7. - - * parameterCategory: - Message section 4, octet 10. - - * parameterNumber: - Message section 4, octet 11. - - Kwargs: - - * probability (:class:`Probability`): - If present, the data encodes a forecast probability analysis with the - given properties. - - """ - cf = itranslation.grib2_phenom_to_cf_info(param_discipline=discipline, - param_category=parameterCategory, - param_number=parameterNumber) - if cf is not None: - if probability is None: - metadata['standard_name'] = cf.standard_name - metadata['long_name'] = cf.long_name - metadata['units'] = cf.units - else: - # The basic name+unit info goes into a 'threshold coordinate' which - # encodes probability threshold values. - threshold_coord = DimCoord( - probability.threshold, - standard_name=cf.standard_name, long_name=cf.long_name, - units=cf.units) - metadata['aux_coords_and_dims'].append((threshold_coord, None)) - # The main cube has an adjusted name, and units of '1'. - base_name = cf.standard_name or cf.long_name - long_name = 'probability_of_{}_{}'.format( - base_name, probability.probability_type_name) - metadata['standard_name'] = None - metadata['long_name'] = long_name - metadata['units'] = Unit(1) - - -def time_range_unit(indicatorOfUnitOfTimeRange): - """ - Translate the time range indicator to an equivalent - :class:`cf_units.Unit`. - - Args: - - * indicatorOfUnitOfTimeRange: - Message section 4, octet 18. - - Returns: - :class:`cf_units.Unit`. - - """ - try: - unit = Unit(_TIME_RANGE_UNITS[indicatorOfUnitOfTimeRange]) - except (KeyError, ValueError): - msg = 'Product definition section 4 contains unsupported ' \ - 'time range unit [{}]'.format(indicatorOfUnitOfTimeRange) - raise TranslationError(msg) - return unit - - -def hybrid_factories(section, metadata): - """ - Translate the section 4 optional hybrid vertical coordinates. - - Updates the metadata in-place with the translations. - - Reference GRIB2 Code Table 4.5. - - Relevant notes: - [3] Hybrid pressure level (119) shall be used instead of Hybrid level (105) - - Args: - - * section: - Dictionary of coded key/value pairs from section 4 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - NV = section['NV'] - if NV > 0: - typeOfFirstFixedSurface = section['typeOfFirstFixedSurface'] - if typeOfFirstFixedSurface == _TYPE_OF_FIXED_SURFACE_MISSING: - msg = 'Product definition section 4 contains missing ' \ - 'type of first fixed surface' - raise TranslationError(msg) - - typeOfSecondFixedSurface = section['typeOfSecondFixedSurface'] - if typeOfSecondFixedSurface != _TYPE_OF_FIXED_SURFACE_MISSING: - msg = 'Product definition section 4 contains unsupported type ' \ - 'of second fixed surface [{}]'.format(typeOfSecondFixedSurface) - raise TranslationError(msg) - - if typeOfFirstFixedSurface in [105, 119]: - # Hybrid level (105) and Hybrid pressure level (119). - scaleFactor = section['scaleFactorOfFirstFixedSurface'] - if scaleFactor != 0: - msg = 'Product definition section 4 contains invalid scale ' \ - 'factor of first fixed surface [{}]'.format(scaleFactor) - raise TranslationError(msg) - - # Create the model level number scalar coordinate. - scaledValue = section['scaledValueOfFirstFixedSurface'] - coord = DimCoord(scaledValue, standard_name='model_level_number', - attributes=dict(positive='up')) - metadata['aux_coords_and_dims'].append((coord, None)) - # Create the level pressure scalar coordinate. - pv = section['pv'] - offset = scaledValue - coord = DimCoord(pv[offset], long_name='level_pressure', - units='Pa') - metadata['aux_coords_and_dims'].append((coord, None)) - # Create the sigma scalar coordinate. - offset += NV // 2 - coord = AuxCoord(pv[offset], long_name='sigma') - metadata['aux_coords_and_dims'].append((coord, None)) - # Create the associated factory reference. - args = [{'long_name': 'level_pressure'}, {'long_name': 'sigma'}, - Reference('surface_air_pressure')] - factory = Factory(HybridPressureFactory, args) - metadata['factories'].append(factory) - else: - msg = 'Product definition section 4 contains unsupported ' \ - 'first fixed surface [{}]'.format(typeOfFirstFixedSurface) - raise TranslationError(msg) - - -def vertical_coords(section, metadata): - """ - Translate the vertical coordinates or hybrid vertical coordinates. - - Updates the metadata in-place with the translations. - - Reference GRIB2 Code Table 4.5. - - Args: - - * section: - Dictionary of coded key/value pairs from section 4 of the message. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - if section['NV'] > 0: - # Generate hybrid vertical coordinates. - hybrid_factories(section, metadata) - else: - # Generate vertical coordinate. - typeOfFirstFixedSurface = section['typeOfFirstFixedSurface'] - key = 'scaledValueOfFirstFixedSurface' - scaledValueOfFirstFixedSurface = section[key] - fixed_surface = _FIXED_SURFACE.get(typeOfFirstFixedSurface) - - if fixed_surface is None: - if typeOfFirstFixedSurface != _TYPE_OF_FIXED_SURFACE_MISSING: - if scaledValueOfFirstFixedSurface == _MDI: - if options.warn_on_unsupported: - msg = 'Unable to translate type of first fixed ' \ - 'surface with missing scaled value.' - warnings.warn(msg) - else: - if options.warn_on_unsupported: - msg = 'Unable to translate type of first fixed ' \ - 'surface with scaled value.' - warnings.warn(msg) - else: - key = 'scaleFactorOfFirstFixedSurface' - scaleFactorOfFirstFixedSurface = section[key] - typeOfSecondFixedSurface = section['typeOfSecondFixedSurface'] - - if typeOfSecondFixedSurface != _TYPE_OF_FIXED_SURFACE_MISSING: - if typeOfFirstFixedSurface != typeOfSecondFixedSurface: - msg = 'Product definition section 4 has different ' \ - 'types of first and second fixed surface' - raise TranslationError(msg) - - key = 'scaledValueOfSecondFixedSurface' - scaledValueOfSecondFixedSurface = section[key] - - if scaledValueOfSecondFixedSurface == _MDI: - msg = 'Product definition section 4 has missing ' \ - 'scaled value of second fixed surface' - raise TranslationError(msg) - else: - key = 'scaleFactorOfSecondFixedSurface' - scaleFactorOfSecondFixedSurface = section[key] - first = unscale(scaledValueOfFirstFixedSurface, - scaleFactorOfFirstFixedSurface) - second = unscale(scaledValueOfSecondFixedSurface, - scaleFactorOfSecondFixedSurface) - point = 0.5 * (first + second) - bounds = [first, second] - coord = DimCoord(point, - standard_name=fixed_surface.standard_name, - long_name=fixed_surface.long_name, - units=fixed_surface.units, - bounds=bounds) - # Add the vertical coordinate to metadata aux coords. - metadata['aux_coords_and_dims'].append((coord, None)) - else: - point = unscale(scaledValueOfFirstFixedSurface, - scaleFactorOfFirstFixedSurface) - coord = DimCoord(point, - standard_name=fixed_surface.standard_name, - long_name=fixed_surface.long_name, - units=fixed_surface.units) - # Add the vertical coordinate to metadata aux coords. - metadata['aux_coords_and_dims'].append((coord, None)) - - -def forecast_period_coord(indicatorOfUnitOfTimeRange, forecastTime): - """ - Create the forecast period coordinate. - - Args: - - * indicatorOfUnitOfTimeRange: - Message section 4, octets 18. - - * forecastTime: - Message section 4, octets 19-22. - - Returns: - The scalar forecast period :class:`iris.coords.DimCoord`. - - """ - # Determine the forecast period and associated units. - unit = time_range_unit(indicatorOfUnitOfTimeRange) - point = unit.convert(forecastTime, 'hours') - # Create the forecast period scalar coordinate. - coord = DimCoord(point, standard_name='forecast_period', units='hours') - return coord - - -def statistical_forecast_period_coord(section, frt_coord): - """ - Create a forecast period coordinate for a time-statistic message. - - This applies only with a product definition template 4.8. - - Args: - - * section: - Dictionary of coded key/value pairs from section 4 of the message. - - * frt_coord: - The scalar forecast reference time :class:`iris.coords.DimCoord`. - - Returns: - The scalar forecast period :class:`iris.coords.DimCoord`, containing a - single, bounded point (period value). - - """ - # Get the period end time as a datetime. - end_time = datetime(section['yearOfEndOfOverallTimeInterval'], - section['monthOfEndOfOverallTimeInterval'], - section['dayOfEndOfOverallTimeInterval'], - section['hourOfEndOfOverallTimeInterval'], - section['minuteOfEndOfOverallTimeInterval'], - section['secondOfEndOfOverallTimeInterval']) - - # Get forecast reference time (frt) as a datetime. - frt_point = frt_coord.units.num2date(frt_coord.points[0]) - - # Get the period start time (as a timedelta relative to the frt). - forecast_time = section['forecastTime'] - if options.support_hindcast_values: - # Apply the hindcast fix. - forecast_time = _hindcast_fix(forecast_time) - forecast_units = time_range_unit(section['indicatorOfUnitOfTimeRange']) - forecast_seconds = forecast_units.convert(forecast_time, 'seconds') - start_time_delta = timedelta(seconds=forecast_seconds) - - # Get the period end time (as a timedelta relative to the frt). - end_time_delta = end_time - frt_point - - # Get the middle of the period (as a timedelta relative to the frt). - # Note: timedelta division in 2.7 is odd. Even though we request integer - # division, it's to the nearest _micro_second. - mid_time_delta = (start_time_delta + end_time_delta) // 2 - - # Create and return the forecast period coordinate. - def timedelta_hours(timedelta): - return timedelta.total_seconds() / 3600.0 - - mid_point_hours = timedelta_hours(mid_time_delta) - bounds_hours = [timedelta_hours(start_time_delta), - timedelta_hours(end_time_delta)] - fp_coord = DimCoord(mid_point_hours, bounds=bounds_hours, - standard_name='forecast_period', units='hours') - return fp_coord - - -def other_time_coord(rt_coord, fp_coord): - """ - Return the counterpart to the given scalar 'time' or - 'forecast_reference_time' coordinate, by combining it with the - given forecast_period coordinate. - - Bounds are not supported. - - Args: - - * rt_coord: - The scalar "reference time" :class:`iris.coords.DimCoord`, - as defined by section 1. This must be either a 'time' or - 'forecast_reference_time' coordinate. - - * fp_coord: - The scalar 'forecast_period' :class:`iris.coords.DimCoord`. - - Returns: - The scalar :class:`iris.coords.DimCoord` for either 'time' or - 'forecast_reference_time'. - - """ - if not rt_coord.units.is_time_reference(): - fmt = 'Invalid unit for reference time coord: {}' - raise ValueError(fmt.format(rt_coord.units)) - if not fp_coord.units.is_time(): - fmt = 'Invalid unit for forecast_period coord: {}' - raise ValueError(fmt.format(fp_coord.units)) - if rt_coord.has_bounds() or fp_coord.has_bounds(): - raise ValueError('Coordinate bounds are not supported') - if rt_coord.shape != (1,) or fp_coord.shape != (1,): - raise ValueError('Vector coordinates are not supported') - - if rt_coord.standard_name == 'time': - rt_base_unit = str(rt_coord.units).split(' since ')[0] - fp = fp_coord.units.convert(fp_coord.points[0], rt_base_unit) - frt = rt_coord.points[0] - fp - return DimCoord(frt, 'forecast_reference_time', units=rt_coord.units) - elif rt_coord.standard_name == 'forecast_reference_time': - return validity_time_coord(rt_coord, fp_coord) - else: - fmt = 'Unexpected reference time coordinate: {}' - raise ValueError(fmt.format(rt_coord.name())) - - -def validity_time_coord(frt_coord, fp_coord): - """ - Create the validity or phenomenon time coordinate. - - Args: - - * frt_coord: - The scalar forecast reference time :class:`iris.coords.DimCoord`. - - * fp_coord: - The scalar forecast period :class:`iris.coords.DimCoord`. - - Returns: - The scalar time :class:`iris.coords.DimCoord`. - It has bounds if the period coord has them, otherwise not. - - """ - if frt_coord.shape != (1,): - msg = 'Expected scalar forecast reference time coordinate when ' \ - 'calculating validity time, got shape {!r}'.format(frt_coord.shape) - raise ValueError(msg) - - if fp_coord.shape != (1,): - msg = 'Expected scalar forecast period coordinate when ' \ - 'calculating validity time, got shape {!r}'.format(fp_coord.shape) - raise ValueError(msg) - - def coord_timedelta(coord, value): - # Helper to convert a time coordinate value into a timedelta. - seconds = coord.units.convert(value, 'seconds') - return timedelta(seconds=seconds) - - # Calculate validity (phenomenon) time in forecast-reference-time units. - frt_point = frt_coord.units.num2date(frt_coord.points[0]) - point_delta = coord_timedelta(fp_coord, fp_coord.points[0]) - point = frt_coord.units.date2num(frt_point + point_delta) - - # Calculate bounds (if any) in the same way. - if fp_coord.bounds is None: - bounds = None - else: - bounds_deltas = [coord_timedelta(fp_coord, bound_point) - for bound_point in fp_coord.bounds[0]] - bounds = [frt_coord.units.date2num(frt_point + delta) - for delta in bounds_deltas] - - # Create the time scalar coordinate. - coord = DimCoord(point, bounds=bounds, - standard_name='time', units=frt_coord.units) - return coord - - -def time_coords(section, metadata, rt_coord): - if 'forecastTime' in section.keys(): - forecast_time = section['forecastTime'] - # The gribapi encodes the forecast time as 'startStep' for pdt 4.4x; - # product_definition_template_40 makes use of this function. The - # following will be removed once the suspected bug is fixed. - elif 'startStep' in section.keys(): - forecast_time = section['startStep'] - - # Calculate the forecast period coordinate. - fp_coord = forecast_period_coord(section['indicatorOfUnitOfTimeRange'], - forecast_time) - # Add the forecast period coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((fp_coord, None)) - # Calculate the "other" time coordinate - i.e. whichever of 'time' - # or 'forecast_reference_time' we don't already have. - other_coord = other_time_coord(rt_coord, fp_coord) - # Add the time coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((other_coord, None)) - # Add the reference time coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((rt_coord, None)) - - -def generating_process(section, include_forecast_process=True): - if options.warn_on_unsupported: - # Reference Code Table 4.3. - warnings.warn('Unable to translate type of generating process.') - warnings.warn('Unable to translate background generating ' - 'process identifier.') - if include_forecast_process: - warnings.warn('Unable to translate forecast generating ' - 'process identifier.') - - -def data_cutoff(hoursAfterDataCutoff, minutesAfterDataCutoff): - """ - Handle the after reference time data cutoff. - - Args: - - * hoursAfterDataCutoff: - Message section 4, octets 15-16. - - * minutesAfterDataCutoff: - Message section 4, octet 17. - - """ - if (hoursAfterDataCutoff != _MDI or - minutesAfterDataCutoff != _MDI): - if options.warn_on_unsupported: - warnings.warn('Unable to translate "hours and/or minutes ' - 'after data cutoff".') - - -def statistical_method_name(section): - # Decode the type of statistic as a cell_method 'method' string. - # Templates 8, 9, 10, 11 and 15 all use this type code, which is defined - # in table 4.10. - # However, the actual keyname is different for template 15. - section_number = section['productDefinitionTemplateNumber'] - if section_number in (8, 9, 10, 11): - stat_keyname = 'typeOfStatisticalProcessing' - elif section_number == 15: - stat_keyname = 'statisticalProcess' - else: - # This should *never* happen, as only called by pdt 8 and 15. - msg = ("Internal error: can't get statistical method for unsupported " - "pdt : 4.{:d}.") - raise ValueError(msg.format(section_number)) - statistic_code = section[stat_keyname] - statistic_name = _STATISTIC_TYPE_NAMES.get(statistic_code) - if statistic_name is None: - msg = ('Product definition section 4 contains an unsupported ' - 'statistical process type [{}] ') - raise TranslationError(msg.format(statistic_code)) - return statistic_name - - -def statistical_cell_method(section): - """ - Create a cell method representing a time statistic. - - This applies only with a product definition template 4.8. - - Args: - - * section: - Dictionary of coded key/value pairs from section 4 of the message. - - Returns: - A cell method over 'time'. - - """ - # Handle the number of time ranges -- we currently only support one. - n_time_ranges = section['numberOfTimeRange'] - if n_time_ranges != 1: - if n_time_ranges == 0: - msg = ('Product definition section 4 specifies aggregation over ' - '"0 time ranges".') - raise TranslationError(msg) - else: - msg = ('Product definition section 4 specifies aggregation over ' - 'multiple time ranges [{}], which is not yet ' - 'supported.'.format(n_time_ranges)) - raise TranslationError(msg) - - # Decode the type of statistic (aggregation method). - statistic_name = statistical_method_name(section) - - # Decode the type of time increment. - increment_typecode = section['typeOfTimeIncrement'] - if increment_typecode not in (2, 255): - # NOTE: All our current test data seems to contain the value 2, which - # is all we currently support. - # The exact interpretation of this is still unclear so we also accept - # a missing value. - msg = ('grib statistic time-increment type [{}] ' - 'is not supported.'.format(increment_typecode)) - raise TranslationError(msg) - - interval_number = section['timeIncrement'] - if interval_number == 0: - intervals_string = None - else: - units_string = _TIME_RANGE_UNITS[ - section['indicatorOfUnitForTimeIncrement']] - intervals_string = '{} {}'.format(interval_number, units_string) - - # Create a cell method to represent the time aggregation. - cell_method = CellMethod(method=statistic_name, - coords='time', - intervals=intervals_string) - return cell_method - - -def ensemble_identifier(section): - if options.warn_on_unsupported: - # Reference Code Table 4.6. - warnings.warn('Unable to translate type of ensemble forecast.') - warnings.warn('Unable to translate number of forecasts in ensemble.') - - # Create the realization coordinates. - realization = DimCoord(section['perturbationNumber'], - standard_name='realization', - units='no_unit') - return realization - - -def product_definition_template_0(section, metadata, rt_coord): - """ - Translate template representing an analysis or forecast at a horizontal - level or in a horizontal layer at a point in time. - - 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. - - * rt_coord: - The scalar "reference time" :class:`iris.coords.DimCoord`. - This will be either 'time' or 'forecast_reference_time'. - - """ - # Handle generating process details. - generating_process(section) - - # Handle the data cutoff. - data_cutoff(section['hoursAfterDataCutoff'], - section['minutesAfterDataCutoff']) - - time_coords(section, metadata, rt_coord) - - # Check for vertical coordinates. - vertical_coords(section, metadata) - - -def product_definition_template_1(section, metadata, frt_coord): - """ - Translate template representing individual ensemble forecast, control - and perturbed, at a horizontal level or in a horizontal layer at a - point in time. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 4 of the message. - - * metadata: - :class:`collectins.OrderedDict` of metadata. - - * frt_coord: - The scalar forecast reference time :class:`iris.coords.DimCoord`. - - """ - # Perform identical message processing. - product_definition_template_0(section, metadata, frt_coord) - - realization = ensemble_identifier(section) - - # Add the realization coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((realization, None)) - - -def product_definition_template_8(section, metadata, frt_coord): - """ - Translate template representing average, accumulation and/or extreme values - or other statistically processed values 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`. - - """ - # Handle generating process details. - generating_process(section) - - # Handle the data cutoff. - data_cutoff(section['hoursAfterDataCutoff'], - section['minutesAfterDataCutoff']) - - # Create a cell method to represent the time statistic. - time_statistic_cell_method = statistical_cell_method(section) - # 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, - # 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) - metadata['aux_coords_and_dims'].append((fp_coord, None)) - - # Calculate a bounded validity time coord matching the forecast period. - t_coord = validity_time_coord(frt_coord, fp_coord) - # Add the time coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((t_coord, None)) - - # Check for vertical coordinates. - vertical_coords(section, metadata) - - -def product_definition_template_9(section, metadata, frt_coord): - """ - Translate template representing probability 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`. - - """ - # Start by calling PDT8 as all elements of that are common to this. - product_definition_template_8(section, metadata, frt_coord) - - # Remove the cell_method encoding the underlying statistic, as CF does not - # currently support this type of representation. - cell_method, = metadata['cell_methods'] - metadata['cell_methods'] = [] - # NOTE: we currently don't record the nature of the underlying statistic, - # as we don't have an agreed way of representing that in CF. - - # Return a probability object to control the production of a probability - # result. This is done once the underlying phenomenon type is determined, - # in 'translate_phenomenon'. - probability_typecode = section['probabilityType'] - if probability_typecode == 1: - # Type is "above upper level". - threshold_value = section['scaledValueOfUpperLimit'] - if threshold_value == _MDI: - msg = 'Product definition section 4 has missing ' \ - 'scaled value of upper limit' - raise TranslationError(msg) - threshold_scaling = section['scaleFactorOfUpperLimit'] - if threshold_scaling == _MDI: - msg = 'Product definition section 4 has missing ' \ - 'scale factor of upper limit' - raise TranslationError(msg) - # Encode threshold information. - threshold = unscale(threshold_value, threshold_scaling) - probability_type = Probability('above_threshold', threshold) - # Note that GRIB provides separate "above lower threshold" and "above - # upper threshold" probability types. This naming style doesn't - # recognise that distinction. For now, assume this is not important. - else: - msg = ('Product definition section 4 contains an unsupported ' - 'probability type [{}]'.format(probability_typecode)) - raise TranslationError(msg) - - 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 - or perturbed; average, accumulation and/or extreme values - or other statistically processed values 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) - - realization = ensemble_identifier(section) - - # Add the realization coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((realization, None)) - - -def product_definition_template_15(section, metadata, frt_coord): - """ - Translate template representing : "average, accumulation, extreme values, - or other statistically processed values over a spatial area at a - horizontal level or in a horizontal layer at a point in time". - - 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`. - - """ - # Check unique keys for this template. - spatial_processing_code = section['spatialProcessing'] - - if spatial_processing_code != 0: - # For now, we only support the simplest case, representing a statistic - # over the whole notional area of a cell. - msg = ('Product definition section 4 contains an unsupported ' - 'spatial processing type [{}]'.format(spatial_processing_code)) - raise TranslationError(msg) - - # NOTE: PDT 4.15 alse defines a 'numberOfPointsUsed' key, but we think this - # is irrelevant to the currently supported spatial-processing types. - - # Process parts in common with pdt 4.0. - product_definition_template_0(section, metadata, frt_coord) - - # Decode the statistic method name. - cell_method_name = statistical_method_name(section) - - # Record an 'area' cell-method using this statistic. - metadata['cell_methods'] = [CellMethod(coords=('area',), - method=cell_method_name)] - - -def satellite_common(section, metadata): - # Number of contributing spectral bands. - NB = section['NB'] - - if NB > 0: - # Create the satellite series coordinate. - satelliteSeries = section['satelliteSeries'] - coord = AuxCoord(satelliteSeries, long_name='satellite_series') - # Add the satellite series coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((coord, None)) - - # Create the satellite number coordinate. - satelliteNumber = section['satelliteNumber'] - coord = AuxCoord(satelliteNumber, long_name='satellite_number') - # Add the satellite number coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((coord, None)) - - # Create the satellite instrument type coordinate. - instrumentType = section['instrumentType'] - coord = AuxCoord(instrumentType, long_name='instrument_type') - # Add the instrument type coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((coord, None)) - - # Create the central wave number coordinate. - scaleFactor = section['scaleFactorOfCentralWaveNumber'] - scaledValue = section['scaledValueOfCentralWaveNumber'] - wave_number = unscale(scaledValue, scaleFactor) - standard_name = 'sensor_band_central_radiation_wavenumber' - coord = AuxCoord(wave_number, - standard_name=standard_name, - units=Unit('m-1')) - # Add the central wave number coordinate to the metadata aux coords. - metadata['aux_coords_and_dims'].append((coord, None)) - - -def product_definition_template_31(section, metadata, rt_coord): - """ - Translate template representing a satellite product. - - 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. - - * rt_coord: - The scalar observation time :class:`iris.coords.DimCoord'. - - """ - generating_process(section, include_forecast_process=False) - - satellite_common(section, metadata) - - # Add the observation time coordinate. - metadata['aux_coords_and_dims'].append((rt_coord, None)) - - -def product_definition_template_32(section, metadata, rt_coord): - """ - Translate template representing an analysis or forecast at a horizontal - level or in a horizontal layer at a point in time for simulated (synthetic) - satellite data. - - 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. - - * rt_coord: - The scalar observation time :class:`iris.coords.DimCoord'. - - """ - generating_process(section, include_forecast_process=False) - - # Handle the data cutoff. - data_cutoff(section['hoursAfterDataCutoff'], - section['minutesAfterDataCutoff']) - - time_coords(section, metadata, rt_coord) - - satellite_common(section, metadata) - - -def product_definition_template_40(section, metadata, frt_coord): - """ - Translate template representing an analysis or forecast at a horizontal - level or in a horizontal layer at a point in time for atmospheric chemical - constituents. - - Updates the metadata in-place with the translations. - - Args: - - * section: - Dictionary of coded key/value pairs from section 4 of the message. - - * metadata: - :class:`collectins.OrderedDict` of metadata. - - * frt_coord: - The scalar forecast reference time :class:`iris.coords.DimCoord`. - - """ - # 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. - metadata['attributes']['WMO_constituent_type'] = constituent_type - - -def product_definition_section(section, metadata, discipline, tablesVersion, - rt_coord): - """ - Translate section 4 from the GRIB2 message. - - 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. - - * discipline: - Message section 0, octet 7. - - * tablesVersion: - Message section 1, octet 10. - - * rt_coord: - The scalar reference time :class:`iris.coords.DimCoord`. - - """ - # Reference GRIB2 Code Table 4.0. - template = section['productDefinitionTemplateNumber'] - - probability = None - if template == 0: - # Process analysis or forecast at a horizontal level or - # in a horizontal layer at a point in time. - product_definition_template_0(section, metadata, rt_coord) - elif template == 1: - # Process individual ensemble forecast, control and perturbed, at - # a horizontal level or in a horizontal layer at a point in time. - product_definition_template_1(section, metadata, rt_coord) - elif template == 8: - # Process statistically processed values at a horizontal level or in a - # horizontal layer in a continuous or non-continuous time interval. - product_definition_template_8(section, metadata, rt_coord) - 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 == 15: - product_definition_template_15(section, metadata, rt_coord) - elif template == 31: - # Process satellite product. - product_definition_template_31(section, metadata, rt_coord) - elif template == 32: - product_definition_template_32(section, metadata, rt_coord) - elif template == 40: - product_definition_template_40(section, metadata, rt_coord) - else: - msg = 'Product definition template [{}] is not ' \ - 'supported'.format(template) - raise TranslationError(msg) - - # Translate GRIB2 phenomenon to CF phenomenon. - if tablesVersion != _CODE_TABLES_MISSING: - translate_phenomenon(metadata, discipline, - section['parameterCategory'], - section['parameterNumber'], - probability=probability) - - -############################################################################### -# -# Data Representation Section 5 -# -############################################################################### - -def data_representation_section(section): - """ - Translate section 5 from the GRIB2 message. - Grid point template decoding is fully provided by the ECMWF GRIB API, - all grid point and spectral templates are supported, the data payload - is returned from the GRIB API already unpacked. - - """ - # Reference GRIB2 Code Table 5.0. - template = section['dataRepresentationTemplateNumber'] - - # Supported templates for both grid point and spectral data: - grid_point_templates = (0, 1, 2, 3, 4, 40, 41, 61) - spectral_templates = (50, 51) - supported_templates = grid_point_templates + spectral_templates - - if template not in supported_templates: - msg = 'Data Representation Section Template [{}] is not ' \ - 'supported'.format(template) - raise TranslationError(msg) - - -############################################################################### -# -# Bitmap Section 6 -# -############################################################################### - -def bitmap_section(section): - """ - Translate section 6 from the GRIB2 message. - - The bitmap can take the following values: - - * 0: Bitmap applies to the data and is specified in this section - of this message. - * 1-253: Bitmap applies to the data, is specified by originating - centre and is not specified in section 6 of this message. - * 254: Bitmap applies to the data, is specified in an earlier - section 6 of this message and is not specified in this - section 6 of this message. - * 255: Bitmap does not apply to the data. - - Only values 0 and 255 are supported. - - """ - # Reference GRIB2 Code Table 6.0. - bitMapIndicator = section['bitMapIndicator'] - - if bitMapIndicator not in [_BITMAP_CODE_NONE, _BITMAP_CODE_PRESENT]: - msg = 'Bitmap Section 6 contains unsupported ' \ - 'bitmap indicator [{}]'.format(bitMapIndicator) - raise TranslationError(msg) - - -############################################################################### - -def grib2_convert(field, metadata): - """ - Translate the GRIB2 message into the appropriate cube metadata. - - Updates the metadata in-place with the translations. - - Args: - - * field: - GRIB2 message to be translated. - - * metadata: - :class:`collections.OrderedDict` of metadata. - - """ - # Section 1 - Identification Section. - centre = _CENTRES.get(field.sections[1]['centre']) - if centre is not None: - metadata['attributes']['centre'] = centre - rt_coord = reference_time_coord(field.sections[1]) - - # Section 3 - Grid Definition Section (Grid Definition Template) - grid_definition_section(field.sections[3], metadata) - - # Section 4 - Product Definition Section (Product Definition Template) - product_definition_section(field.sections[4], metadata, - field.sections[0]['discipline'], - field.sections[1]['tablesVersion'], - rt_coord) - - # Section 5 - Data Representation Section (Data Representation Template) - data_representation_section(field.sections[5]) - - # Section 6 - Bitmap Section. - bitmap_section(field.sections[6]) - - -############################################################################### - -def convert(field): - """ - Translate the GRIB message into the appropriate cube metadata. - - Args: - - * field: - GRIB message to be translated. - - Returns: - A :class:`iris.fileformats.rules.ConversionMetadata` object. - - """ - if hasattr(field, 'sections'): - editionNumber = field.sections[0]['editionNumber'] - - 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) - - result = ConversionMetadata._make(metadata.values()) - else: - editionNumber = field.edition - - 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 deleted file mode 100644 index 8182246305..0000000000 --- a/lib/iris/fileformats/grib/_save_rules.py +++ /dev/null @@ -1,1248 +0,0 @@ -# (C) British Crown Copyright 2010 - 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 . -""" -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`. - -""" - -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 -from iris.coord_systems import (GeogCS, RotatedGeogCS, TransverseMercator, - LambertConformal) -import iris.exceptions -from iris.fileformats.grib import grib_phenom_translation as gptx -from iris.fileformats.grib._load_convert import (_STATISTIC_TYPE_NAMES, - _TIME_RANGE_UNITS) -from iris.util import is_regular, regular_step - - -# Invert code tables from :mod:`iris.fileformats.grib._load_convert`. -_STATISTIC_TYPE_NAMES = {val: key for key, val in - _STATISTIC_TYPE_NAMES.items()} -_TIME_RANGE_UNITS = {val: key for key, val in _TIME_RANGE_UNITS.items()} - - -def fixup_float32_as_int32(value): - """ - Workaround for use when the ECMWF GRIB API treats an IEEE 32-bit - floating-point value as a signed, 4-byte integer. - - Returns the integer value which will result in the on-disk - representation corresponding to the IEEE 32-bit floating-point - value. - - """ - value_as_float32 = np.array(value, dtype='f4') - value_as_uint32 = value_as_float32.view(dtype='u4') - if value_as_uint32 >= 0x80000000: - # Convert from two's-complement to sign-and-magnitude. - # NB. Because of the silly representation of negative - # integers in GRIB2, there is no value we can pass to - # grib_set that will result in the bit pattern 0x80000000. - # But since that bit pattern corresponds to a floating - # point value of negative-zero, we can safely treat it as - # positive-zero instead. - value_as_grib_int = 0x80000000 - int(value_as_uint32) - else: - value_as_grib_int = int(value_as_uint32) - return value_as_grib_int - - -def fixup_int32_as_uint32(value): - """ - Workaround for use when the ECMWF GRIB API treats a signed, 4-byte - integer value as an unsigned, 4-byte integer. - - Returns the unsigned integer value which will result in the on-disk - representation corresponding to the signed, 4-byte integer value. - - """ - value = int(value) - if -0x7fffffff <= value <= 0x7fffffff: - if value < 0: - # Convert from two's-complement to sign-and-magnitude. - value = 0x80000000 - value - else: - msg = '{} out of range -2147483647 to 2147483647.'.format(value) - raise ValueError(msg) - return value - - -def ensure_set_int32_value(grib, key, value): - """ - Ensure the workaround function :func:`fixup_int32_as_uint32` is applied as - necessary to problem keys. - - """ - try: - gribapi.grib_set(grib, key, value) - except gribapi.GribInternalError: - value = fixup_int32_as_uint32(value) - gribapi.grib_set(grib, key, value) - - -############################################################################### -# -# Constants -# -############################################################################### - -# Reference Flag Table 3.3 -_RESOLUTION_AND_COMPONENTS_GRID_WINDS_BIT = 3 # NB "bit5", from MSB=1. - -# Reference Regulation 92.1.6 -_DEFAULT_DEGREES_UNITS = 1.0e-6 - - -############################################################################### -# -# 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) - - # Code table 1.4 - # analysis, forecast, processed satellite, processed radar, - if cube.coords('realization'): - # assume realization will always have 1 and only 1 point - # as cubes saving to GRIB2 a 2D horizontal slices - if cube.coord('realization').points[0] != 0: - gribapi.grib_set_long(grib, "typeOfProcessedData", 4) - else: - gribapi.grib_set_long(grib, "typeOfProcessedData", 3) - else: - 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 - - # Initially set shape_of_earth keys to missing (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) - - if isinstance(cs, GeogCS): - ellipsoid = cs - else: - ellipsoid = cs.ellipsoid - if ellipsoid is None: - msg = "Could not determine shape of the earth from coord system "\ - "of horizontal grid." - raise iris.exceptions.TranslationError(msg) - - # Spherical earth. - 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) - # Oblate spheroid earth. - 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, x_str, y_str): - gribapi.grib_set_long(grib, x_str, x_coord.shape[0]) - gribapi.grib_set_long(grib, y_str, 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) - gribapi.grib_set(grib, "DxInDegrees", float(abs(x_step))) - gribapi.grib_set(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 horizontal_grid_common(cube, grib, xy=False): - nx_str, ny_str = ("Nx", "Ny") if xy else ("Ni", "Nj") - # Grib encoding of the sequences of X and Y points. - 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, nx_str, ny_str) - scanning_mode_flags(x_coord, y_coord, grib) - - -def latlon_points_regular(cube, grib): - y_coord = cube.coord(dimensions=[0]) - x_coord = cube.coord(dimensions=[1]) - latlon_first_last(x_coord, y_coord, grib) - dx_dy(x_coord, y_coord, grib) - - -def latlon_points_irregular(cube, grib): - y_coord = cube.coord(dimensions=[0]) - x_coord = cube.coord(dimensions=[1]) - - # Distinguish between true-north and grid-oriented vectors. - is_grid_wind = cube.name() in ('x_wind', 'y_wind', 'grid_eastward_wind', - 'grid_northward_wind') - # Encode in bit "5" of 'resolutionAndComponentFlags' (other bits unused). - component_flags = 0 - if is_grid_wind: - component_flags |= 2 ** _RESOLUTION_AND_COMPONENTS_GRID_WINDS_BIT - gribapi.grib_set(grib, 'resolutionAndComponentFlags', component_flags) - - # Record the X and Y coordinate values. - # NOTE: there is currently a bug in the gribapi which means that the size - # of the longitudes array does not equal 'Nj', as it should. - # See : https://software.ecmwf.int/issues/browse/SUP-1096 - # So, this only works at present if the x and y dimensions are **equal**. - lon_values = x_coord.points / _DEFAULT_DEGREES_UNITS - lat_values = y_coord.points / _DEFAULT_DEGREES_UNITS - gribapi.grib_set_array(grib, 'longitudes', - np.array(np.round(lon_values), dtype=np.int64)) - gribapi.grib_set_array(grib, 'latitudes', - np.array(np.round(lat_values), dtype=np.int64)) - - -def rotated_pole(cube, grib): - # Grib encoding of a rotated pole coordinate system. - cs = cube.coord(dimensions=[0]).coord_system - - if cs.north_pole_grid_longitude != 0.0: - raise iris.exceptions.TranslationError( - 'Grib save does not yet support Rotated-pole coordinates with ' - 'a rotated prime meridian.') -# 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 = cs.grid_north_pole_latitude / _DEFAULT_DEGREES_UNITS - longitude = (((cs.grid_north_pole_longitude + 180) % 360) / - _DEFAULT_DEGREES_UNITS) - gribapi.grib_set(grib, "latitudeOfSouthernPole", - int(round(latitude))) - gribapi.grib_set(grib, "longitudeOfSouthernPole", int(round(longitude))) - gribapi.grib_set(grib, "angleOfRotation", 0) - - -def points_in_unit(coord, unit): - points = coord.units.convert(coord.points, unit) - points = np.around(points).astype(int) - return points - - -def step(points, atol): - diffs = points[1:] - points[:-1] - mean_diff = np.mean(diffs).astype(points.dtype) - if not np.allclose(diffs, mean_diff, atol=atol): - raise ValueError() - return int(mean_diff) - - -def grid_definition_template_0(cube, grib): - """ - Set keys within the provided grib message based on - Grid Definition Template 3.0. - - Template 3.0 is used to represent "latitude/longitude (or equidistant - cylindrical, or Plate Carree)". - The coordinates are regularly spaced, true latitudes and longitudes. - - """ - # Constant resolution, aka 'regular' true lat-lon grid. - gribapi.grib_set_long(grib, "gridDefinitionTemplateNumber", 0) - horizontal_grid_common(cube, grib) - latlon_points_regular(cube, grib) - - -def grid_definition_template_1(cube, grib): - """ - Set keys within the provided grib message based on - Grid Definition Template 3.1. - - Template 3.1 is used to represent "rotated latitude/longitude (or - equidistant cylindrical, or Plate Carree)". - The coordinates are regularly spaced, rotated latitudes and longitudes. - - """ - # Constant resolution, aka 'regular' rotated lat-lon grid. - gribapi.grib_set_long(grib, "gridDefinitionTemplateNumber", 1) - - # Record details of the rotated coordinate system. - rotated_pole(cube, grib) - - # Encode the lat/lon points. - horizontal_grid_common(cube, grib) - latlon_points_regular(cube, grib) - - -def grid_definition_template_5(cube, grib): - """ - Set keys within the provided grib message based on - Grid Definition Template 3.5. - - Template 3.5 is used to represent "variable resolution rotated - latitude/longitude". - The coordinates are irregularly spaced, rotated latitudes and longitudes. - - """ - # NOTE: we must set Ni=Nj=1 before establishing the template. - # Without this, setting "gridDefinitionTemplateNumber" = 5 causes an - # immediate error. - # See: https://software.ecmwf.int/issues/browse/SUP-1095 - # This is acceptable, as the subsequent call to 'horizontal_grid_common' - # will set these to the correct horizontal dimensions - # (by calling 'grid_dims'). - gribapi.grib_set(grib, "Ni", 1) - gribapi.grib_set(grib, "Nj", 1) - gribapi.grib_set(grib, "gridDefinitionTemplateNumber", 5) - - # Record details of the rotated coordinate system. - rotated_pole(cube, grib) - # Encode the lat/lon points. - horizontal_grid_common(cube, grib) - latlon_points_irregular(cube, grib) - - -def grid_definition_template_12(cube, grib): - """ - Set keys within the provided grib message based on - Grid Definition Template 3.12. - - Template 3.12 is used to represent a Transverse Mercator grid. - - """ - gribapi.grib_set(grib, "gridDefinitionTemplateNumber", 12) - - # Retrieve some information from the cube. - y_coord = cube.coord(dimensions=[0]) - x_coord = cube.coord(dimensions=[1]) - cs = y_coord.coord_system - - # Normalise the coordinate values to centimetres - the resolution - # used in the GRIB message. - y_cm = points_in_unit(y_coord, 'cm') - x_cm = points_in_unit(x_coord, 'cm') - - # Set some keys specific to GDT12. - # Encode the horizontal points. - - # NB. Since we're already in centimetres, our tolerance for - # discrepancy in the differences is 1. - try: - x_step = step(x_cm, atol=1) - y_step = step(y_cm, atol=1) - except ValueError: - msg = ('Irregular coordinates not supported for transverse ' - 'Mercator.') - raise iris.exceptions.TranslationError(msg) - gribapi.grib_set(grib, 'Di', abs(x_step)) - gribapi.grib_set(grib, 'Dj', abs(y_step)) - horizontal_grid_common(cube, grib) - - # GRIBAPI expects unsigned ints in X1, X2, Y1, Y2 but it should accept - # signed ints, so work around this. - # See https://software.ecmwf.int/issues/browse/SUP-1101 - ensure_set_int32_value(grib, 'Y1', int(y_cm[0])) - ensure_set_int32_value(grib, 'Y2', int(y_cm[-1])) - ensure_set_int32_value(grib, 'X1', int(x_cm[0])) - ensure_set_int32_value(grib, 'X2', int(x_cm[-1])) - - # Lat and lon of reference point are measured in millionths of a degree. - gribapi.grib_set(grib, "latitudeOfReferencePoint", - cs.latitude_of_projection_origin / _DEFAULT_DEGREES_UNITS) - gribapi.grib_set(grib, "longitudeOfReferencePoint", - cs.longitude_of_central_meridian / _DEFAULT_DEGREES_UNITS) - - # Convert a value in metres into the closest integer number of - # centimetres. - def m_to_cm(value): - return int(round(value * 100)) - - # False easting and false northing are measured in units of (10^-2)m. - gribapi.grib_set(grib, 'XR', m_to_cm(cs.false_easting)) - gribapi.grib_set(grib, 'YR', m_to_cm(cs.false_northing)) - - # GRIBAPI expects a signed int for scaleFactorAtReferencePoint - # but it should accept a float, so work around this. - # See https://software.ecmwf.int/issues/browse/SUP-1100 - value = cs.scale_factor_at_central_meridian - key_type = gribapi.grib_get_native_type(grib, - "scaleFactorAtReferencePoint") - if key_type is not float: - value = fixup_float32_as_int32(value) - gribapi.grib_set(grib, "scaleFactorAtReferencePoint", value) - - -def grid_definition_template_30(cube, grib): - """ - Set keys within the provided grib message based on - Grid Definition Template 3.30. - - Template 3.30 is used to represent a Lambert Conformal grid. - - """ - - gribapi.grib_set(grib, "gridDefinitionTemplateNumber", 30) - - # Retrieve some information from the cube. - y_coord = cube.coord(dimensions=[0]) - x_coord = cube.coord(dimensions=[1]) - cs = y_coord.coord_system - - # Normalise the coordinate values to millimetres - the resolution - # used in the GRIB message. - y_mm = points_in_unit(y_coord, 'mm') - x_mm = points_in_unit(x_coord, 'mm') - - # Encode the horizontal points. - - # NB. Since we're already in millimetres, our tolerance for - # discrepancy in the differences is 1. - try: - x_step = step(x_mm, atol=1) - y_step = step(y_mm, atol=1) - except ValueError: - msg = ('Irregular coordinates not supported for Lambert ' - 'Conformal.') - raise iris.exceptions.TranslationError(msg) - gribapi.grib_set(grib, 'Dx', abs(x_step)) - gribapi.grib_set(grib, 'Dy', abs(y_step)) - - horizontal_grid_common(cube, grib, xy=True) - - # Transform first point into geographic CS - geog = cs.ellipsoid if cs.ellipsoid is not None else GeogCS(1) - first_x, first_y = geog.as_cartopy_crs().transform_point( - x_coord.points[0], - y_coord.points[0], - cs.as_cartopy_crs()) - first_x = first_x % 360 - central_lon = cs.central_lon % 360 - - gribapi.grib_set(grib, "latitudeOfFirstGridPoint", - int(np.round(first_y * 1e6))) - gribapi.grib_set(grib, "longitudeOfFirstGridPoint", - int(np.round(first_x * 1e6))) - gribapi.grib_set(grib, "LaD", cs.central_lat * 1e6) - gribapi.grib_set(grib, "LoV", central_lon * 1e6) - latin1, latin2 = cs.secant_latitudes - gribapi.grib_set(grib, "Latin1", latin1 * 1e6) - gribapi.grib_set(grib, "Latin2", latin2 * 1e6) - gribapi.grib_set(grib, 'resolutionAndComponentFlags', - 0x1 << _RESOLUTION_AND_COMPONENTS_GRID_WINDS_BIT) - - # Which pole are the parallels closest to? That is the direction - # that the cone converges. - poliest_sec = latin1 if abs(latin1) > abs(latin2) else latin2 - centre_flag = 0x0 if poliest_sec > 0 else 0x1 - gribapi.grib_set(grib, 'projectionCentreFlag', centre_flag) - gribapi.grib_set(grib, "latitudeOfSouthernPole", 0) - gribapi.grib_set(grib, "longitudeOfSouthernPole", 0) - - -def grid_definition_section(cube, grib): - """ - Set keys within the grid definition section of the provided grib message, - based on the properties of the cube. - - """ - x_coord = cube.coord(dimensions=[1]) - y_coord = cube.coord(dimensions=[0]) - cs = x_coord.coord_system # N.B. already checked same cs for x and y. - regular_x_and_y = is_regular(x_coord) and is_regular(y_coord) - - if isinstance(cs, GeogCS): - if not regular_x_and_y: - raise iris.exceptions.TranslationError( - 'Saving an irregular latlon grid to GRIB (PDT3.4) is not ' - 'yet supported.') - - grid_definition_template_0(cube, grib) - - elif isinstance(cs, RotatedGeogCS): - # Rotated coordinate system cases. - # Choose between GDT 3.1 and 3.5 according to coordinate regularity. - if regular_x_and_y: - grid_definition_template_1(cube, grib) - else: - grid_definition_template_5(cube, grib) - - elif isinstance(cs, TransverseMercator): - # Transverse Mercator coordinate system (template 3.12). - grid_definition_template_12(cube, grib) - - elif isinstance(cs, LambertConformal): - # Lambert Conformal coordinate system (template 3.30). - grid_definition_template_30(cube, grib) - - else: - raise ValueError('Grib saving is not supported for coordinate system: ' - '{}'.format(cs)) - - -############################################################################### -# -# Product Definition Section 4 -# -############################################################################### - -def set_discipline_and_parameter(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(grib, "discipline", grib2_info.discipline) - gribapi.grib_set(grib, "parameterCategory", grib2_info.category) - gribapi.grib_set(grib, "parameterNumber", grib2_info.number) - else: - gribapi.grib_set(grib, "discipline", 255) - gribapi.grib_set(grib, "parameterCategory", 255) - gribapi.grib_set(grib, "parameterNumber", 255) - warnings.warn('Unable to determine Grib2 parameter code for cube.\n' - 'discipline, parameterCategory and parameterNumber ' - 'have been set to "missing".') - - -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) - - return rt, rt_meaning, fp, grib_time_code - - -def _missing_forecast_period(cube): - """ - Returns a reference time and significance code together with a forecast - period and corresponding units type code. - - """ - t_coord = cube.coord("time") - - if cube.coords('forecast_reference_time'): - # Make copies and convert them to common "hours since" units. - hours_since = cf_units.Unit('hours since epoch', - calendar=t_coord.units.calendar) - frt_coord = cube.coord('forecast_reference_time').copy() - frt_coord.convert_units(hours_since) - t_coord = t_coord.copy() - t_coord.convert_units(hours_since) - # Extract values. - t = t_coord.bounds[0, 0] if t_coord.has_bounds() else t_coord.points[0] - frt = frt_coord.points[0] - # Calculate GRIB parameters. - rt = frt_coord.units.num2date(frt) - rt_meaning = 1 # Forecast reference time. - fp = t - frt - integer_fp = int(fp) - if integer_fp != fp: - msg = 'Truncating floating point forecast period {} to ' \ - 'integer value {}' - warnings.warn(msg.format(fp, integer_fp)) - fp = integer_fp - fp_meaning = 1 # Hours - else: - # With no forecast period or forecast reference time set assume a - # reference time significance of "Observation time" and set the - # forecast period to 0h. - t = t_coord.bounds[0, 0] if t_coord.has_bounds() else t_coord.points[0] - rt = t_coord.units.num2date(t) - rt_meaning = 3 # Observation time - fp = 0 - fp_meaning = 1 # Hours - - return rt, rt_meaning, fp, fp_meaning - - -def set_forecast_time(cube, grib): - """ - Set the forecast time keys based on the forecast_period coordinate. In - the absence of a forecast_period and forecast_reference_time, - the forecast time is set to zero. - - """ - 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(grib, "indicatorOfUnitOfTimeRange", grib_time_code) - gribapi.grib_set(grib, "forecastTime", fp) - - -def set_fixed_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") - - elif cube.coords("air_potential_temperature"): - grib_v_code = 107 - output_unit = cf_units.Unit('K') - v_coord = cube.coord("air_potential_temperature") - - # 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(grib, "typeOfFirstFixedSurface", 1) - gribapi.grib_set(grib, "scaleFactorOfFirstFixedSurface", 0) - gribapi.grib_set(grib, "scaledValueOfFirstFixedSurface", 0) - # Set secondary surface = 'missing'. - gribapi.grib_set(grib, "typeOfSecondFixedSurface", -1) - gribapi.grib_set(grib, "scaleFactorOfSecondFixedSurface", 255) - gribapi.grib_set(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(grib, "typeOfFirstFixedSurface", grib_v_code) - gribapi.grib_set(grib, "scaleFactorOfFirstFixedSurface", 0) - gribapi.grib_set(grib, "scaledValueOfFirstFixedSurface", output_v) - gribapi.grib_set(grib, "typeOfSecondFixedSurface", -1) - gribapi.grib_set(grib, "scaleFactorOfSecondFixedSurface", 255) - gribapi.grib_set(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(grib, "typeOfFirstFixedSurface", grib_v_code) - gribapi.grib_set(grib, "typeOfSecondFixedSurface", grib_v_code) - gribapi.grib_set(grib, "scaleFactorOfFirstFixedSurface", 0) - gribapi.grib_set(grib, "scaleFactorOfSecondFixedSurface", 0) - gribapi.grib_set(grib, "scaledValueOfFirstFixedSurface", - int(output_v[0])) - gribapi.grib_set(grib, "scaledValueOfSecondFixedSurface", - int(output_v[1])) - - -def set_time_range(time_coord, grib): - """ - Set the time range keys in the specified message - based on the bounds of the provided time coordinate. - - """ - if len(time_coord.points) != 1: - msg = 'Expected length one time coordinate, got {} points' - raise ValueError(msg.format(len(time_coord.points))) - - if time_coord.nbounds != 2: - msg = 'Expected time coordinate with two bounds, got {} bounds' - raise ValueError(msg.format(time_coord.nbounds)) - - # Set type to hours and convert period to this unit. - gribapi.grib_set(grib, "indicatorOfUnitForTimeRange", - _TIME_RANGE_UNITS['hours']) - hours_since_units = cf_units.Unit('hours since epoch', - calendar=time_coord.units.calendar) - start_hours, end_hours = time_coord.units.convert(time_coord.bounds[0], - hours_since_units) - # Cast from np.float to Python int. The lengthOfTimeRange key is a - # 4 byte integer so we cast to highlight truncation of any floating - # point value. The grib_api will do the cast from float to int, but it - # cannot handle numpy floats. - time_range_in_hours = end_hours - start_hours - integer_hours = int(time_range_in_hours) - if integer_hours != time_range_in_hours: - msg = 'Truncating floating point lengthOfTimeRange {} to ' \ - 'integer value {}' - warnings.warn(msg.format(time_range_in_hours, integer_hours)) - gribapi.grib_set(grib, "lengthOfTimeRange", integer_hours) - - -def set_time_increment(cell_method, grib): - """ - Set the time increment keys in the specified message - based on the provided cell method. - - """ - # Type of time increment, e.g incrementing forecast period, incrementing - # forecast reference time, etc. Set to missing, but we could use the - # cell method coord to infer a value (see code table 4.11). - gribapi.grib_set(grib, "typeOfTimeIncrement", 255) - - # Default values for the time increment value and units type. - inc = 0 - units_type = 255 - # Attempt to determine time increment from cell method intervals string. - intervals = cell_method.intervals - if intervals is not None and len(intervals) == 1: - interval, = intervals - try: - inc, units = interval.split() - inc = float(inc) - if units in ('hr', 'hour', 'hours'): - units_type = _TIME_RANGE_UNITS['hours'] - else: - raise ValueError('Unable to parse units of interval') - except ValueError: - # Problem interpreting the interval string. - inc = 0 - units_type = 255 - else: - # Cast to int as timeIncrement key is a 4 byte integer. - integer_inc = int(inc) - if integer_inc != inc: - warnings.warn('Truncating floating point timeIncrement {} to ' - 'integer value {}'.format(inc, integer_inc)) - inc = integer_inc - - gribapi.grib_set(grib, "indicatorOfUnitForTimeIncrement", units_type) - gribapi.grib_set(grib, "timeIncrement", inc) - - -def _cube_is_time_statistic(cube): - """ - Test whether we can identify this cube as a statistic over time. - - 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. - - """ - 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 - - return result - - -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): - """ - Set keys within the provided grib message that are common across - all of the supported product definition templates. - - """ - set_discipline_and_parameter(cube, grib) - - # Various missing values. - gribapi.grib_set(grib, "typeOfGeneratingProcess", 255) - gribapi.grib_set(grib, "backgroundProcess", 255) - gribapi.grib_set(grib, "generatingProcessIdentifier", 255) - - # Generic time handling. - set_forecast_time(cube, grib) - - # Handle vertical coords. - set_fixed_surfaces(cube, grib) - - -def product_definition_template_0(cube, grib): - """ - Set keys within the provided grib message based on Product - Definition Template 4.0. - - Template 4.0 is used to represent an analysis or forecast at - a horizontal level at a point in time. - - """ - gribapi.grib_set_long(grib, "productDefinitionTemplateNumber", 0) - 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 - Definition Template 4.8. - - Template 4.8 is used to represent an aggregation over a time - interval. - - """ - gribapi.grib_set(grib, "productDefinitionTemplateNumber", 8) - _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.11. - - Template 4.11 is used to represent an aggregation over a time - interval for an ensemble member. - - """ - gribapi.grib_set(grib, "productDefinitionTemplateNumber", 11) - set_ensemble(cube, grib) - _product_definition_template_8_10_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. - - Templates 4.8 and 4.11 are used to represent aggregations over a time - interval. - - """ - product_definition_template_common(cube, grib) - - # Check for time coordinate. - time_coord = cube.coord('time') - - if len(time_coord.points) != 1: - msg = 'Expected length one time coordinate, got {} points' - raise ValueError(msg.format(time_coord.points)) - - if time_coord.nbounds != 2: - msg = 'Expected time coordinate with two bounds, got {} bounds' - raise ValueError(msg.format(time_coord.nbounds)) - - # Extract the datetime-like object corresponding to the end of - # the overall processing interval. - end = time_coord.units.num2date(time_coord.bounds[0, -1]) - - # Set the associated keys for the end of the interval (octets 35-41 - # in section 4). - gribapi.grib_set(grib, "yearOfEndOfOverallTimeInterval", end.year) - gribapi.grib_set(grib, "monthOfEndOfOverallTimeInterval", end.month) - gribapi.grib_set(grib, "dayOfEndOfOverallTimeInterval", end.day) - gribapi.grib_set(grib, "hourOfEndOfOverallTimeInterval", end.hour) - gribapi.grib_set(grib, "minuteOfEndOfOverallTimeInterval", end.minute) - gribapi.grib_set(grib, "secondOfEndOfOverallTimeInterval", end.second) - - # Only one time range specification. If there were a series of aggregations - # (e.g. the mean of an accumulation) one might set this to a higher value, - # but we currently only handle a single time related cell method. - gribapi.grib_set(grib, "numberOfTimeRange", 1) - gribapi.grib_set(grib, "numberOfMissingInStatisticalProcess", 0) - - # Period over which statistical processing is performed. - set_time_range(time_coord, 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): - """ - Set keys within the provided grib message based on Product - Definition Template 4.40. - - Template 4.40 is used to represent an analysis or forecast at a horizontal - level or in a horizontal layer at a point in time for atmospheric chemical - constituents. - - """ - gribapi.grib_set(grib, "productDefinitionTemplateNumber", 40) - product_definition_template_common(cube, grib) - constituent_type = cube.attributes['WMO_constituent_type'] - gribapi.grib_set(grib, "constituentType", constituent_type) - - -def product_definition_section(cube, grib): - """ - Set keys within the product definition section of the provided - grib message based on the properties of the cube. - - """ - if not cube.coord("time").has_bounds(): - 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: - # forecast (template 4.0) - product_definition_template_0(cube, grib) - elif _cube_is_time_statistic(cube): - 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 - try: - pdt(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)) - else: - # Don't know how to handle this kind of data - msg = 'A suitable product template could not be deduced' - raise iris.exceptions.TranslationError(msg) - - -############################################################################### -# -# Data Representation Section 5 -# -############################################################################### - -def data_section(cube, grib): - # Masked data? - if ma.isMaskedArray(cube.data): - 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 cube'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 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") - - # Time period exists? - if not cube.coords("time"): - raise iris.exceptions.TranslationError("time coord not found") - - -def run(cube, grib): - """ - Set 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_definition_section(cube, grib) - - # Section 4 - Product Definition Section (Product Definition Template) - product_definition_section(cube, grib) - - # Section 5 - Data Representation Section (Data Representation Template) - data_section(cube, grib) diff --git a/lib/iris/fileformats/grib/grib_phenom_translation.py b/lib/iris/fileformats/grib/grib_phenom_translation.py deleted file mode 100644 index 68c533666b..0000000000 --- a/lib/iris/fileformats/grib/grib_phenom_translation.py +++ /dev/null @@ -1,333 +0,0 @@ -# (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 . -''' -Provide grib 1 and 2 phenomenon translations to + from CF terms. - -This is done by wrapping '_grib_cf_map.py', -which is in a format provided by the metadata translation project. - -Currently supports only these ones: - -* grib1 --> cf -* grib2 --> cf -* cf --> grib2 - -''' - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa -import six - -import collections -import warnings - -import cf_units - -from iris.fileformats.grib import _grib_cf_map as grcf -import iris.std_names - - -class _LookupTable(dict): - """ - Specialised dictionary object for making lookup tables. - - Returns None for unknown keys (instead of raising exception). - Raises exception for any attempt to change an existing entry, - (but it is still possible to remove keys) - - """ - def __init__(self, *args, **kwargs): - self._super = super(_LookupTable, self) - self._super.__init__(*args, **kwargs) - - def __getitem__(self, key): - if key not in self: - return None - return self._super.__getitem__(key) - - def __setitem__(self, key, value): - if key in self and self[key] is not value: - raise KeyError('Attempted to set dict[{}] = {}, ' - 'but this is already set to {}.'.format( - key, value, self[key])) - self._super.__setitem__(key, value) - - -# Define namedtuples for keys+values of the Grib1 lookup table. - -_Grib1ToCfKeyClass = collections.namedtuple( - 'Grib1CfKey', - ('table2_version', 'centre_number', 'param_number')) - -# NOTE: this form is currently used for both Grib1 *and* Grib2 -_GribToCfDataClass = collections.namedtuple( - 'Grib1CfData', - ('standard_name', 'long_name', 'units', 'set_height')) - - -# Create the grib1-to-cf lookup table. - -def _make_grib1_cf_table(): - """ Build the Grib1 to CF phenomenon translation table. """ - table = _LookupTable() - - def _make_grib1_cf_entry(table2_version, centre_number, param_number, - standard_name, long_name, units, set_height=None): - """ - Check data, convert types and create a new _GRIB1_CF_TABLE key/value. - - Note that set_height is an optional parameter. Used to denote - phenomena that imply a height definition (agl), - e.g. "2-metre tempererature". - - """ - grib1_key = _Grib1ToCfKeyClass(table2_version=int(table2_version), - centre_number=int(centre_number), - param_number=int(param_number)) - if standard_name is not None: - if standard_name not in iris.std_names.STD_NAMES: - warnings.warn('{} is not a recognised CF standard name ' - '(skipping).'.format(standard_name)) - return None - # convert units string to iris Unit (i.e. mainly, check it is good) - a_cf_unit = cf_units.Unit(units) - cf_data = _GribToCfDataClass(standard_name=standard_name, - long_name=long_name, - units=a_cf_unit, - set_height=set_height) - return (grib1_key, cf_data) - - # Interpret the imported Grib1-to-CF table. - for (grib1data, cfdata) in six.iteritems(grcf.GRIB1_LOCAL_TO_CF): - assert grib1data.edition == 1 - association_entry = _make_grib1_cf_entry( - table2_version=grib1data.t2version, - centre_number=grib1data.centre, - param_number=grib1data.iParam, - standard_name=cfdata.standard_name, - long_name=cfdata.long_name, - units=cfdata.units) - if association_entry is not None: - key, value = association_entry - table[key] = value - - # Do the same for special Grib1 codes that include an implied height level. - for (grib1data, (cfdata, extra_dimcoord)) \ - in six.iteritems(grcf.GRIB1_LOCAL_TO_CF_CONSTRAINED): - assert grib1data.edition == 1 - if extra_dimcoord.standard_name != 'height': - raise ValueError('Got implied dimension coord of "{}", ' - 'currently can only handle "height".'.format( - extra_dimcoord.standard_name)) - if extra_dimcoord.units != 'm': - raise ValueError('Got implied dimension units of "{}", ' - 'currently can only handle "m".'.format( - extra_dimcoord.units)) - if len(extra_dimcoord.points) != 1: - raise ValueError('Implied dimension has {} points, ' - 'currently can only handle 1.'.format( - len(extra_dimcoord.points))) - association_entry = _make_grib1_cf_entry( - table2_version=int(grib1data.t2version), - centre_number=int(grib1data.centre), - param_number=int(grib1data.iParam), - standard_name=cfdata.standard_name, - long_name=cfdata.long_name, - units=cfdata.units, - set_height=extra_dimcoord.points[0]) - if association_entry is not None: - key, value = association_entry - table[key] = value - - return table - - -_GRIB1_CF_TABLE = _make_grib1_cf_table() - - -# Define a namedtuple for the keys of the Grib2 lookup table. - -_Grib2ToCfKeyClass = collections.namedtuple( - 'Grib2CfKey', - ('param_discipline', 'param_category', 'param_number')) - - -# Create the grib2-to-cf lookup table. - -def _make_grib2_to_cf_table(): - """ Build the Grib2 to CF phenomenon translation table. """ - table = _LookupTable() - - def _make_grib2_cf_entry(param_discipline, param_category, param_number, - standard_name, long_name, units): - """ - Check data, convert types and make a _GRIB2_CF_TABLE key/value pair. - - Note that set_height is an optional parameter. Used to denote - phenomena that imply a height definition (agl), - e.g. "2-metre tempererature". - - """ - grib2_key = _Grib2ToCfKeyClass(param_discipline=int(param_discipline), - param_category=int(param_category), - param_number=int(param_number)) - if standard_name is not None: - if standard_name not in iris.std_names.STD_NAMES: - warnings.warn('{} is not a recognised CF standard name ' - '(skipping).'.format(standard_name)) - return None - # convert units string to iris Unit (i.e. mainly, check it is good) - a_cf_unit = cf_units.Unit(units) - cf_data = _GribToCfDataClass(standard_name=standard_name, - long_name=long_name, - units=a_cf_unit, - set_height=None) - return (grib2_key, cf_data) - - # Interpret the grib2 info from grib_cf_map - for grib2data, cfdata in six.iteritems(grcf.GRIB2_TO_CF): - assert grib2data.edition == 2 - association_entry = _make_grib2_cf_entry( - param_discipline=grib2data.discipline, - param_category=grib2data.category, - param_number=grib2data.number, - standard_name=cfdata.standard_name, - long_name=cfdata.long_name, - units=cfdata.units) - if association_entry is not None: - key, value = association_entry - table[key] = value - - return table - - -_GRIB2_CF_TABLE = _make_grib2_to_cf_table() - - -# Define namedtuples for key+values of the cf-to-grib2 lookup table. - -_CfToGrib2KeyClass = collections.namedtuple( - 'CfGrib2Key', - ('standard_name', 'long_name')) - -_CfToGrib2DataClass = collections.namedtuple( - 'CfGrib2Data', - ('discipline', 'category', 'number', 'units')) - - -# Create the cf-to-grib2 lookup table. - -def _make_cf_to_grib2_table(): - """ Build the Grib1 to CF phenomenon translation table. """ - table = _LookupTable() - - def _make_cf_grib2_entry(standard_name, long_name, - param_discipline, param_category, param_number, - units): - """ - Check data, convert types and make a new _CF_TABLE key/value pair. - - """ - assert standard_name is not None or long_name is not None - if standard_name is not None: - long_name = None - if standard_name not in iris.std_names.STD_NAMES: - warnings.warn('{} is not a recognised CF standard name ' - '(skipping).'.format(standard_name)) - return None - cf_key = _CfToGrib2KeyClass(standard_name, long_name) - # convert units string to iris Unit (i.e. mainly, check it is good) - a_cf_unit = cf_units.Unit(units) - grib2_data = _CfToGrib2DataClass(discipline=int(param_discipline), - category=int(param_category), - number=int(param_number), - units=a_cf_unit) - return (cf_key, grib2_data) - - # Interpret the imported CF-to-Grib2 table into a lookup table - for cfdata, grib2data in six.iteritems(grcf.CF_TO_GRIB2): - assert grib2data.edition == 2 - a_cf_unit = cf_units.Unit(cfdata.units) - association_entry = _make_cf_grib2_entry( - standard_name=cfdata.standard_name, - long_name=cfdata.long_name, - param_discipline=grib2data.discipline, - param_category=grib2data.category, - param_number=grib2data.number, - units=a_cf_unit) - if association_entry is not None: - key, value = association_entry - table[key] = value - - return table - -_CF_GRIB2_TABLE = _make_cf_to_grib2_table() - - -# Interface functions for translation lookup - -def grib1_phenom_to_cf_info(table2_version, centre_number, param_number): - """ - Lookup grib-1 parameter --> cf_data or None. - - Returned cf_data has attributes: - - * standard_name - * long_name - * units : a :class:`cf_units.Unit` - * set_height : a scalar 'height' value , or None - - """ - grib1_key = _Grib1ToCfKeyClass(table2_version=table2_version, - centre_number=centre_number, - param_number=param_number) - return _GRIB1_CF_TABLE[grib1_key] - - -def grib2_phenom_to_cf_info(param_discipline, param_category, param_number): - """ - Lookup grib-2 parameter --> cf_data or None. - - Returned cf_data has attributes: - - * standard_name - * long_name - * units : a :class:`cf_units.Unit` - - """ - grib2_key = _Grib2ToCfKeyClass(param_discipline=int(param_discipline), - param_category=int(param_category), - param_number=int(param_number)) - return _GRIB2_CF_TABLE[grib2_key] - - -def cf_phenom_to_grib2_info(standard_name, long_name=None): - """ - Lookup CF names --> grib2_data or None. - - Returned grib2_data has attributes: - - * discipline - * category - * number - * units : a :class:`cf_units.Unit` - The unit represents the defined reference units for the message data. - - """ - if standard_name is not None: - long_name = None - return _CF_GRIB2_TABLE[(standard_name, long_name)] diff --git a/lib/iris/fileformats/grib/message.py b/lib/iris/fileformats/grib/message.py deleted file mode 100644 index 7869aa32c9..0000000000 --- a/lib/iris/fileformats/grib/message.py +++ /dev/null @@ -1,484 +0,0 @@ -# (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 . -""" -Defines a lightweight wrapper class to wrap a single GRIB message. - -""" - -from __future__ import (absolute_import, division, print_function) -from six.moves import (filter, input, map, range, zip) # noqa -import six - -from collections import namedtuple -import re - -import gribapi -import numpy as np -import numpy.ma as ma - -from iris._lazy_data import as_lazy_data -from iris.exceptions import TranslationError - - -class _OpenFileRef(object): - """ - A reference to an open file that ensures that the file is closed - when the object is garbage collected. - """ - def __init__(self, open_file): - self.open_file = open_file - - def __del__(self): - if not self.open_file.closed: - self.open_file.close() - - -class GribMessage(object): - """ - An in-memory representation of a GribMessage, providing - access to the :meth:`~GribMessage.data` payload and the metadata - elements by section via the :meth:`~GribMessage.sections` property. - - """ - - @staticmethod - def messages_from_filename(filename): - """ - Return a generator of :class:`GribMessage` instances; one for - each message in the supplied GRIB file. - - Args: - - * filename (string): - Name of the file to generate fields from. - - """ - grib_fh = open(filename, 'rb') - # create an _OpenFileRef to manage the closure of the file handle - file_ref = _OpenFileRef(grib_fh) - - while True: - offset = grib_fh.tell() - grib_id = gribapi.grib_new_from_file(grib_fh) - if grib_id is None: - break - raw_message = _RawGribMessage(grib_id) - recreate_raw = _MessageLocation(filename, offset) - yield GribMessage(raw_message, recreate_raw, file_ref=file_ref) - - def __init__(self, raw_message, recreate_raw, file_ref=None): - """ - It is recommended to obtain GribMessage instance from the static method - :meth:`~GribMessage.messages_from_filename`, rather than creating - them directly. - - """ - # A RawGribMessage giving gribapi access to the original grib message. - self._raw_message = raw_message - # A _MessageLocation which dask uses to read the message data array, - # by which time this message may be dead and the original grib file - # closed. - self._recreate_raw = recreate_raw - # An _OpenFileRef to keep the grib file open while this GribMessage is - # alive, so that we can always use self._raw_message to fetch keys. - self._file_ref = file_ref - - @property - def sections(self): - """ - Return the key-value pairs of the message keys, grouped by containing - section. - - Sections in a message are indexed by GRIB section-number, - and values in a section are indexed by key strings. - - .. For example:: - - print(grib_message.sections[4]['parameterNumber']) - grib_message.sections[1]['minute'] = 0 - - """ - return self._raw_message.sections - - @property - def bmdi(self): - # Not sure of any cases where GRIB provides a fill value. - # Default for fill value is None. - return None - - def core_data(self): - return self.data - - @property - def data(self): - """ - The data array from the GRIB message as a dask Array. - - The shape of the array will match the logical shape of the - message's grid. For example, a simple global grid would be - available as a 2-dimensional array with shape (Nj, Ni). - - """ - sections = self.sections - grid_section = sections[3] - if grid_section['sourceOfGridDefinition'] != 0: - raise TranslationError( - 'Unsupported source of grid definition: {}'.format( - grid_section['sourceOfGridDefinition'])) - - reduced = (grid_section['numberOfOctectsForNumberOfPoints'] != 0 or - grid_section['interpretationOfNumberOfPoints'] != 0) - template = grid_section['gridDefinitionTemplateNumber'] - if reduced and template not in (40,): - raise TranslationError('Grid definition Section 3 contains ' - 'unsupported quasi-regular grid.') - - if template in (0, 1, 5, 12, 20, 30, 40, 90): - # We can ignore the first two bits (i-neg, j-pos) because - # that is already captured in the coordinate values. - if grid_section['scanningMode'] & 0x3f: - msg = 'Unsupported scanning mode: {}'.format( - grid_section['scanningMode']) - raise TranslationError(msg) - if template in (20, 30, 90): - shape = (grid_section['Ny'], grid_section['Nx']) - elif template == 40 and reduced: - shape = (grid_section['numberOfDataPoints'],) - else: - shape = (grid_section['Nj'], grid_section['Ni']) - proxy = _DataProxy(shape, np.dtype('f8'), self._recreate_raw) - data = as_lazy_data(proxy) - else: - fmt = 'Grid definition template {} is not supported' - raise TranslationError(fmt.format(template)) - return data - - def __getstate__(self): - """ - Alter state of object prior to pickle, ensure open file is closed. - - """ - if not self._file_ref.open_file.closed: - self._file_ref.open_file.close() - return self - - -class _MessageLocation(namedtuple('_MessageLocation', 'filename offset')): - """A reference to a specific GRIB message within a file.""" - - __slots__ = () - - def __call__(self): - return _RawGribMessage.from_file_offset(self.filename, self.offset) - - -class _DataProxy(object): - """A reference to the data payload of a single GRIB message.""" - - __slots__ = ('shape', 'dtype', 'recreate_raw') - - def __init__(self, shape, dtype, recreate_raw): - self.shape = shape - self.dtype = dtype - self.recreate_raw = recreate_raw - - @property - def ndim(self): - return len(self.shape) - - def _bitmap(self, bitmap_section): - """ - Get the bitmap for the data from the message. The GRIB spec defines - that the bitmap is composed of values 0 or 1, where: - - * 0: no data value at corresponding data point (data point masked). - * 1: data value at corresponding data point (data point unmasked). - - The bitmap can take the following values: - - * 0: Bitmap applies to the data and is specified in this section - of this message. - * 1-253: Bitmap applies to the data, is specified by originating - centre and is not specified in section 6 of this message. - * 254: Bitmap applies to the data, is specified in an earlier - section 6 of this message and is not specified in this - section 6 of this message. - * 255: Bitmap does not apply to the data. - - Only values 0 and 255 are supported. - - Returns the bitmap as a 1D array of length equal to the - number of data points in the message. - - """ - # Reference GRIB2 Code Table 6.0. - bitMapIndicator = bitmap_section['bitMapIndicator'] - - if bitMapIndicator == 0: - bitmap = bitmap_section['bitmap'] - elif bitMapIndicator == 255: - bitmap = None - else: - msg = 'Bitmap Section 6 contains unsupported ' \ - 'bitmap indicator [{}]'.format(bitMapIndicator) - raise TranslationError(msg) - return bitmap - - def __getitem__(self, keys): - # NB. Currently assumes that the validity of this interpretation - # is checked before this proxy is created. - message = self.recreate_raw() - sections = message.sections - bitmap_section = sections[6] - bitmap = self._bitmap(bitmap_section) - data = sections[7]['codedValues'] - - if bitmap is not None: - # Note that bitmap and data are both 1D arrays at this point. - if np.count_nonzero(bitmap) == data.shape[0]: - # Only the non-masked values are included in codedValues. - _data = np.empty(shape=bitmap.shape) - _data[bitmap.astype(bool)] = data - # `ma.masked_array` masks where input = 1, the opposite of - # the behaviour specified by the GRIB spec. - data = ma.masked_array(_data, mask=np.logical_not(bitmap), - fill_value=np.nan) - else: - msg = 'Shapes of data and bitmap do not match.' - raise TranslationError(msg) - - data = data.reshape(self.shape) - - return data.__getitem__(keys) - - def __repr__(self): - msg = '<{self.__class__.__name__} shape={self.shape} ' \ - 'dtype={self.dtype!r} recreate_raw={self.recreate_raw!r} ' - return msg.format(self=self) - - def __getstate__(self): - return {attr: getattr(self, attr) for attr in self.__slots__} - - def __setstate__(self, state): - for key, value in six.iteritems(state): - setattr(self, key, value) - - -class _RawGribMessage(object): - """ - Lightweight GRIB message wrapper, containing **only** the coded keys - of the input GRIB message. - - """ - _NEW_SECTION_KEY_MATCHER = re.compile(r'section([0-9]{1})Length') - - @staticmethod - def from_file_offset(filename, offset): - with open(filename, 'rb') as f: - f.seek(offset) - message_id = gribapi.grib_new_from_file(f) - if message_id is None: - fmt = 'Invalid GRIB message: {} @ {}' - raise RuntimeError(fmt.format(filename, offset)) - return _RawGribMessage(message_id) - - def __init__(self, message_id): - """ - A _RawGribMessage object contains the **coded** keys from a - GRIB message that is identified by the input message id. - - Args: - - * message_id: - An integer generated by gribapi referencing a GRIB message within - an open GRIB file. - - """ - self._message_id = message_id - self._sections = None - - def __del__(self): - """ - Release the gribapi reference to the message at end of object's life. - - """ - gribapi.grib_release(self._message_id) - - @property - def sections(self): - """ - Return the key-value pairs of the message keys, grouped by containing - section. - - Key-value pairs are collected into a dictionary of - :class:`Section` objects. One such object is made for - each section in the message, such that the section number is the - object's key in the containing dictionary. Each object contains - key-value pairs for all of the message keys in the given section. - - """ - if self._sections is None: - self._sections = self._get_message_sections() - return self._sections - - def _get_message_keys(self): - """Creates a generator of all the keys in the message.""" - - keys_itr = gribapi.grib_keys_iterator_new(self._message_id) - gribapi.grib_skip_computed(keys_itr) - while gribapi.grib_keys_iterator_next(keys_itr): - yield gribapi.grib_keys_iterator_get_name(keys_itr) - gribapi.grib_keys_iterator_delete(keys_itr) - - def _get_message_sections(self): - """ - Group keys by section. - - Returns a dictionary mapping section number to :class:`Section` - instance. - - .. seealso:: - The sections property (:meth:`~sections`). - - """ - sections = {} - # The first keys in a message are for the whole message and are - # contained in section 0. - section = new_section = 0 - section_keys = [] - - for key_name in self._get_message_keys(): - # The `section<1-7>Length` keys mark the start of each new - # section, except for section 8 which is marked by the key '7777'. - key_match = re.match(self._NEW_SECTION_KEY_MATCHER, key_name) - if key_match is not None: - new_section = int(key_match.group(1)) - elif key_name == '7777': - new_section = 8 - if section != new_section: - sections[section] = Section(self._message_id, section, - section_keys) - section_keys = [] - section = new_section - section_keys.append(key_name) - sections[section] = Section(self._message_id, section, section_keys) - return sections - - -class Section(object): - """ - A Section of a GRIB message, supporting dictionary like access to - attributes using gribapi key strings. - - Values for keys may be changed using assignment but this does not - write to the file. - - """ - # Keys are read from the file as required and values are cached. - # Within GribMessage instances all keys will have been fetched - - def __init__(self, message_id, number, keys): - self._message_id = message_id - self._number = number - self._keys = keys - self._cache = {} - - def __repr__(self): - items = [] - for key in self._keys: - value = self._cache.get(key, '?') - items.append('{}={}'.format(key, value)) - return '<{} {}: {}>'.format(type(self).__name__, self._number, - ', '.join(items)) - - def __getitem__(self, key): - if key not in self._cache: - if key == 'numberOfSection': - value = self._number - elif key not in self._keys: - raise KeyError('{!r} not defined in section {}'.format( - key, self._number)) - else: - value = self._get_key_value(key) - self._cache[key] = value - return self._cache[key] - - def __setitem__(self, key, value): - # Allow the overwriting of any entry already in the _cache. - if key in self._cache: - self._cache[key] = value - else: - raise KeyError('{!r} cannot be redefined in ' - 'section {}'.format(key, self._number)) - - def _get_key_value(self, key): - """ - Get the value associated with the given key in the GRIB message. - - Args: - - * key: - The GRIB key to retrieve the value of. - - Returns the value associated with the requested key in the GRIB - message. - - """ - vector_keys = ('codedValues', 'pv', 'satelliteSeries', - 'satelliteNumber', 'instrumentType', - 'scaleFactorOfCentralWaveNumber', - 'scaledValueOfCentralWaveNumber', - 'longitudes', 'latitudes') - if key in vector_keys: - res = gribapi.grib_get_array(self._message_id, key) - elif key == 'bitmap': - # The bitmap is stored as contiguous boolean bits, one bit for each - # data point. GRIBAPI returns these as strings, so it must be - # type-cast to return an array of ints (0, 1). - res = gribapi.grib_get_array(self._message_id, key, int) - elif key in ('typeOfFirstFixedSurface', 'typeOfSecondFixedSurface'): - # By default these values are returned as unhelpful strings but - # we can use int representation to compare against instead. - res = gribapi.grib_get(self._message_id, key, int) - else: - res = gribapi.grib_get(self._message_id, key) - return res - - def get_computed_key(self, key): - """ - Get the computed value associated with the given key in the GRIB - message. - - Args: - - * key: - The GRIB key to retrieve the value of. - - Returns the value associated with the requested key in the GRIB - message. - - """ - vector_keys = ('longitudes', 'latitudes', 'distinctLatitudes') - if key in vector_keys: - res = gribapi.grib_get_array(self._message_id, key) - else: - res = gribapi.grib_get(self._message_id, key) - return res - - def keys(self): - """Return coded keys available in this Section.""" - return self._keys diff --git a/lib/iris/io/__init__.py b/lib/iris/io/__init__.py index 6eeaf8060f..5f5ec0c3e8 100644 --- a/lib/iris/io/__init__.py +++ b/lib/iris/io/__init__.py @@ -256,13 +256,12 @@ def _grib_save(cube, target, append=False, **kwargs): # A simple wrapper for the grib save routine, which allows the saver to be # registered without having the grib implementation installed. try: - import gribapi + from iris_grib import save_grib2 except ImportError: - raise RuntimeError('Unable to save GRIB file - the ECMWF ' - '`gribapi` package is not installed.') - from iris.fileformats import grib as igrib + raise RuntimeError('Unable to save GRIB file - ' + '"iris_grib" package is not installed.') - return igrib.save_grib2(cube, target, append, **kwargs) + save_grib2(cube, target, append, **kwargs) def _check_init_savers(): diff --git a/lib/iris/tests/__init__.py b/lib/iris/tests/__init__.py index bf2d9fef23..1cac7e34f5 100644 --- a/lib/iris/tests/__init__.py +++ b/lib/iris/tests/__init__.py @@ -94,9 +94,8 @@ GDAL_AVAILABLE = True try: - import gribapi + from iris_grib.message import GribMessage GRIB_AVAILABLE = True - from iris.fileformats.grib.message import GribMessage except ImportError: GRIB_AVAILABLE = False @@ -1166,8 +1165,9 @@ class MyPlotTests(test.GraphicsTest): return skip(fn) -skip_grib = unittest.skipIf(not GRIB_AVAILABLE, 'Test(s) require "gribapi", ' - 'which is not available.') +skip_grib = unittest.skipIf(not GRIB_AVAILABLE, + 'Test(s) require "iris-grib" package, ' + 'which is not available.') skip_sample_data = unittest.skipIf(not SAMPLE_DATA_AVAILABLE, diff --git a/lib/iris/tests/integration/format_interop/test_pp_grib.py b/lib/iris/tests/integration/format_interop/test_pp_grib.py index 0b6908d001..3a2607ea23 100644 --- a/lib/iris/tests/integration/format_interop/test_pp_grib.py +++ b/lib/iris/tests/integration/format_interop/test_pp_grib.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. # @@ -25,9 +25,6 @@ import iris -if tests.GRIB_AVAILABLE: - import gribapi - @tests.skip_grib class TestBoundedTime(tests.IrisTest): diff --git a/lib/iris/tests/integration/test_grib2.py b/lib/iris/tests/integration/test_grib2.py index 08fcfc0143..71e80147b9 100644 --- a/lib/iris/tests/integration/test_grib2.py +++ b/lib/iris/tests/integration/test_grib2.py @@ -36,8 +36,8 @@ # Grib support is optional. if tests.GRIB_AVAILABLE: - from iris.fileformats.grib import load_pairs_from_fields - from iris.fileformats.grib.message import GribMessage + from iris_grib import load_pairs_from_fields + from iris_grib.message import GribMessage @tests.skip_data diff --git a/lib/iris/tests/integration/test_pickle.py b/lib/iris/tests/integration/test_pickle.py index c508cb49c2..a6506ea91e 100644 --- a/lib/iris/tests/integration/test_pickle.py +++ b/lib/iris/tests/integration/test_pickle.py @@ -28,7 +28,7 @@ import iris if tests.GRIB_AVAILABLE: import gribapi - from iris.fileformats.grib.message import GribMessage + from iris_grib.message import GribMessage @tests.skip_data diff --git a/lib/iris/tests/system_test.py b/lib/iris/tests/system_test.py index 28d0517b36..b7b495f6cd 100644 --- a/lib/iris/tests/system_test.py +++ b/lib/iris/tests/system_test.py @@ -37,11 +37,6 @@ import iris.tests as tests -if tests.GRIB_AVAILABLE: - import gribapi - import iris.fileformats.grib as grib - - class SystemInitialTest(tests.IrisTest): def system_test_supported_filetypes(self): diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index e2712b58ab..c3abc9beaa 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -84,8 +84,6 @@ class StandardReportWithExclusions(pep8.StandardReport): '*/iris/std_names.py', '*/iris/fileformats/cf.py', '*/iris/fileformats/dot.py', - '*/iris/fileformats/grib/_grib_cf_map.py', - '*/iris/fileformats/grib/_grib1_load_rules.py', '*/iris/fileformats/pp_load_rules.py', '*/iris/fileformats/rules.py', '*/iris/fileformats/um_cf_map.py', @@ -310,8 +308,7 @@ def test_license_headers(self): 'docs/iris/src/developers_guide/gitwash_dumper.py', 'docs/iris/build/*', 'lib/iris/analysis/_scipy_interpolate.py', - 'lib/iris/fileformats/_pyke_rules/*', - 'lib/iris/fileformats/grib/_grib_cf_map.py') + 'lib/iris/fileformats/_pyke_rules/*') try: last_change_by_fname = self.last_change_by_fname() diff --git a/lib/iris/tests/test_grib_load_translations.py b/lib/iris/tests/test_grib_load_translations.py index f052e1be73..dbb6bc8e1f 100644 --- a/lib/iris/tests/test_grib_load_translations.py +++ b/lib/iris/tests/test_grib_load_translations.py @@ -45,7 +45,8 @@ if tests.GRIB_AVAILABLE: import gribapi - import iris.fileformats.grib + import iris.fileformats + import iris_grib def _mock_gribapi_fetch(message, key): @@ -202,7 +203,7 @@ def _run_timetests(self, test_set): # Operates on lists of cases for various time-units and grib-editions. # Format: (edition, code, expected-exception, # equivalent-seconds, description-string) - with mock.patch('iris.fileformats.grib.gribapi', _mock_gribapi): + with mock.patch('iris_grib.gribapi', _mock_gribapi): for test_controls in test_set: ( grib_edition, timeunit_codenum, @@ -219,7 +220,7 @@ def _run_timetests(self, test_set): if expected_error: # Expect GribWrapper construction to fail. with self.assertRaises(type(expected_error)) as ar_context: - msg = iris.fileformats.grib.GribWrapper(message) + msg = iris_grib.GribWrapper(message) self.assertEqual( ar_context.exception.args, expected_error.args) @@ -228,7 +229,7 @@ def _run_timetests(self, test_set): # 'ELSE'... # Expect the wrapper construction to work. # Make a GribWrapper object and test it. - wrapped_msg = iris.fileformats.grib.GribWrapper(message) + wrapped_msg = iris_grib.GribWrapper(message) # Check the units string. forecast_timeunit = wrapped_msg._forecastTimeUnit @@ -325,12 +326,12 @@ def test_warn_unknown_pdts(self): gribapi.grib_write(grib_message, temp_gribfile) # Load the message from the file as a cube. - cube_generator = iris.fileformats.grib.load_cubes( + cube_generator = iris_grib.load_cubes( temp_gribfile_path) with self.assertRaises(iris.exceptions.TranslationError) as te: cube = next(cube_generator) - self.assertEqual('Product definition template [5]' - ' is not supported', str(te.exception)) + self.assertEqual('Product definition template [5]' + ' is not supported', str(te.exception)) @tests.skip_grib @@ -352,12 +353,12 @@ def mock_grib(self): def cube_from_message(self, grib): # Parameter translation now uses the GribWrapper, so we must convert # the Mock-based fake message to a FakeGribMessage. - with mock.patch('iris.fileformats.grib.gribapi', _mock_gribapi): + with mock.patch('iris_grib.gribapi', _mock_gribapi): grib_message = FakeGribMessage(**grib.__dict__) - wrapped_msg = iris.fileformats.grib.GribWrapper(grib_message) + wrapped_msg = iris_grib.GribWrapper(grib_message) cube, _, _ = iris.fileformats.rules._make_cube( wrapped_msg, - iris.fileformats.grib._grib1_load_rules.grib1_convert) + iris_grib._grib1_load_rules.grib1_convert) return cube diff --git a/lib/iris/tests/test_grib_save_rules.py b/lib/iris/tests/test_grib_save_rules.py index ee72a5e141..3cae1e5e04 100644 --- a/lib/iris/tests/test_grib_save_rules.py +++ b/lib/iris/tests/test_grib_save_rules.py @@ -1,4 +1,4 @@ -# (C) British Crown Copyright 2010 - 2015, Met Office +# (C) British Crown Copyright 2010 - 2017, Met Office # # This file is part of Iris. # @@ -33,7 +33,7 @@ if tests.GRIB_AVAILABLE: import gribapi - import iris.fileformats.grib._save_rules as grib_save_rules + import iris_grib._save_rules as grib_save_rules else: gribapi = None diff --git a/lib/iris/tests/test_io_init.py b/lib/iris/tests/test_io_init.py index e516e1c52e..4a946047ce 100644 --- a/lib/iris/tests/test_io_init.py +++ b/lib/iris/tests/test_io_init.py @@ -55,7 +55,6 @@ def test_decode_uri(self): class TestFileFormatPicker(tests.IrisTest): - @tests.skip_grib def test_known_formats(self): self.assertString(str(iff.FORMAT_AGENT), tests.get_result_path(('file_load', diff --git a/lib/iris/tests/test_uri_callback.py b/lib/iris/tests/test_uri_callback.py index e92ed8e667..b344e883ea 100644 --- a/lib/iris/tests/test_uri_callback.py +++ b/lib/iris/tests/test_uri_callback.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. # @@ -30,8 +30,6 @@ class TestCallbacks(tests.IrisTest): @tests.skip_grib def test_grib_callback(self): - import iris.fileformats.grib - def grib_thing_getter(cube, field, filename): if hasattr(field, 'sections'): # New-style loader callback : 'field' is a GribMessage, which has 'sections'. diff --git a/lib/iris/tests/unit/fileformats/grib/__init__.py b/lib/iris/tests/unit/fileformats/grib/__init__.py deleted file mode 100644 index dd03799003..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/__init__.py +++ /dev/null @@ -1,206 +0,0 @@ -# (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 the :mod:`iris.fileformats.grib` package.""" - -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 -from iris.fileformats.grib.message import GribMessage -from iris.tests import mock - - -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/grib1_load_rules/__init__.py b/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/__init__.py deleted file mode 100644 index b0b8dc2d37..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# (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 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/grib1_load_rules/test_grib1_convert.py b/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/test_grib1_convert.py deleted file mode 100644 index e72ad54f9f..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/grib1_load_rules/test_grib1_convert.py +++ /dev/null @@ -1,152 +0,0 @@ -# (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._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 -# importing anything else -import iris.tests as tests - -import cf_units -import gribapi -import mock - -import iris.aux_factory -import iris.coords -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 - - -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): - @staticmethod - def is_forecast_period(coord): - return (coord.standard_name == 'forecast_period' and - coord.units == 'hours') - - @staticmethod - def is_time(coord): - return (coord.standard_name == 'time' and - coord.units == 'hours since epoch') - - def assert_bounded_message(self, **kwargs): - attributes = {'productDefinitionTemplateNumber': 0, - 'edition': 1, '_forecastTime': 15, - '_forecastTimeUnit': 'hours', - 'phenomenon_bounds': lambda u: (80, 120), - '_phenomenonDateTime': -1, - 'table2Version': 9999, - '_originatingCentre': 'xxx'} - attributes.update(kwargs) - message = mock.Mock(**attributes) - self._test_for_coord(message, grib1_convert, self.is_forecast_period, - expected_points=[35], - expected_bounds=[[15, 55]]) - self._test_for_coord(message, grib1_convert, self.is_time, - expected_points=[100], - expected_bounds=[[80, 120]]) - - def test_time_range_indicator_2(self): - self.assert_bounded_message(timeRangeIndicator=2) - - def test_time_range_indicator_3(self): - self.assert_bounded_message(timeRangeIndicator=3) - - def test_time_range_indicator_4(self): - self.assert_bounded_message(timeRangeIndicator=4) - - def test_time_range_indicator_5(self): - self.assert_bounded_message(timeRangeIndicator=5) - - def test_time_range_indicator_51(self): - self.assert_bounded_message(timeRangeIndicator=51) - - def test_time_range_indicator_113(self): - self.assert_bounded_message(timeRangeIndicator=113) - - def test_time_range_indicator_114(self): - self.assert_bounded_message(timeRangeIndicator=114) - - def test_time_range_indicator_115(self): - self.assert_bounded_message(timeRangeIndicator=115) - - def test_time_range_indicator_116(self): - self.assert_bounded_message(timeRangeIndicator=116) - - def test_time_range_indicator_117(self): - self.assert_bounded_message(timeRangeIndicator=117) - - def test_time_range_indicator_118(self): - self.assert_bounded_message(timeRangeIndicator=118) - - def test_time_range_indicator_123(self): - self.assert_bounded_message(timeRangeIndicator=123) - - def test_time_range_indicator_124(self): - self.assert_bounded_message(timeRangeIndicator=124) - - def test_time_range_indicator_125(self): - self.assert_bounded_message(timeRangeIndicator=125) - - -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 = grib1_convert(gw) - - factory, = results[0] - self.assertEqual(factory.factory_class, - iris.aux_factory.HybridPressureFactory) - delta, sigma, ref = factory.args - self.assertEqual(delta, {'long_name': 'level_pressure'}) - self.assertEqual(sigma, {'long_name': 'sigma'}) - self.assertEqual(ref, Reference(name='surface_pressure')) - - ml_ref = iris.coords.CoordDefn('model_level_number', None, None, - cf_units.Unit('1'), - {'positive': 'up'}, None) - lp_ref = iris.coords.CoordDefn(None, 'level_pressure', None, - cf_units.Unit('Pa'), - {}, None) - s_ref = iris.coords.CoordDefn(None, 'sigma', None, - cf_units.Unit('1'), - {}, None) - - aux_coord_defns = [coord._as_defn() for coord, dim in results[8]] - self.assertIn(ml_ref, aux_coord_defns) - self.assertIn(lp_ref, aux_coord_defns) - self.assertIn(s_ref, aux_coord_defns) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 0bc5a8a92b..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# (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/unit/fileformats/grib/grib_phenom_translation/test_grib_phenom_translation.py b/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/test_grib_phenom_translation.py deleted file mode 100644 index eeb61f02cf..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/grib_phenom_translation/test_grib_phenom_translation.py +++ /dev/null @@ -1,167 +0,0 @@ -# (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` module. - -''' -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 cf_units - -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)]) - assert ll['a'] == 1 - assert ll['q'] is None - ll['q'] = 15 - assert ll['q'] == 15 - ll['q'] = 15 - assert ll['q'] == 15 - with self.assertRaises(KeyError): - ll['q'] = 7 - del ll['q'] - ll['q'] = 7 - assert ll['q'] == 7 - - -@tests.skip_grib -class TestGribPhenomenonLookup(tests.IrisTest): - def test_grib1_cf_lookup(self): - def check_grib1_cf(param, - standard_name, long_name, units, - height=None, - t2version=128, centre=98, expect_none=False): - a_cf_unit = cf_units.Unit(units) - cfdata = gptx.grib1_phenom_to_cf_info(param_number=param, - table2_version=t2version, - centre_number=centre) - if expect_none: - self.assertIsNone(cfdata) - else: - self.assertEqual(cfdata.standard_name, standard_name) - self.assertEqual(cfdata.long_name, long_name) - self.assertEqual(cfdata.units, a_cf_unit) - if height is None: - self.assertIsNone(cfdata.set_height) - else: - self.assertEqual(cfdata.set_height, float(height)) - - check_grib1_cf(165, 'x_wind', None, 'm s-1', 10.0) - check_grib1_cf(168, 'dew_point_temperature', None, 'K', 2) - check_grib1_cf(130, 'air_temperature', None, 'K') - check_grib1_cf(235, None, "grib_skin_temperature", "K") - check_grib1_cf(235, None, "grib_skin_temperature", "K", - t2version=9999, expect_none=True) - check_grib1_cf(235, None, "grib_skin_temperature", "K", - centre=9999, expect_none=True) - check_grib1_cf(9999, None, "grib_skin_temperature", "K", - expect_none=True) - - def test_grib2_cf_lookup(self): - def check_grib2_cf(discipline, category, number, - standard_name, long_name, units, - expect_none=False): - a_cf_unit = cf_units.Unit(units) - cfdata = gptx.grib2_phenom_to_cf_info(param_discipline=discipline, - param_category=category, - param_number=number) - if expect_none: - self.assertIsNone(cfdata) - else: - self.assertEqual(cfdata.standard_name, standard_name) - self.assertEqual(cfdata.long_name, long_name) - self.assertEqual(cfdata.units, a_cf_unit) - - # These should work - check_grib2_cf(0, 0, 2, "air_potential_temperature", None, "K") - check_grib2_cf(0, 19, 1, None, "grib_physical_atmosphere_albedo", "%") - check_grib2_cf(2, 0, 2, "soil_temperature", None, "K") - check_grib2_cf(10, 2, 0, "sea_ice_area_fraction", None, 1) - check_grib2_cf(2, 0, 0, "land_area_fraction", None, 1) - check_grib2_cf(0, 19, 1, None, "grib_physical_atmosphere_albedo", "%") - check_grib2_cf(0, 1, 64, - "atmosphere_mass_content_of_water_vapor", None, - "kg m-2") - check_grib2_cf(2, 0, 7, "surface_altitude", None, "m") - - # These should fail - check_grib2_cf(9999, 2, 0, "sea_ice_area_fraction", None, 1, - expect_none=True) - check_grib2_cf(10, 9999, 0, "sea_ice_area_fraction", None, 1, - expect_none=True) - check_grib2_cf(10, 2, 9999, "sea_ice_area_fraction", None, 1, - expect_none=True) - - def test_cf_grib2_lookup(self): - def check_cf_grib2(standard_name, long_name, - discipline, category, number, units, - expect_none=False): - a_cf_unit = cf_units.Unit(units) - gribdata = gptx.cf_phenom_to_grib2_info(standard_name, long_name) - if expect_none: - self.assertIsNone(gribdata) - else: - self.assertEqual(gribdata.discipline, discipline) - self.assertEqual(gribdata.category, category) - self.assertEqual(gribdata.number, number) - self.assertEqual(gribdata.units, a_cf_unit) - - # These should work - check_cf_grib2("sea_surface_temperature", None, - 10, 3, 0, 'K') - check_cf_grib2("air_temperature", None, - 0, 0, 0, 'K') - check_cf_grib2("soil_temperature", None, - 2, 0, 2, "K") - check_cf_grib2("land_area_fraction", None, - 2, 0, 0, '1') - check_cf_grib2("land_binary_mask", None, - 2, 0, 0, '1') - check_cf_grib2("atmosphere_mass_content_of_water_vapor", None, - 0, 1, 64, "kg m-2") - check_cf_grib2("surface_altitude", None, - 2, 0, 7, "m") - - # These should fail - check_cf_grib2("air_temperature", "user_long_UNRECOGNISED", - 0, 0, 0, 'K') - check_cf_grib2("air_temperature_UNRECOGNISED", None, - 0, 0, 0, 'K', - expect_none=True) - check_cf_grib2(None, "user_long_UNRECOGNISED", - 0, 0, 0, 'K', - expect_none=True) - check_cf_grib2(None, "precipitable_water", - 0, 1, 3, 'kg m-2') - check_cf_grib2("invalid_unknown", "precipitable_water", - 0, 1, 3, 'kg m-2', - expect_none=True) - check_cf_grib2(None, None, 0, 0, 0, '', - expect_none=True) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py b/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py deleted file mode 100644 index b6afad2e24..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -"""Unit tests for the :mod:`iris.fileformats.grib._load_convert` package.""" - -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 collections import OrderedDict - - -def empty_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'] = [] - return metadata - - -class LoadConvertTest(tests.IrisTest): - def assertMetadataEqual(self, result, expected): - # Compare two metadata dictionaries. Gives slightly more - # helpful error message than: self.assertEqual(result, expected) - self.assertEqual(result.keys(), expected.keys()) - for key in result.keys(): - self.assertEqual(result[key], expected[key]) 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 deleted file mode 100644 index 186a431074..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test__hindcast_fix.py +++ /dev/null @@ -1,73 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Tests for function :func:`iris.fileformats.grib._load_convert._hindcast_fix`. - -""" - -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 collections import namedtuple - -from iris.fileformats.grib._load_convert import _hindcast_fix as hindcast_fix - - -class TestHindcastFix(tests.IrisTest): - # setup tests : provided value, fix-applies, expected-fixed - FixTest = namedtuple('FixTest', ('given', 'fixable', 'fixed')) - test_values = [ - FixTest(0, False, None), - FixTest(100, False, None), - FixTest(2 * 2**30 - 1, False, None), - FixTest(2 * 2**30, False, None), - FixTest(2 * 2**30 + 1, True, -1), - FixTest(2 * 2**30 + 2, True, -2), - FixTest(3 * 2**30 - 1, True, -(2**30 - 1)), - FixTest(3 * 2**30, False, None)] - - def setUp(self): - self.patch_warn = self.patch('warnings.warn') - - def test_fix(self): - # Check hindcast fixing. - for given, fixable, fixed in self.test_values: - result = hindcast_fix(given) - expected = fixed if fixable else given - self.assertEqual(result, expected) - - def test_fix_warning(self): - # Check warning appears when enabled. - self.patch('iris.fileformats.grib._load_convert.options' - '.warn_on_unsupported', True) - hindcast_fix(2 * 2**30 + 5) - self.assertEqual(self.patch_warn.call_count, 1) - self.assertIn('Re-interpreting large grib forecastTime', - self.patch_warn.call_args[0][0]) - - def test_fix_warning_disabled(self): - # Default is no warning. - hindcast_fix(2 * 2**30 + 5) - self.assertEqual(self.patch_warn.call_count, 0) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index dbfef702c5..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_bitmap_section.py +++ /dev/null @@ -1,46 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.bitmap_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 importing anything else. -import iris.tests as tests - -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import bitmap_section -from iris.tests.unit.fileformats.grib import _make_test_message - - -class Test(tests.IrisTest): - def test_bitmap_unsupported(self): - # bitMapIndicator in range 1-254. - # Note that bitMapIndicator = 1-253 and bitMapIndicator = 254 mean two - # different things, but load_convert treats them identically. - message = _make_test_message({6: {'bitMapIndicator': 100, - 'bitmap': None}}) - with self.assertRaisesRegexp(TranslationError, 'unsupported bitmap'): - bitmap_section(message.sections[6]) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 218791cd3f..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_convert.py +++ /dev/null @@ -1,79 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.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 importing anything else. -import iris.tests as tests - -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import convert -from iris.tests import mock -from iris.tests.unit.fileformats.grib import _make_test_message - - -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 - with mock.patch(this, side_effect=func) as grib2_convert: - # The call being tested. - result = convert(field) - self.assertTrue(grib2_convert.called) - metadata = ([factory], [], None, None, None, {}, [], [], []) - self.assertEqual(result, metadata) - - def test_edition_1_bad(self): - sections = [{'editionNumber': 1}] - field = _make_test_message(sections) - 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 deleted file mode 100644 index 729d2b9b30..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_data_cutoff.py +++ /dev/null @@ -1,77 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Tests for function :func:`iris.fileformats.grib._load_convert.data_cutoff`. - -""" - -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 iris.fileformats.grib._load_convert import _MDI as MDI -from iris.fileformats.grib._load_convert import data_cutoff -from iris.tests import mock - - -class TestDataCutoff(tests.IrisTest): - def _check(self, hours, minutes, request_warning, expect_warning=False): - # Setup the environment. - patch_target = 'iris.fileformats.grib._load_convert.options' - with mock.patch(patch_target) as options: - options.warn_on_unsupported = request_warning - with mock.patch('warnings.warn') as warn: - # The call being tested. - data_cutoff(hours, minutes) - # Check the result. - if expect_warning: - self.assertEqual(len(warn.mock_calls), 1) - args, kwargs = warn.call_args - self.assertIn('data cutoff', args[0]) - else: - self.assertEqual(len(warn.mock_calls), 0) - - def test_neither(self): - self._check(MDI, MDI, False) - - def test_hours(self): - self._check(3, MDI, False) - - def test_minutes(self): - self._check(MDI, 20, False) - - def test_hours_and_minutes(self): - self._check(30, 40, False) - - def test_neither_warning(self): - self._check(MDI, MDI, True, False) - - def test_hours_warning(self): - self._check(3, MDI, True, True) - - def test_minutes_warning(self): - self._check(MDI, 20, True, True) - - def test_hours_and_minutes_warning(self): - self._check(30, 40, True, True) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 973c103999..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid.py +++ /dev/null @@ -1,101 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.ellipsoid. - -""" - -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.ma as ma - -import iris.coord_systems as icoord_systems -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import ellipsoid - - -# Reference GRIB2 Code Table 3.2 - Shape of the Earth. - - -MDI = ma.masked - - -class Test(tests.IrisTest): - def test_shape_unsupported(self): - unsupported = [8, 9, 10, MDI] - emsg = 'unsupported shape of the earth' - for shape in unsupported: - with self.assertRaisesRegexp(TranslationError, emsg): - ellipsoid(shape, MDI, MDI, MDI) - - def test_spherical_default_supported(self): - cs_by_shape = {0: icoord_systems.GeogCS(6367470), - 6: icoord_systems.GeogCS(6371229)} - for shape, expected in cs_by_shape.items(): - result = ellipsoid(shape, MDI, MDI, MDI) - self.assertEqual(result, expected) - - def test_spherical_shape_1_no_radius(self): - shape = 1 - emsg = 'radius to be specified' - with self.assertRaisesRegexp(ValueError, emsg): - ellipsoid(shape, MDI, MDI, MDI) - - def test_spherical_shape_1(self): - shape = 1 - radius = 10 - result = ellipsoid(shape, MDI, MDI, radius) - expected = icoord_systems.GeogCS(radius) - self.assertEqual(result, expected) - - def test_oblate_shape_3_7_no_axes(self): - for shape in [3, 7]: - emsg = 'axis to be specified' - with self.assertRaisesRegexp(ValueError, emsg): - ellipsoid(shape, MDI, MDI, MDI) - - def test_oblate_shape_3_7_no_major(self): - for shape in [3, 7]: - emsg = 'major axis to be specified' - with self.assertRaisesRegexp(ValueError, emsg): - ellipsoid(shape, MDI, 1, MDI) - - def test_oblate_shape_3_7_no_minor(self): - for shape in [3, 7]: - emsg = 'minor axis to be specified' - with self.assertRaisesRegexp(ValueError, emsg): - ellipsoid(shape, 1, MDI, MDI) - - def test_oblate_shape_3_7(self): - for shape in [3, 7]: - major, minor = 1, 10 - scale = 1 - result = ellipsoid(shape, major, minor, MDI) - if shape == 3: - # Convert km to m. - scale = 1000 - expected = icoord_systems.GeogCS(major * scale, minor * scale) - self.assertEqual(result, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 25252da835..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ellipsoid_geometry.py +++ /dev/null @@ -1,47 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.ellipsoid_geometry. - -""" - -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 iris.fileformats.grib._load_convert import ellipsoid_geometry - - -class Test(tests.IrisTest): - def setUp(self): - self.section = {'scaledValueOfEarthMajorAxis': 10, - 'scaleFactorOfEarthMajorAxis': 1, - 'scaledValueOfEarthMinorAxis': 100, - 'scaleFactorOfEarthMinorAxis': 2, - 'scaledValueOfRadiusOfSphericalEarth': 1000, - 'scaleFactorOfRadiusOfSphericalEarth': 3} - - def test_geometry(self): - result = ellipsoid_geometry(self.section) - self.assertEqual(result, (1.0, 1.0, 1.0)) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 8dea616fc8..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_ensemble_identifier.py +++ /dev/null @@ -1,71 +0,0 @@ -# (C) British Crown Copyright 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 . -""" -Test function -:func:`iris.fileformats.grib._load_convert.ensemble_identifier`. - -""" - -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 -import warnings - -from iris.coords import DimCoord -from iris.fileformats.grib._load_convert import ensemble_identifier -from iris.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - module = 'iris.fileformats.grib._load_convert' - self.patch('warnings.warn') - this = '{}.product_definition_template_0'.format(module) - - def _check(self, request_warning): - section = {'perturbationNumber': 17} - this = 'iris.fileformats.grib._load_convert.options' - with mock.patch(this, warn_on_unsupported=request_warning): - realization = ensemble_identifier(section) - expected = DimCoord(section['perturbationNumber'], - standard_name='realization', - units='no_unit') - - if request_warning: - warn_msgs = [mcall[1][0] for mcall in warnings.warn.mock_calls] - expected_msgs = ['type of ensemble', 'number of forecasts'] - for emsg in expected_msgs: - matches = [wmsg for wmsg in warn_msgs if emsg in wmsg] - self.assertEqual(len(matches), 1) - warn_msgs.remove(matches[0]) - else: - self.assertEqual(len(warnings.warn.mock_calls), 0) - - def test_ens_no_warn(self): - self._check(False) - - def test_ens_warn(self): - self._check(True) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 4d98ffdfb9..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_float32_from_int32.py +++ /dev/null @@ -1,47 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Unit tests for `iris.fileformats.grib._load_convert.fixup_float32_from_int32`. - -""" - -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 iris.fileformats.grib._load_convert import fixup_float32_from_int32 - - -class Test(tests.IrisTest): - def test_negative(self): - result = fixup_float32_from_int32(-0x3f000000) - self.assertEqual(result, -0.5) - - def test_zero(self): - result = fixup_float32_from_int32(0) - self.assertEqual(result, 0) - - def test_positive(self): - result = fixup_float32_from_int32(0x3f000000) - self.assertEqual(result, 0.5) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index f1144f14fc..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_fixup_int32_from_uint32.py +++ /dev/null @@ -1,57 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Unit tests for `iris.fileformats.grib._load_convert.fixup_int32_from_uint32`. - -""" - -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 iris.fileformats.grib._load_convert import fixup_int32_from_uint32 - - -class Test(tests.IrisTest): - def test_negative(self): - result = fixup_int32_from_uint32(0x80000005) - self.assertEqual(result, -5) - - def test_negative_zero(self): - result = fixup_int32_from_uint32(0x80000000) - self.assertEqual(result, 0) - - def test_zero(self): - result = fixup_int32_from_uint32(0) - self.assertEqual(result, 0) - - def test_positive(self): - result = fixup_int32_from_uint32(200000) - self.assertEqual(result, 200000) - - def test_already_negative(self): - # If we *already* have a negative value the fixup routine should - # leave it alone. - result = fixup_int32_from_uint32(-7) - self.assertEqual(result, -7) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 3c516a1bff..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_forecast_period_coord.py +++ /dev/null @@ -1,56 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.forecast_period_coord. - -""" - -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 iris.coords import DimCoord -from iris.fileformats.grib._load_convert import forecast_period_coord - - -class Test(tests.IrisTest): - def test(self): - # (indicatorOfUnitOfTimeRange, forecastTime, expected-hours) - times = [(0, 60, 1), # minutes - (1, 2, 2), # hours - (2, 1, 24), # days - (10, 2, 6), # 3 hours - (11, 3, 18), # 6 hours - (12, 2, 24), # 12 hours - (13, 3600, 1)] # seconds - - for indicatorOfUnitOfTimeRange, forecastTime, hours in times: - coord = forecast_period_coord(indicatorOfUnitOfTimeRange, - forecastTime) - self.assertIsInstance(coord, DimCoord) - self.assertEqual(coord.standard_name, 'forecast_period') - self.assertEqual(coord.units, 'hours') - self.assertEqual(coord.shape, (1,)) - self.assertEqual(coord.points[0], hours) - self.assertFalse(coord.has_bounds()) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 3d554bd841..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_generating_process.py +++ /dev/null @@ -1,68 +0,0 @@ -# (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 . -""" -Tests for function -:func:`iris.fileformats.grib._load_convert.generating_process`. - -""" - -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 iris.fileformats.grib._load_convert import generating_process - - -class TestGeneratingProcess(tests.IrisTest): - def setUp(self): - self.warn_patch = self.patch('warnings.warn') - - def test_nowarn(self): - generating_process(None) - self.assertEqual(self.warn_patch.call_count, 0) - - def _check_warnings(self, with_forecast=True): - module = 'iris.fileformats.grib._load_convert' - self.patch(module + '.options.warn_on_unsupported', True) - call_args = [None] - call_kwargs = {} - expected_fragments = [ - 'Unable to translate type of generating process', - 'Unable to translate background generating process'] - if with_forecast: - expected_fragments.append( - 'Unable to translate forecast generating process') - else: - call_kwargs['include_forecast_process'] = False - generating_process(*call_args, **call_kwargs) - got_msgs = [call[0][0] for call in self.warn_patch.call_args_list] - for got_msg, expected_fragment in zip(sorted(got_msgs), - sorted(expected_fragments)): - self.assertIn(expected_fragment, got_msg) - - def test_warn_full(self): - self._check_warnings() - - def test_warn_no_forecast(self): - self._check_warnings(with_forecast=False) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 4888e25330..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grib2_convert.py +++ /dev/null @@ -1,77 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.grib2_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 importing anything else. -import iris.tests as tests - -import copy - -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 - - -class Test(tests.IrisTest): - def setUp(self): - this = 'iris.fileformats.grib._load_convert' - self.patch('{}.reference_time_coord'.format(this), return_value=None) - self.patch('{}.grid_definition_section'.format(this)) - self.patch('{}.product_definition_section'.format(this)) - self.patch('{}.data_representation_section'.format(this)) - self.patch('{}.bitmap_section'.format(this)) - - def test(self): - sections = [{'discipline': mock.sentinel.discipline}, # section 0 - {'centre': 'ecmf', # section 1 - 'tablesVersion': mock.sentinel.tablesVersion}, - None, # section 2 - mock.sentinel.grid_definition_section, # section 3 - mock.sentinel.product_definition_section, # section 4 - mock.sentinel.data_representation_section, # section 5 - mock.sentinel.bitmap_section] # section 6 - field = _make_test_message(sections) - metadata = {'factories': [], 'references': [], - 'standard_name': None, - 'long_name': None, 'units': None, 'attributes': {}, - 'cell_methods': [], 'dim_coords_and_dims': [], - 'aux_coords_and_dims': []} - expected = copy.deepcopy(metadata) - centre = 'European Centre for Medium Range Weather Forecasts' - expected['attributes'] = {'centre': centre} - # The call being tested. - grib2_convert(field, metadata) - self.assertEqual(metadata, expected) - this = iris.fileformats.grib._load_convert - this.reference_time_coord.assert_called_with(sections[1]) - this.grid_definition_section.assert_called_with(sections[3], - expected) - args = (sections[4], expected, sections[0]['discipline'], - sections[1]['tablesVersion'], None) - this.product_definition_section.assert_called_with(*args) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 0d0e4bc9ac..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_0_and_1.py +++ /dev/null @@ -1,62 +0,0 @@ -# (C) British Crown Copyright 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 . -""" -Test function -:func:`iris.fileformats.grib._load_convert.grid_definition_template_0_and_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 iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import \ - grid_definition_template_0_and_1 - - -class Test(tests.IrisTest): - - def test_unsupported_quasi_regular__number_of_octets(self): - section = {'numberOfOctectsForNumberOfPoints': 1} - cs = None - metadata = None - with self.assertRaisesRegexp(TranslationError, 'quasi-regular'): - grid_definition_template_0_and_1(section, - metadata, - 'latitude', - 'longitude', - cs) - - def test_unsupported_quasi_regular__interpretation(self): - section = {'numberOfOctectsForNumberOfPoints': 1, - 'interpretationOfNumberOfPoints': 1} - cs = None - metadata = None - with self.assertRaisesRegexp(TranslationError, 'quasi-regular'): - grid_definition_template_0_and_1(section, - metadata, - 'latitude', - 'longitude', - cs) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index ac59195a3b..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_12.py +++ /dev/null @@ -1,166 +0,0 @@ -# (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 -:func:`iris.fileformats.grib._load_convert.grid_definition_template_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 -# before importing anything else. -import iris.tests as tests - -import numpy as np - -import iris.coord_systems -import iris.coords -import iris.exceptions -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 - - -class Test(tests.IrisTest): - def section_3(self): - section = { - 'shapeOfTheEarth': 7, - 'scaleFactorOfRadiusOfSphericalEarth': MDI, - 'scaledValueOfRadiusOfSphericalEarth': MDI, - 'scaleFactorOfEarthMajorAxis': 3, - 'scaledValueOfEarthMajorAxis': 6377563396, - 'scaleFactorOfEarthMinorAxis': 3, - 'scaledValueOfEarthMinorAxis': 6356256909, - 'Ni': 4, - 'Nj': 3, - 'latitudeOfReferencePoint': 49000000, - 'longitudeOfReferencePoint': -2000000, - 'resolutionAndComponentFlags': 0, - 'scaleFactorAtReferencePoint': 0.9996012717, - 'XR': 40000000, - 'YR': -10000000, - 'scanningMode': 64, - 'Di': 200000, - 'Dj': 100000, - 'X1': 29300000, - 'Y1': 9200000, - 'X2': 29900000, - 'Y2': 9400000 - } - return section - - def expected(self, y_dim, x_dim): - # Prepare the expectation. - expected = empty_metadata() - ellipsoid = iris.coord_systems.GeogCS(6377563.396, 6356256.909) - cs = iris.coord_systems.TransverseMercator(49, -2, 400000, -100000, - 0.9996012717, ellipsoid) - nx = 4 - x_origin = 293000 - dx = 2000 - x = iris.coords.DimCoord(np.arange(nx) * dx + x_origin, - 'projection_x_coordinate', units='m', - coord_system=cs) - ny = 3 - y_origin = 92000 - dy = 1000 - y = iris.coords.DimCoord(np.arange(ny) * dy + y_origin, - 'projection_y_coordinate', units='m', - coord_system=cs) - expected['dim_coords_and_dims'].append((y, y_dim)) - expected['dim_coords_and_dims'].append((x, x_dim)) - return expected - - def test(self): - section = self.section_3() - metadata = empty_metadata() - grid_definition_template_12(section, metadata) - expected = self.expected(0, 1) - self.assertEqual(metadata, expected) - - def test_spherical(self): - section = self.section_3() - section['shapeOfTheEarth'] = 0 - metadata = empty_metadata() - grid_definition_template_12(section, metadata) - expected = self.expected(0, 1) - cs = expected['dim_coords_and_dims'][0][0].coord_system - cs.ellipsoid = iris.coord_systems.GeogCS(6367470) - self.assertEqual(metadata, expected) - - def test_negative_x(self): - section = self.section_3() - section['scanningMode'] = 0b11000000 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - '-x scanning'): - grid_definition_template_12(section, metadata) - - def test_negative_y(self): - section = self.section_3() - section['scanningMode'] = 0b00000000 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - '-y scanning'): - grid_definition_template_12(section, metadata) - - def test_transposed(self): - section = self.section_3() - section['scanningMode'] = 0b01100000 - metadata = empty_metadata() - grid_definition_template_12(section, metadata) - expected = self.expected(1, 0) - self.assertEqual(metadata, expected) - - def test_di_tolerance(self): - # Even though Ni * Di doesn't exactly match X1 to X2 it should - # be close enough to allow the translation. - section = self.section_3() - section['X2'] += 1 - metadata = empty_metadata() - grid_definition_template_12(section, metadata) - expected = self.expected(0, 1) - x = expected['dim_coords_and_dims'][1][0] - x.points = np.linspace(293000, 299000.01, 4) - self.assertEqual(metadata, expected) - - def test_incompatible_grid_extent(self): - section = self.section_3() - section['X2'] += 100 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - 'grid'): - grid_definition_template_12(section, metadata) - - def test_scale_workaround(self): - section = self.section_3() - section['scaleFactorAtReferencePoint'] = 1065346526 - metadata = empty_metadata() - grid_definition_template_12(section, metadata) - expected = self.expected(0, 1) - # A float32 can't hold exactly the same value. - cs = expected['dim_coords_and_dims'][0][0].coord_system - cs.scale_factor_at_central_meridian = 0.9996012449264526 - self.assertEqual(metadata, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 82c5195dfd..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_20.py +++ /dev/null @@ -1,106 +0,0 @@ -# (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._load_convert.grid_definition_template_20`. -""" - -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 cartopy.crs as ccrs -import numpy as np - -import iris.coord_systems -import iris.coords -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 - - -class Test(tests.IrisTest): - - def section_3(self): - section = { - 'shapeOfTheEarth': 0, - 'scaleFactorOfRadiusOfSphericalEarth': 0, - 'scaledValueOfRadiusOfSphericalEarth': 6367470, - 'scaleFactorOfEarthMajorAxis': 0, - 'scaledValueOfEarthMajorAxis': MDI, - 'scaleFactorOfEarthMinorAxis': 0, - 'scaledValueOfEarthMinorAxis': MDI, - 'Nx': 15, - 'Ny': 10, - 'latitudeOfFirstGridPoint': 32549114, - 'longitudeOfFirstGridPoint': 225385728, - 'resolutionAndComponentFlags': 0b00001000, - 'LaD': 60000000, - 'orientationOfTheGrid': 262000000, - 'Dx': 320000000, - 'Dy': 320000000, - 'projectionCentreFlag': 0b00000000, - 'scanningMode': 0b01000000, - } - return section - - def expected(self, y_dim, x_dim): - # Prepare the expectation. - expected = empty_metadata() - cs = iris.coord_systems.GeogCS(6367470) - cs = iris.coord_systems.Stereographic( - central_lat=90., - central_lon=262., - false_easting=0, - false_northing=0, - true_scale_lat=60., - ellipsoid=iris.coord_systems.GeogCS(6367470)) - lon0 = 225385728 * 1e-6 - lat0 = 32549114 * 1e-6 - x0m, y0m = cs.as_cartopy_crs().transform_point( - lon0, lat0, ccrs.Geodetic()) - dxm = dym = 320000. - x_points = x0m + dxm * np.arange(15) - y_points = y0m + dym * np.arange(10) - x = iris.coords.DimCoord(x_points, - standard_name='projection_x_coordinate', - units='m', - coord_system=cs, - circular=False) - y = iris.coords.DimCoord(y_points, - standard_name='projection_y_coordinate', - units='m', - coord_system=cs) - expected['dim_coords_and_dims'].append((y, y_dim)) - expected['dim_coords_and_dims'].append((x, x_dim)) - return expected - - def test(self): - section = self.section_3() - metadata = empty_metadata() - grid_definition_template_20(section, metadata) - expected = self.expected(0, 1) - self.assertEqual(metadata, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index a524eff9e5..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_30.py +++ /dev/null @@ -1,108 +0,0 @@ -# (C) British Crown Copyright 2015 - 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._load_convert.grid_definition_template_30`. -""" - -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 cartopy.crs as ccrs -import numpy as np - -import iris.coord_systems -import iris.coords -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 - - -class Test(tests.IrisTest): - - def section_3(self): - section = { - 'shapeOfTheEarth': 0, - 'scaleFactorOfRadiusOfSphericalEarth': 0, - 'scaledValueOfRadiusOfSphericalEarth': 6367470, - 'scaleFactorOfEarthMajorAxis': 0, - 'scaledValueOfEarthMajorAxis': MDI, - 'scaleFactorOfEarthMinorAxis': 0, - 'scaledValueOfEarthMinorAxis': MDI, - 'Nx': 15, - 'Ny': 10, - 'longitudeOfFirstGridPoint': 239550000, - 'latitudeOfFirstGridPoint': 21641000, - 'resolutionAndComponentFlags': 0b00001000, - 'LaD': 60000000, - 'LoV': 262000000, - 'Dx': 320000000, - 'Dy': 320000000, - 'projectionCentreFlag': 0b00000000, - 'scanningMode': 0b01000000, - 'Latin1': 60000000, - 'Latin2': 30000000, - } - return section - - def expected(self, y_dim, x_dim): - # Prepare the expectation. - expected = empty_metadata() - cs = iris.coord_systems.GeogCS(6367470) - cs = iris.coord_systems.LambertConformal( - central_lat=60., - central_lon=262., - false_easting=0, - false_northing=0, - secant_latitudes=(60., 30.), - ellipsoid=iris.coord_systems.GeogCS(6367470)) - lon0 = 239.55 - lat0 = 21.641 - x0m, y0m = cs.as_cartopy_crs().transform_point( - lon0, lat0, ccrs.Geodetic()) - dxm = dym = 320000. - x_points = x0m + dxm * np.arange(15) - y_points = y0m + dym * np.arange(10) - x = iris.coords.DimCoord(x_points, - standard_name='projection_x_coordinate', - units='m', - coord_system=cs, - circular=False) - y = iris.coords.DimCoord(y_points, - standard_name='projection_y_coordinate', - units='m', - coord_system=cs) - expected['dim_coords_and_dims'].append((y, y_dim)) - expected['dim_coords_and_dims'].append((x, x_dim)) - return expected - - def test(self): - section = self.section_3() - metadata = empty_metadata() - grid_definition_template_30(section, metadata) - expected = self.expected(0, 1) - self.assertEqual(metadata, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index eec5513d1d..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_40.py +++ /dev/null @@ -1,177 +0,0 @@ -# (C) British Crown Copyright 2015 - 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._load_convert.grid_definition_template_40`. - -""" - -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.coord_systems -import iris.coords -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 - - -class _Section(dict): - def get_computed_key(self, key): - return self.get(key) - - -class Test_regular(tests.IrisTest): - - def section_3(self): - section = _Section({ - 'shapeOfTheEarth': 0, - 'scaleFactorOfRadiusOfSphericalEarth': 0, - 'scaledValueOfRadiusOfSphericalEarth': 6367470, - 'scaleFactorOfEarthMajorAxis': 0, - 'scaledValueOfEarthMajorAxis': MDI, - 'scaleFactorOfEarthMinorAxis': 0, - 'scaledValueOfEarthMinorAxis': MDI, - 'iDirectionIncrement': 22500000, - 'longitudeOfFirstGridPoint': 0, - 'Ni': 16, - 'scanningMode': 0b01000000, - 'distinctLatitudes': np.array([-73.79921363, -52.81294319, - -31.70409175, -10.56988231, - 10.56988231, 31.70409175, - 52.81294319, 73.79921363]), - 'numberOfOctectsForNumberOfPoints': 0, - 'interpretationOfNumberOfPoints': 0, - }) - return section - - def expected(self, y_dim, x_dim, y_neg=True): - # Prepare the expectation. - expected = empty_metadata() - cs = iris.coord_systems.GeogCS(6367470) - nx = 16 - dx = 22.5 - x_origin = 0 - x = iris.coords.DimCoord(np.arange(nx) * dx + x_origin, - standard_name='longitude', - units='degrees_east', - coord_system=cs, - circular=True) - y_points = np.array([73.79921363, 52.81294319, - 31.70409175, 10.56988231, - -10.56988231, -31.70409175, - -52.81294319, -73.79921363]) - if not y_neg: - y_points = y_points[::-1] - y = iris.coords.DimCoord(y_points, - standard_name='latitude', - units='degrees_north', - coord_system=cs) - expected['dim_coords_and_dims'].append((y, y_dim)) - expected['dim_coords_and_dims'].append((x, x_dim)) - return expected - - def test(self): - section = self.section_3() - metadata = empty_metadata() - grid_definition_template_40(section, metadata) - expected = self.expected(0, 1, y_neg=False) - self.assertEqual(metadata, expected) - - def test_transposed(self): - section = self.section_3() - section['scanningMode'] = 0b01100000 - metadata = empty_metadata() - grid_definition_template_40(section, metadata) - expected = self.expected(1, 0, y_neg=False) - self.assertEqual(metadata, expected) - - def test_reverse_latitude(self): - section = self.section_3() - section['scanningMode'] = 0b00000000 - metadata = empty_metadata() - grid_definition_template_40(section, metadata) - expected = self.expected(0, 1, y_neg=True) - self.assertEqual(metadata, expected) - - -class Test_reduced(tests.IrisTest): - - def section_3(self): - section = _Section({ - 'shapeOfTheEarth': 0, - 'scaleFactorOfRadiusOfSphericalEarth': 0, - 'scaledValueOfRadiusOfSphericalEarth': 6367470, - 'scaleFactorOfEarthMajorAxis': 0, - 'scaledValueOfEarthMajorAxis': MDI, - 'scaleFactorOfEarthMinorAxis': 0, - 'scaledValueOfEarthMinorAxis': MDI, - 'longitudes': np.array([0., 180., - 0., 120., 240., - 0., 120., 240., - 0., 180.]), - 'latitudes': np.array([-59.44440829, -59.44440829, - -19.87571915, -19.87571915, -19.87571915, - 19.87571915, 19.87571915, 19.87571915, - 59.44440829, 59.44440829]), - 'numberOfOctectsForNumberOfPoints': 1, - 'interpretationOfNumberOfPoints': 1, - }) - return section - - def expected(self): - # Prepare the expectation. - expected = empty_metadata() - cs = iris.coord_systems.GeogCS(6367470) - x_points = np.array([0., 180., - 0., 120., 240., - 0., 120., 240., - 0., 180.]) - y_points = np.array([-59.44440829, -59.44440829, - -19.87571915, -19.87571915, -19.87571915, - 19.87571915, 19.87571915, 19.87571915, - 59.44440829, 59.44440829]) - x = iris.coords.AuxCoord(x_points, - standard_name='longitude', - units='degrees_east', - coord_system=cs) - y = iris.coords.AuxCoord(y_points, - standard_name='latitude', - units='degrees_north', - coord_system=cs) - expected['aux_coords_and_dims'].append((y, 0)) - expected['aux_coords_and_dims'].append((x, 0)) - return expected - - def test(self): - section = self.section_3() - metadata = empty_metadata() - expected = self.expected() - grid_definition_template_40(section, metadata) - self.assertEqual(metadata, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 534b41474e..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_4_and_5.py +++ /dev/null @@ -1,140 +0,0 @@ -# (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 . -""" -Test function -:func:`iris.fileformats.grib._load_convert.grid_definition_template_4_and_5`. - -""" - -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 -import warnings - -import numpy as np - -from iris.coords import DimCoord -from iris.fileformats.grib._load_convert import \ - _MDI as MDI, \ - grid_definition_template_4_and_5 -from iris.tests import mock - - -RESOLUTION = 1e6 - - -class Test(tests.IrisTest): - def setUp(self): - self.patch('warnings.warn') - module = 'iris.fileformats.grib._load_convert' - self.patch('{}._is_circular'.format(module), return_value=False) - self.metadata = {'factories': [], 'references': [], - 'standard_name': None, - 'long_name': None, 'units': None, 'attributes': {}, - 'cell_methods': [], 'dim_coords_and_dims': [], - 'aux_coords_and_dims': []} - self.cs = mock.sentinel.coord_system - self.data = np.arange(10, dtype=np.float64) - - def _check(self, section, request_warning, - expect_warning=False, y_dim=0, x_dim=1): - this = 'iris.fileformats.grib._load_convert.options' - with mock.patch(this, warn_on_unsupported=request_warning): - metadata = deepcopy(self.metadata) - # The called being tested. - grid_definition_template_4_and_5(section, metadata, - 'latitude', 'longitude', self.cs) - expected = deepcopy(self.metadata) - coord = DimCoord(self.data, - standard_name='latitude', - units='degrees', - coord_system=self.cs) - expected['dim_coords_and_dims'].append((coord, y_dim)) - coord = DimCoord(self.data, - standard_name='longitude', - units='degrees', - coord_system=self.cs) - expected['dim_coords_and_dims'].append((coord, x_dim)) - self.assertEqual(metadata, expected) - if expect_warning: - self.assertEqual(len(warnings.warn.mock_calls), 1) - args, kwargs = warnings.warn.call_args - self.assertIn('resolution and component flags', args[0]) - else: - self.assertEqual(len(warnings.warn.mock_calls), 0) - - def test_resolution_default_0(self): - for request_warn in [False, True]: - section = {'basicAngleOfTheInitialProductionDomain': 0, - 'subdivisionsOfBasicAngle': 0, - 'resolutionAndComponentFlags': 0, - 'longitudes': self.data * RESOLUTION, - 'latitudes': self.data * RESOLUTION, - 'scanningMode': 0} - self._check(section, request_warn) - - def test_resolution_default_mdi(self): - for request_warn in [False, True]: - section = {'basicAngleOfTheInitialProductionDomain': MDI, - 'subdivisionsOfBasicAngle': MDI, - 'resolutionAndComponentFlags': 0, - 'longitudes': self.data * RESOLUTION, - 'latitudes': self.data * RESOLUTION, - 'scanningMode': 0} - self._check(section, request_warn) - - def test_resolution(self): - angle = 10 - for request_warn in [False, True]: - section = {'basicAngleOfTheInitialProductionDomain': 1, - 'subdivisionsOfBasicAngle': angle, - 'resolutionAndComponentFlags': 0, - 'longitudes': self.data * angle, - 'latitudes': self.data * angle, - 'scanningMode': 0} - self._check(section, request_warn) - - def test_uv_resolved_warn(self): - angle = 100 - for warn in [False, True]: - section = {'basicAngleOfTheInitialProductionDomain': 1, - 'subdivisionsOfBasicAngle': angle, - 'resolutionAndComponentFlags': 0x08, - 'longitudes': self.data * angle, - 'latitudes': self.data * angle, - 'scanningMode': 0} - self._check(section, warn, expect_warning=warn) - - def test_j_consecutive(self): - angle = 1000 - for request_warn in [False, True]: - section = {'basicAngleOfTheInitialProductionDomain': 1, - 'subdivisionsOfBasicAngle': angle, - 'resolutionAndComponentFlags': 0, - 'longitudes': self.data * angle, - 'latitudes': self.data * angle, - 'scanningMode': 0x20} - self._check(section, request_warn, y_dim=1, x_dim=0) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 0cc8f19a7a..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_5.py +++ /dev/null @@ -1,99 +0,0 @@ -# (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 . -""" -Test function -:func:`iris.fileformats.grib._load_convert.grid_definition_template_5`. - -""" - -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.fileformats.grib._load_convert import grid_definition_template_5 -from iris.tests import mock - - -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' - - self.major = mock.sentinel.major - self.minor = mock.sentinel.minor - self.radius = mock.sentinel.radius - - mfunc = '{}.ellipsoid_geometry'.format(module) - return_value = (self.major, self.minor, self.radius) - self.patch(mfunc, return_value=return_value) - - mfunc = '{}.ellipsoid'.format(module) - self.ellipsoid = mock.sentinel.ellipsoid - 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) - - mclass = 'iris.coord_systems.RotatedGeogCS' - self.cs = mock.sentinel.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': []} - - def test(self): - metadata = deepcopy(self.metadata) - angleOfRotation = mock.sentinel.angleOfRotation - shapeOfTheEarth = mock.sentinel.shapeOfTheEarth - section = {'latitudeOfSouthernPole': 45000000, - 'longitudeOfSouthernPole': 90000000, - 'angleOfRotation': angleOfRotation, - 'shapeOfTheEarth': shapeOfTheEarth} - # The called being tested. - grid_definition_template_5(section, metadata) - from iris.fileformats.grib._load_convert import \ - ellipsoid_geometry, \ - ellipsoid, \ - grid_definition_template_4_and_5 as gdt_4_5 - self.assertEqual(ellipsoid_geometry.call_count, 1) - ellipsoid.assert_called_once_with(shapeOfTheEarth, self.major, - self.minor, self.radius) - from iris.coord_systems import RotatedGeogCS - RotatedGeogCS.assert_called_once_with(-45.0, 270.0, angleOfRotation, - self.ellipsoid) - gdt_4_5.assert_called_once_with(section, metadata, 'grid_latitude', - 'grid_longitude', self.cs) - expected = deepcopy(self.metadata) - expected['dim_coords_and_dims'].append((self.coord, self.dim)) - self.assertEqual(metadata, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 9597852575..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_90.py +++ /dev/null @@ -1,199 +0,0 @@ -# (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 -:func:`iris.fileformats.grib._load_convert.grid_definition_template_90`. - -""" - -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 numpy as np - -import iris.coord_systems -import iris.coords -import iris.exceptions -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 - - -class Test(tests.IrisTest): - def uk(self): - section = { - 'shapeOfTheEarth': 3, - 'scaleFactorOfRadiusOfSphericalEarth': MDI, - 'scaledValueOfRadiusOfSphericalEarth': MDI, - 'scaleFactorOfEarthMajorAxis': 4, - 'scaledValueOfEarthMajorAxis': 63781688, - 'scaleFactorOfEarthMinorAxis': 4, - 'scaledValueOfEarthMinorAxis': 63565840, - 'Nx': 390, - 'Ny': 227, - 'latitudeOfSubSatellitePoint': 0, - 'longitudeOfSubSatellitePoint': 0, - 'resolutionAndComponentFlags': 0, - 'dx': 3622, - 'dy': 3610, - 'Xp': 1856000, - 'Yp': 1856000, - 'scanningMode': 192, - 'orientationOfTheGrid': 0, - 'Nr': 6610674, - 'Xo': 1733, - 'Yo': 3320 - } - return section - - def expected_uk(self, y_dim, x_dim): - # Prepare the expectation. - expected = empty_metadata() - major = 6378168.8 - ellipsoid = iris.coord_systems.GeogCS(major, 6356584.0) - height = (6610674e-6 - 1) * major - lat = lon = 0 - easting = northing = 0 - cs = iris.coord_systems.VerticalPerspective(lat, lon, height, easting, - northing, ellipsoid) - nx = 390 - x_origin = 369081.56145444815 - dx = -3000.663101255676 - x = iris.coords.DimCoord(np.arange(nx) * dx + x_origin, - 'projection_x_coordinate', units='m', - coord_system=cs) - ny = 227 - y_origin = 4392884.59201891 - dy = 3000.604229521113 - y = iris.coords.DimCoord(np.arange(ny) * dy + y_origin, - 'projection_y_coordinate', units='m', - coord_system=cs) - expected['dim_coords_and_dims'].append((y, y_dim)) - expected['dim_coords_and_dims'].append((x, x_dim)) - return expected - - def compare(self, metadata, expected): - # Compare the result with the expectation. - self.assertEqual(len(metadata['dim_coords_and_dims']), - len(expected['dim_coords_and_dims'])) - for result_pair, expected_pair in zip(metadata['dim_coords_and_dims'], - expected['dim_coords_and_dims']): - result_coord, result_dims = result_pair - expected_coord, expected_dims = expected_pair - # Ensure the dims match. - self.assertEqual(result_dims, expected_dims) - # Ensure the coordinate systems match (allowing for precision). - result_cs = result_coord.coord_system - expected_cs = expected_coord.coord_system - self.assertEqual(type(result_cs), type(expected_cs)) - self.assertEqual(result_cs.latitude_of_projection_origin, - expected_cs.latitude_of_projection_origin) - self.assertEqual(result_cs.longitude_of_projection_origin, - expected_cs.longitude_of_projection_origin) - self.assertAlmostEqual(result_cs.perspective_point_height, - expected_cs.perspective_point_height) - self.assertEqual(result_cs.false_easting, - expected_cs.false_easting) - self.assertEqual(result_cs.false_northing, - expected_cs.false_northing) - self.assertAlmostEqual(result_cs.ellipsoid.semi_major_axis, - expected_cs.ellipsoid.semi_major_axis) - self.assertEqual(result_cs.ellipsoid.semi_minor_axis, - expected_cs.ellipsoid.semi_minor_axis) - # Now we can ignore the coordinate systems and compare the - # rest of the coordinate attributes. - result_coord.coord_system = None - expected_coord.coord_system = None - self.assertEqual(result_coord, expected_coord) - - # Ensure no other metadata was created. - for name in six.iterkeys(expected): - if name == 'dim_coords_and_dims': - continue - self.assertEqual(metadata[name], expected[name]) - - def test_uk(self): - section = self.uk() - metadata = empty_metadata() - grid_definition_template_90(section, metadata) - expected = self.expected_uk(0, 1) - self.compare(metadata, expected) - - def test_uk_transposed(self): - section = self.uk() - section['scanningMode'] = 0b11100000 - metadata = empty_metadata() - grid_definition_template_90(section, metadata) - expected = self.expected_uk(1, 0) - self.compare(metadata, expected) - - def test_non_zero_latitude(self): - section = self.uk() - section['latitudeOfSubSatellitePoint'] = 1 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - 'non-zero latitude'): - grid_definition_template_90(section, metadata) - - def test_rotated_meridian(self): - section = self.uk() - section['orientationOfTheGrid'] = 1 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - 'orientation'): - grid_definition_template_90(section, metadata) - - def test_zero_height(self): - section = self.uk() - section['Nr'] = 0 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - 'zero'): - grid_definition_template_90(section, metadata) - - def test_orthographic(self): - section = self.uk() - section['Nr'] = MDI - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, - 'orthographic'): - grid_definition_template_90(section, metadata) - - def test_scanning_mode_positive_x(self): - section = self.uk() - section['scanningMode'] = 0b01000000 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, r'\+x'): - grid_definition_template_90(section, metadata) - - def test_scanning_mode_negative_y(self): - section = self.uk() - section['scanningMode'] = 0b10000000 - metadata = empty_metadata() - with self.assertRaisesRegexp(iris.exceptions.TranslationError, '-y'): - grid_definition_template_90(section, metadata) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 4b47d99c4f..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_other_time_coord.py +++ /dev/null @@ -1,120 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.other_time_coord. - -""" - -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.coords -from iris.fileformats.grib._load_convert import other_time_coord - - -class TestValid(tests.IrisTest): - def test_t(self): - rt = iris.coords.DimCoord(48, 'time', units='hours since epoch') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - result = other_time_coord(rt, fp) - expected = iris.coords.DimCoord(42, 'forecast_reference_time', - units='hours since epoch') - self.assertEqual(result, expected) - - def test_frt(self): - rt = iris.coords.DimCoord(48, 'forecast_reference_time', - units='hours since epoch') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - result = other_time_coord(rt, fp) - expected = iris.coords.DimCoord(54, 'time', units='hours since epoch') - self.assertEqual(result, expected) - - -class TestInvalid(tests.IrisTest): - def test_t_with_bounds(self): - rt = iris.coords.DimCoord(48, 'time', units='hours since epoch', - bounds=[36, 60]) - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'bounds'): - other_time_coord(rt, fp) - - def test_frt_with_bounds(self): - rt = iris.coords.DimCoord(48, 'forecast_reference_time', - units='hours since epoch', - bounds=[42, 54]) - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'bounds'): - other_time_coord(rt, fp) - - def test_fp_with_bounds(self): - rt = iris.coords.DimCoord(48, 'time', units='hours since epoch') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours', - bounds=[3, 9]) - with self.assertRaisesRegexp(ValueError, 'bounds'): - other_time_coord(rt, fp) - - def test_vector_t(self): - rt = iris.coords.DimCoord([0, 3], 'time', units='hours since epoch') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'Vector'): - other_time_coord(rt, fp) - - def test_vector_frt(self): - rt = iris.coords.DimCoord([0, 3], 'forecast_reference_time', - units='hours since epoch') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'Vector'): - other_time_coord(rt, fp) - - def test_vector_fp(self): - rt = iris.coords.DimCoord(48, 'time', units='hours since epoch') - fp = iris.coords.DimCoord([6, 12], 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'Vector'): - other_time_coord(rt, fp) - - def test_invalid_rt_name(self): - rt = iris.coords.DimCoord(1, 'height') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'reference time'): - other_time_coord(rt, fp) - - def test_invalid_t_unit(self): - rt = iris.coords.DimCoord(1, 'time', units='Pa') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'unit.*Pa'): - other_time_coord(rt, fp) - - def test_invalid_frt_unit(self): - rt = iris.coords.DimCoord(1, 'forecast_reference_time', units='km') - fp = iris.coords.DimCoord(6, 'forecast_period', units='hours') - with self.assertRaisesRegexp(ValueError, 'unit.*km'): - other_time_coord(rt, fp) - - def test_invalid_fp_unit(self): - rt = iris.coords.DimCoord(48, 'forecast_reference_time', - units='hours since epoch') - fp = iris.coords.DimCoord(6, 'forecast_period', units='kg') - with self.assertRaisesRegexp(ValueError, 'unit.*kg'): - other_time_coord(rt, fp) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 649aa3c2b6..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_0.py +++ /dev/null @@ -1,108 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function -:func:`iris.fileformats.grib._load_convert.product_definition_template_0`. - -""" - -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.coords -from iris.tests.unit.fileformats.grib.load_convert import (LoadConvertTest, - empty_metadata) -from iris.fileformats.grib._load_convert import product_definition_template_0 -from iris.tests import mock - - -MDI = 0xffffffff - - -def section_4(): - return {'hoursAfterDataCutoff': MDI, - 'minutesAfterDataCutoff': MDI, - 'indicatorOfUnitOfTimeRange': 0, # minutes - 'forecastTime': 360, - 'NV': 0, - 'typeOfFirstFixedSurface': 103, - 'scaleFactorOfFirstFixedSurface': 0, - 'scaledValueOfFirstFixedSurface': 9999, - 'typeOfSecondFixedSurface': 255} - - -class Test(LoadConvertTest): - def test_given_frt(self): - metadata = empty_metadata() - rt_coord = iris.coords.DimCoord(24, 'forecast_reference_time', - units='hours since epoch') - product_definition_template_0(section_4(), metadata, rt_coord) - expected = empty_metadata() - aux = expected['aux_coords_and_dims'] - aux.append((iris.coords.DimCoord(6, 'forecast_period', units='hours'), - None)) - aux.append(( - iris.coords.DimCoord(30, 'time', units='hours since epoch'), None)) - aux.append((rt_coord, None)) - aux.append((iris.coords.DimCoord(9999, long_name='height', units='m'), - None)) - self.assertMetadataEqual(metadata, expected) - - def test_given_t(self): - metadata = empty_metadata() - rt_coord = iris.coords.DimCoord(24, 'time', - units='hours since epoch') - product_definition_template_0(section_4(), metadata, rt_coord) - expected = empty_metadata() - aux = expected['aux_coords_and_dims'] - aux.append((iris.coords.DimCoord(6, 'forecast_period', units='hours'), - None)) - aux.append(( - iris.coords.DimCoord(18, 'forecast_reference_time', - units='hours since epoch'), None)) - aux.append((rt_coord, None)) - aux.append((iris.coords.DimCoord(9999, long_name='height', units='m'), - None)) - self.assertMetadataEqual(metadata, expected) - - def test_generating_process_warnings(self): - metadata = empty_metadata() - rt_coord = iris.coords.DimCoord(24, 'forecast_reference_time', - units='hours since epoch') - convert_options = iris.fileformats.grib._load_convert.options - emit_warnings = convert_options.warn_on_unsupported - try: - convert_options.warn_on_unsupported = True - with mock.patch('warnings.warn') as warn: - product_definition_template_0(section_4(), metadata, rt_coord) - warn_msgs = [call[1][0] for call in warn.mock_calls] - expected = ['Unable to translate type of generating process.', - 'Unable to translate background generating process ' - 'identifier.', - 'Unable to translate forecast generating process ' - 'identifier.'] - self.assertEqual(warn_msgs, expected) - finally: - convert_options.warn_on_unsupported = emit_warnings - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 012107f146..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_1.py +++ /dev/null @@ -1,89 +0,0 @@ -# (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 . -""" -Test function -:func:`iris.fileformats.grib._load_convert.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 copy import deepcopy -import warnings - -from iris.coords import DimCoord -from iris.fileformats.grib._load_convert import product_definition_template_1 -from iris.tests import mock - - -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 - self.patch(this, side_effect=func) - self.metadata = {'factories': [], 'references': [], - 'standard_name': None, - 'long_name': None, 'units': None, 'attributes': {}, - 'cell_methods': [], 'dim_coords_and_dims': [], - 'aux_coords_and_dims': []} - - def _check(self, request_warning): - this = 'iris.fileformats.grib._load_convert.options' - with mock.patch(this, warn_on_unsupported=request_warning): - metadata = deepcopy(self.metadata) - perturbationNumber = 666 - section = {'perturbationNumber': perturbationNumber} - forecast_reference_time = mock.sentinel.forecast_reference_time - # The called being tested. - product_definition_template_1(section, metadata, - forecast_reference_time) - expected = deepcopy(self.metadata) - expected['cell_methods'].append(self.cell_method) - realization = DimCoord(perturbationNumber, - standard_name='realization', - units='no_unit') - expected['aux_coords_and_dims'].append((realization, None)) - self.assertEqual(metadata, expected) - if request_warning: - warn_msgs = [mcall[1][0] for mcall in warnings.warn.mock_calls] - expected_msgs = ['type of ensemble', 'number of forecasts'] - for emsg in expected_msgs: - matches = [wmsg for wmsg in warn_msgs if emsg in wmsg] - self.assertEqual(len(matches), 1) - warn_msgs.remove(matches[0]) - else: - self.assertEqual(len(warnings.warn.mock_calls), 0) - - def test_pdt_no_warn(self): - self._check(False) - - def test_pdt_warn(self): - self._check(True) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index eefb9d333f..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_10.py +++ /dev/null @@ -1,81 +0,0 @@ -# (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 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 = {'productDefinitionTemplateNumber': 10, - '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 deleted file mode 100644 index 75c066ee81..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_11.py +++ /dev/null @@ -1,115 +0,0 @@ -# (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_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 -# before importing anything else. -import iris.tests as tests - -from copy import deepcopy -import warnings - -from iris.coords import DimCoord, CellMethod -from iris.fileformats.grib._load_convert import product_definition_template_11 -from iris.tests import mock - - -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 - 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) - 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') - self.metadata = {'factories': [], 'references': [], - 'standard_name': None, - 'long_name': None, 'units': None, 'attributes': {}, - 'cell_methods': [], 'dim_coords_and_dims': [], - 'aux_coords_and_dims': []} - - def _check(self, request_warning): - grib_lconv_opt = 'iris.fileformats.grib._load_convert.options' - with mock.patch(grib_lconv_opt, warn_on_unsupported=request_warning): - metadata = deepcopy(self.metadata) - perturbationNumber = 666 - section = {'productDefinitionTemplateNumber': 11, - 'perturbationNumber': perturbationNumber, - '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_11(section, metadata, - forecast_reference_time) - expected = {'cell_methods': [], 'aux_coords_and_dims': []} - expected['cell_methods'].append(CellMethod(method='sum', - coords=('time',))) - realization = DimCoord(perturbationNumber, - standard_name='realization', - units='no_unit') - expected['aux_coords_and_dims'].append((realization, None)) - self.maxDiff = None - self.assertEqual(metadata['aux_coords_and_dims'][-1], - expected['aux_coords_and_dims'][0]) - self.assertEqual(metadata['cell_methods'][-1], - expected['cell_methods'][0]) - - if request_warning: - warn_msgs = [mcall[1][0] for mcall in warnings.warn.mock_calls] - expected_msgs = ['type of ensemble', 'number of forecasts'] - for emsg in expected_msgs: - matches = [wmsg for wmsg in warn_msgs if emsg in wmsg] - self.assertEqual(len(matches), 1) - warn_msgs.remove(matches[0]) - else: - self.assertEqual(len(warnings.warn.mock_calls), 0) - - def test_pdt_no_warn(self): - self._check(False) - - def test_pdt_warn(self): - self._check(True) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_15.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_15.py deleted file mode 100644 index 1158c86e7f..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_15.py +++ /dev/null @@ -1,102 +0,0 @@ -# (C) British Crown Copyright 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_15`. - -This basically copies code from 'test_product_definition_template_0', and adds -testing for the statistical method and spatial-processing type. - -""" - -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 iris.coords import CellMethod, DimCoord -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import product_definition_template_15 -from iris.tests.unit.fileformats.grib.load_convert import (LoadConvertTest, - empty_metadata) - - -MDI = 0xffffffff - - -def section_4(): - return {'productDefinitionTemplateNumber': 15, - 'hoursAfterDataCutoff': MDI, - 'minutesAfterDataCutoff': MDI, - 'indicatorOfUnitOfTimeRange': 0, # minutes - 'forecastTime': 360, - 'NV': 0, - 'typeOfFirstFixedSurface': 103, - 'scaleFactorOfFirstFixedSurface': 0, - 'scaledValueOfFirstFixedSurface': 9999, - 'typeOfSecondFixedSurface': 255, - 'statisticalProcess': 2, # method = maximum - 'spatialProcessing': 0, # from source grid, no interpolation - 'numberOfPointsUsed': 0 # unused? - } - - -class Test(LoadConvertTest): - def setUp(self): - self.ref_time_coord = DimCoord(24, 'time', units='hours since epoch') - - def _check_translate(self, section): - metadata = empty_metadata() - product_definition_template_15(section, metadata, - self.ref_time_coord) - return metadata - - def test_t(self): - metadata = self._check_translate(section_4()) - - expected = empty_metadata() - aux = expected['aux_coords_and_dims'] - aux.append((DimCoord(6, 'forecast_period', units='hours'), None)) - aux.append((DimCoord(18, 'forecast_reference_time', - units='hours since epoch'), None)) - aux.append((self.ref_time_coord, None)) - aux.append((DimCoord(9999, long_name='height', units='m'), - None)) - expected['cell_methods'] = [CellMethod(coords=('area',), - method='maximum')] - - self.assertMetadataEqual(metadata, expected) - - def test_bad_statistic_method(self): - section = section_4() - section['statisticalProcess'] = 999 - msg = ('unsupported statistical process type \[999\]') - with self.assertRaisesRegexp(TranslationError, msg): - self._check_translate(section) - - def test_bad_spatial_processing_code(self): - section = section_4() - section['spatialProcessing'] = 999 - msg = ('unsupported spatial processing type \[999\]') - with self.assertRaisesRegexp(TranslationError, msg): - self._check_translate(section) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 55e21e493a..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_31.py +++ /dev/null @@ -1,69 +0,0 @@ -# (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 . -""" -Tests for -:func:`iris.fileformats.grib._load_convert.product_definition_template_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 - -from iris.fileformats.grib._load_convert import product_definition_template_31 -from iris.tests import mock -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata - - -class Test(tests.IrisTest): - def setUp(self): - self.patch('warnings.warn') - self.satellite_common_patch = self.patch( - 'iris.fileformats.grib._load_convert.satellite_common') - self.generating_process_patch = self.patch( - 'iris.fileformats.grib._load_convert.generating_process') - - def test(self): - # Prepare the arguments. - series = mock.sentinel.satelliteSeries - number = mock.sentinel.satelliteNumber - instrument = mock.sentinel.instrumentType - rt_coord = mock.sentinel.observation_time - section = {'NB': 1, - 'satelliteSeries': series, - 'satelliteNumber': number, - 'instrumentType': instrument, - 'scaleFactorOfCentralWaveNumber': 1, - 'scaledValueOfCentralWaveNumber': 12} - - # Call the function. - metadata = empty_metadata() - product_definition_template_31(section, metadata, rt_coord) - - # Check that 'satellite_common' was called. - self.assertEqual(self.satellite_common_patch.call_count, 1) - # Check that 'generating_process' was called. - self.assertEqual(self.generating_process_patch.call_count, 1) - # Check that the scalar time coord was added in. - self.assertIn((rt_coord, None), metadata['aux_coords_and_dims']) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_32.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_32.py deleted file mode 100644 index 89f1c2673a..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_32.py +++ /dev/null @@ -1,80 +0,0 @@ -# (C) British Crown Copyright 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 . -""" -Tests for `iris.fileformats.grib._load_convert.product_definition_template_32`. - -""" - -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 iris.fileformats.grib._load_convert import product_definition_template_32 -from iris.tests import mock -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata - - -MDI = 0xffffffff - - -class Test(tests.IrisTest): - def setUp(self): - self.patch('warnings.warn') - self.generating_process_patch = self.patch( - 'iris.fileformats.grib._load_convert.generating_process') - self.satellite_common_patch = self.patch( - 'iris.fileformats.grib._load_convert.satellite_common') - self.time_coords_patch = self.patch( - 'iris.fileformats.grib._load_convert.time_coords') - self.data_cutoff_patch = self.patch( - 'iris.fileformats.grib._load_convert.data_cutoff') - - def test(self, value=10, factor=1): - # Prepare the arguments. - series = mock.sentinel.satelliteSeries - number = mock.sentinel.satelliteNumber - instrument = mock.sentinel.instrumentType - rt_coord = mock.sentinel.observation_time - section = {'NB': 1, - 'hoursAfterDataCutoff': None, - 'minutesAfterDataCutoff': None, - 'satelliteSeries': series, - 'satelliteNumber': number, - 'instrumentType': instrument, - 'scaleFactorOfCentralWaveNumber': 1, - 'scaledValueOfCentralWaveNumber': 12, - } - - # Call the function. - metadata = empty_metadata() - product_definition_template_32(section, metadata, rt_coord) - - # Check that 'satellite_common' was called. - self.assertEqual(self.satellite_common_patch.call_count, 1) - # Check that 'generating_process' was called. - self.assertEqual(self.generating_process_patch.call_count, 1) - # Check that 'data_cutoff' was called. - self.assertEqual(self.data_cutoff_patch.call_count, 1) - # Check that 'time_coords' was called. - self.assertEqual(self.time_coords_patch.call_count, 1) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 7ccecdd76b..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_40.py +++ /dev/null @@ -1,60 +0,0 @@ -# (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_40`. - -""" - -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.coords -from iris.fileformats.grib._load_convert import \ - _MDI, product_definition_template_40 -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata - - -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(self.section_4, metadata, rt_coord) - expected = empty_metadata() - expected['attributes']['WMO_constituent_type'] = 1 - self.assertEqual(metadata['attributes'], expected['attributes']) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index eeb203aed9..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_8.py +++ /dev/null @@ -1,82 +0,0 @@ -# (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 . -""" -Tests for function -:func:`iris.fileformats.grib._load_convert.product_definition_template_8`. - -""" - -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 - -from iris.fileformats.grib._load_convert import product_definition_template_8 -from iris.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - module = 'iris.fileformats.grib._load_convert' - self.module = module - # Create patches for called routines - self.patch_generating_process = self.patch( - module + '.generating_process') - self.patch_data_cutoff = self.patch(module + '.data_cutoff') - self.patch_statistical_cell_method = self.patch( - module + '.statistical_cell_method', - return_value=mock.sentinel.dummy_cell_method) - 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') - # Construct dummy call arguments - self.section = {} - self.section['hoursAfterDataCutoff'] = mock.sentinel.cutoff_hours - self.section['minutesAfterDataCutoff'] = mock.sentinel.cutoff_mins - self.frt_coord = mock.Mock() - self.metadata = {'cell_methods': [], 'aux_coords_and_dims': []} - - def test_basic(self): - product_definition_template_8( - self.section, self.metadata, self.frt_coord) - # Check all expected functions were called just once. - self.assertEqual(self.patch_generating_process.call_count, 1) - self.assertEqual(self.patch_data_cutoff.call_count, 1) - self.assertEqual(self.patch_statistical_cell_method.call_count, 1) - self.assertEqual(self.patch_statistical_fp_coord.call_count, 1) - self.assertEqual(self.patch_time_coord.call_count, 1) - self.assertEqual(self.patch_vertical_coords.call_count, 1) - # Check metadata content. - self.assertEqual(sorted(self.metadata.keys()), - ['aux_coords_and_dims', 'cell_methods']) - self.assertEqual(self.metadata['cell_methods'], - [mock.sentinel.dummy_cell_method]) - six.assertCountEqual(self, self.metadata['aux_coords_and_dims'], - [(self.frt_coord, None), - (mock.sentinel.dummy_fp_coord, None), - (mock.sentinel.dummy_time_coord, None)]) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index e1aede2dbf..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_product_definition_template_9.py +++ /dev/null @@ -1,87 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Tests for function -:func:`iris.fileformats.grib._load_convert.product_definition_template_9`. - -""" - -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 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.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - # Create patches for called routines - module = 'iris.fileformats.grib._load_convert' - self.patch_pdt8_call = self.patch( - module + '.product_definition_template_8') - # Construct dummy call arguments - self.section = {} - self.section['probabilityType'] = 1 - self.section['scaledValueOfUpperLimit'] = 53 - self.section['scaleFactorOfUpperLimit'] = 1 - self.frt_coord = mock.sentinel.frt_coord - self.metadata = {'cell_methods': [mock.sentinel.cell_method], - 'aux_coords_and_dims': []} - - def test_basic(self): - result = product_definition_template_9( - self.section, self.metadata, self.frt_coord) - # Check expected function was called. - self.assertEqual( - self.patch_pdt8_call.call_args_list, - [mock.call(self.section, self.metadata, self.frt_coord)]) - # Check metadata content (N.B. cell_method has been removed!). - self.assertEqual(self.metadata, {'cell_methods': [], - 'aux_coords_and_dims': []}) - # Check result. - self.assertEqual(result, Probability('above_threshold', 5.3)) - - def test_fail_bad_probability_type(self): - self.section['probabilityType'] = 17 - with self.assertRaisesRegexp(TranslationError, - 'unsupported probability type'): - product_definition_template_9( - self.section, self.metadata, self.frt_coord) - - def test_fail_bad_threshold_value(self): - 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 - with self.assertRaisesRegexp(TranslationError, - 'missing scale factor'): - product_definition_template_9( - self.section, self.metadata, self.frt_coord) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index da5050270c..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_projection_centre.py +++ /dev/null @@ -1,52 +0,0 @@ -# (C) British Crown Copyright 2015 - 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.projection centre. - -""" - -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 iris.fileformats.grib._load_convert import (ProjectionCentre, - projection_centre) - - -class Test(tests.IrisTest): - def test_unset(self): - expected = ProjectionCentre(False, False) - self.assertEqual(projection_centre(0x0), expected) - - def test_bipolar_and_symmetric(self): - expected = ProjectionCentre(False, True) - self.assertEqual(projection_centre(0x40), expected) - - def test_south_pole_on_projection_plane(self): - expected = ProjectionCentre(True, False) - self.assertEqual(projection_centre(0x80), expected) - - def test_both(self): - expected = ProjectionCentre(True, True) - self.assertEqual(projection_centre(0xc0), expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 1c9a5fd97c..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_reference_time_coord.py +++ /dev/null @@ -1,86 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.reference_time_coord. - -Reference Code Table 1.2. - -""" - -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 datetime import datetime - -from cf_units import CALENDAR_GREGORIAN, Unit - -from iris.coords import DimCoord -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import reference_time_coord - - -class Test(tests.IrisTest): - def setUp(self): - self.section = {'year': 2007, - 'month': 1, - 'day': 15, - 'hour': 0, - 'minute': 3, - 'second': 0} - self.unit = Unit('hours since epoch', calendar=CALENDAR_GREGORIAN) - dt = datetime(self.section['year'], self.section['month'], - self.section['day'], self.section['hour'], - self.section['minute'], self.section['second']) - self.point = self.unit.date2num(dt) - - def _check(self, section, standard_name=None): - expected = DimCoord(self.point, standard_name=standard_name, - units=self.unit) - # The call being tested. - coord = reference_time_coord(section) - self.assertEqual(coord, expected) - - def test_start_of_forecast_0(self): - section = deepcopy(self.section) - section['significanceOfReferenceTime'] = 0 - self._check(section, 'forecast_reference_time') - - def test_start_of_forecast_1(self): - section = deepcopy(self.section) - section['significanceOfReferenceTime'] = 1 - self._check(section, 'forecast_reference_time') - - def test_observation_time(self): - section = deepcopy(self.section) - section['significanceOfReferenceTime'] = 3 - self._check(section, 'time') - - def test_unknown_significance(self): - section = deepcopy(self.section) - section['significanceOfReferenceTime'] = 5 - emsg = 'unsupported significance' - with self.assertRaisesRegexp(TranslationError, emsg): - self._check(section) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index cd6b1a06c1..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_resolution_flags.py +++ /dev/null @@ -1,52 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.resolution_flags.` - -""" - -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 iris.fileformats.grib._load_convert import \ - ResolutionFlags, resolution_flags - - -class Test(tests.IrisTest): - def test_unset(self): - expected = ResolutionFlags(False, False, False) - self.assertEqual(resolution_flags(0x0), expected) - - def test_i_increments_given(self): - expected = ResolutionFlags(True, False, False) - self.assertEqual(resolution_flags(0x20), expected) - - def test_j_increments_given(self): - expected = ResolutionFlags(False, True, False) - self.assertEqual(resolution_flags(0x10), expected) - - def test_uv_resolved(self): - expected = ResolutionFlags(False, False, True) - self.assertEqual(resolution_flags(0x08), expected) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/load_convert/test_satellite_common.py b/lib/iris/tests/unit/fileformats/grib/load_convert/test_satellite_common.py deleted file mode 100644 index 1f3c698068..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_satellite_common.py +++ /dev/null @@ -1,81 +0,0 @@ -# (C) British Crown Copyright 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 . -""" -Tests for `iris.fileformats.grib._load_convert.satellite_common`. - -""" - -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 - -from iris.coords import AuxCoord -from iris.fileformats.grib._load_convert import satellite_common -from iris.tests import mock -from iris.tests.unit.fileformats.grib.load_convert import empty_metadata - - -class Test(tests.IrisTest): - def _check(self, factors=1, values=111): - # Prepare the arguments. - series = mock.sentinel.satelliteSeries - number = mock.sentinel.satelliteNumber - instrument = mock.sentinel.instrumentType - section = {'NB': 1, - 'satelliteSeries': series, - 'satelliteNumber': number, - 'instrumentType': instrument, - 'scaleFactorOfCentralWaveNumber': factors, - 'scaledValueOfCentralWaveNumber': values} - - # Call the function. - metadata = empty_metadata() - satellite_common(section, metadata) - - # Check the result. - expected = empty_metadata() - coord = AuxCoord(series, long_name='satellite_series') - expected['aux_coords_and_dims'].append((coord, None)) - coord = AuxCoord(number, long_name='satellite_number') - expected['aux_coords_and_dims'].append((coord, None)) - coord = AuxCoord(instrument, long_name='instrument_type') - expected['aux_coords_and_dims'].append((coord, None)) - standard_name = 'sensor_band_central_radiation_wavenumber' - coord = AuxCoord(values / (10.0 ** factors), - standard_name=standard_name, - units='m-1') - expected['aux_coords_and_dims'].append((coord, None)) - self.assertEqual(metadata, expected) - - def test_basic(self): - self._check() - - def test_multiple_wavelengths(self): - # Check with multiple values, and several different scaling factors. - values = np.array([1, 11, 123, 1975]) - for i_factor in (-3, -1, 0, 1, 3): - factors = np.ones(values.shape) * i_factor - self._check(values=values, factors=factors) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 2d92a41c67..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_scanning_mode.py +++ /dev/null @@ -1,59 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.scanning_mode. - -""" - -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 iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import ScanningMode, scanning_mode - - -class Test(tests.IrisTest): - def test_unset(self): - expected = ScanningMode(False, False, False, False) - self.assertEqual(scanning_mode(0x0), expected) - - def test_i_negative(self): - expected = ScanningMode(i_negative=True, j_positive=False, - j_consecutive=False, i_alternative=False) - self.assertEqual(scanning_mode(0x80), expected) - - def test_j_positive(self): - expected = ScanningMode(i_negative=False, j_positive=True, - j_consecutive=False, i_alternative=False) - self.assertEqual(scanning_mode(0x40), expected) - - def test_j_consecutive(self): - expected = ScanningMode(i_negative=False, j_positive=False, - j_consecutive=True, i_alternative=False) - self.assertEqual(scanning_mode(0x20), expected) - - def test_i_alternative(self): - with self.assertRaises(TranslationError): - scanning_mode(0x10) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index e121005b9e..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_cell_method.py +++ /dev/null @@ -1,130 +0,0 @@ -# (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 . -""" -Tests for function -:func:`iris.fileformats.grib._load_convert.statistical_cell_method`. - -""" - -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 iris.coords import CellMethod -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import statistical_cell_method - - -class Test(tests.IrisTest): - def setUp(self): - self.section = {} - self.section['productDefinitionTemplateNumber'] = 8 - self.section['numberOfTimeRange'] = 1 - self.section['typeOfStatisticalProcessing'] = 0 - self.section['typeOfTimeIncrement'] = 2 - self.section['timeIncrement'] = 0 - - def expected_cell_method(self, - coords=('time',), method='mean', intervals=None): - keys = dict(coords=coords, method=method, intervals=intervals) - cell_method = CellMethod(**keys) - return cell_method - - def test_basic(self): - cell_method = statistical_cell_method(self.section) - self.assertEqual(cell_method, self.expected_cell_method()) - - def test_intervals(self): - self.section['timeIncrement'] = 3 - self.section['indicatorOfUnitForTimeIncrement'] = 1 - cell_method = statistical_cell_method(self.section) - self.assertEqual(cell_method, - self.expected_cell_method(intervals=('3 hours',))) - - def test_different_statistic(self): - self.section['typeOfStatisticalProcessing'] = 6 - cell_method = statistical_cell_method(self.section) - self.assertEqual( - cell_method, - self.expected_cell_method(method='standard_deviation')) - - def test_fail_bad_ranges(self): - self.section['numberOfTimeRange'] = 0 - with self.assertRaisesRegexp(TranslationError, - 'aggregation over "0 time ranges"'): - statistical_cell_method(self.section) - - def test_fail_multiple_ranges(self): - self.section['numberOfTimeRange'] = 2 - with self.assertRaisesRegexp(TranslationError, - 'multiple time ranges \[2\]'): - statistical_cell_method(self.section) - - def test_fail_unknown_statistic(self): - self.section['typeOfStatisticalProcessing'] = 17 - with self.assertRaisesRegexp( - TranslationError, - 'contains an unsupported statistical process type \[17\]'): - statistical_cell_method(self.section) - - def test_fail_bad_increment_type(self): - self.section['typeOfTimeIncrement'] = 7 - with self.assertRaisesRegexp( - TranslationError, - 'time-increment type \[7\] is not supported'): - statistical_cell_method(self.section) - - def test_pdt_9(self): - # Should behave the same as PDT 4.8. - self.section['productDefinitionTemplateNumber'] = 9 - cell_method = statistical_cell_method(self.section) - self.assertEqual(cell_method, self.expected_cell_method()) - - def test_pdt_10(self): - # Should behave the same as PDT 4.8. - self.section['productDefinitionTemplateNumber'] = 10 - cell_method = statistical_cell_method(self.section) - self.assertEqual(cell_method, self.expected_cell_method()) - - def test_pdt_11(self): - # Should behave the same as PDT 4.8. - self.section['productDefinitionTemplateNumber'] = 11 - cell_method = statistical_cell_method(self.section) - self.assertEqual(cell_method, self.expected_cell_method()) - - def test_pdt_15(self): - # Encoded slightly differently to PDT 4.8. - self.section['productDefinitionTemplateNumber'] = 15 - test_code = self.section['typeOfStatisticalProcessing'] - del self.section['typeOfStatisticalProcessing'] - self.section['statisticalProcess'] = test_code - cell_method = statistical_cell_method(self.section) - self.assertEqual(cell_method, self.expected_cell_method()) - - def test_fail_unsupported_pdt(self): - # Rejects PDTs other than the ones tested above. - self.section['productDefinitionTemplateNumber'] = 101 - msg = "can't get statistical method for unsupported pdt : 4.101" - with self.assertRaisesRegexp(ValueError, msg): - statistical_cell_method(self.section) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 4bf3fb4a61..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_statistical_forecast_period_coord.py +++ /dev/null @@ -1,82 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Tests for function -:func:`iris.fileformats.grib._load_convert.statistical_forecast_period`. - -""" - -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 datetime - -from iris.fileformats.grib._load_convert import \ - statistical_forecast_period_coord -from iris.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - module = 'iris.fileformats.grib._load_convert' - self.module = module - self.patch_hindcast = self.patch(module + '._hindcast_fix') - self.forecast_seconds = 0.0 - self.forecast_units = mock.Mock() - self.forecast_units.convert = lambda x, y: self.forecast_seconds - self.patch(module + '.time_range_unit', - return_value=self.forecast_units) - self.frt_coord = mock.Mock() - self.frt_coord.points = [1] - self.frt_coord.units.num2date = mock.Mock( - return_value=datetime.datetime(2010, 2, 3)) - self.section = {} - self.section['yearOfEndOfOverallTimeInterval'] = 2010 - self.section['monthOfEndOfOverallTimeInterval'] = 2 - self.section['dayOfEndOfOverallTimeInterval'] = 3 - self.section['hourOfEndOfOverallTimeInterval'] = 8 - self.section['minuteOfEndOfOverallTimeInterval'] = 0 - self.section['secondOfEndOfOverallTimeInterval'] = 0 - self.section['forecastTime'] = mock.Mock() - self.section['indicatorOfUnitOfTimeRange'] = mock.Mock() - - def test_basic(self): - coord = statistical_forecast_period_coord(self.section, - self.frt_coord) - self.assertEqual(coord.standard_name, 'forecast_period') - self.assertEqual(coord.units, 'hours') - self.assertArrayAlmostEqual(coord.points, [4.0]) - self.assertArrayAlmostEqual(coord.bounds, [[0.0, 8.0]]) - - def test_with_hindcast(self): - coord = statistical_forecast_period_coord(self.section, - self.frt_coord) - self.assertEqual(self.patch_hindcast.call_count, 1) - - def test_no_hindcast(self): - self.patch(self.module + '.options.support_hindcast_values', False) - coord = statistical_forecast_period_coord(self.section, - self.frt_coord) - self.assertEqual(self.patch_hindcast.call_count, 0) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 648a68d587..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_time_range_unit.py +++ /dev/null @@ -1,57 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.time_range_unit. - -""" - -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 - -from iris.exceptions import TranslationError -from iris.fileformats.grib._load_convert import time_range_unit - - -class Test(tests.IrisTest): - def setUp(self): - self.unit_by_indicator = {0: Unit('minutes'), - 1: Unit('hours'), - 2: Unit('days'), - 10: Unit('3 hours'), - 11: Unit('6 hours'), - 12: Unit('12 hours'), - 13: Unit('seconds')} - - def test_units(self): - for indicator, unit in self.unit_by_indicator.items(): - result = time_range_unit(indicator) - self.assertEqual(result, unit) - - def test_bad_indicator(self): - emsg = 'unsupported time range' - with self.assertRaisesRegexp(TranslationError, emsg): - time_range_unit(-1) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 293d568023..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_translate_phenomenon.py +++ /dev/null @@ -1,73 +0,0 @@ -# (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 . -""" -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 -# before importing anything else. -import iris.tests as tests - -from copy import deepcopy - -from cf_units import Unit - -from iris.coords import DimCoord -from iris.fileformats.grib._load_convert import (Probability, - translate_phenomenon) -from iris.fileformats.grib.grib_phenom_translation import _GribToCfDataClass - - -class Test_probability(tests.IrisTest): - def setUp(self): - # Patch inner call to return a given phenomenon type. - target_module = 'iris.fileformats.grib._load_convert' - self.phenom_lookup_patch = self.patch( - target_module + '.itranslation.grib2_phenom_to_cf_info', - return_value=_GribToCfDataClass('air_temperature', '', 'K', None)) - # Construct dummy call arguments - self.probability = Probability('', 22.0) - self.metadata = {'aux_coords_and_dims': []} - - def test_basic(self): - result = translate_phenomenon(self.metadata, None, None, None, - probability=self.probability) - # Check metadata. - thresh_coord = DimCoord([22.0], - standard_name='air_temperature', - long_name='', units='K') - self.assertEqual(self.metadata, { - 'standard_name': None, - 'long_name': 'probability_of_air_temperature_', - 'units': Unit(1), - 'aux_coords_and_dims': [(thresh_coord, None)]}) - - def test_no_phenomenon(self): - original_metadata = deepcopy(self.metadata) - self.phenom_lookup_patch.return_value = None - result = translate_phenomenon(self.metadata, None, None, None, - probability=self.probability) - self.assertEqual(self.metadata, original_metadata) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 1f891177cc..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_unscale.py +++ /dev/null @@ -1,67 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.unscale. - -""" - -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 numpy.ma as ma - -from iris.fileformats.grib._load_convert import _MDI as MDI, unscale - -# Reference GRIB2 Regulation 92.1.12. - - -class Test(tests.IrisTest): - def test_single(self): - self.assertEqual(unscale(123, 1), 12.3) - self.assertEqual(unscale(123, -1), 1230.0) - self.assertEqual(unscale(123, 2), 1.23) - self.assertEqual(unscale(123, -2), 12300.0) - - def test_single_mdi(self): - self.assertIs(unscale(10, MDI), ma.masked) - self.assertIs(unscale(MDI, 1), ma.masked) - - def test_array(self): - items = [[1, [0.1, 1.2, 12.3, 123.4]], - [-1, [10.0, 120.0, 1230.0, 12340.0]], - [2, [0.01, 0.12, 1.23, 12.34]], - [-2, [100.0, 1200.0, 12300.0, 123400.0]]] - values = np.array([1, 12, 123, 1234]) - for factor, expected in items: - result = unscale(values, [factor] * values.size) - self.assertFalse(ma.isMaskedArray(result)) - np.testing.assert_array_equal(result, np.array(expected)) - - def test_array_mdi(self): - result = unscale([1, MDI, 100, 1000], [1, 1, 1, MDI]) - self.assertTrue(ma.isMaskedArray(result)) - expected = ma.masked_values([0.1, MDI, 10.0, MDI], MDI) - np.testing.assert_array_almost_equal(result, expected) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 9d9cc9858f..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_validity_time_coord.py +++ /dev/null @@ -1,84 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.validity_time_coord. - -""" - -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 numpy as np - -from iris.coords import DimCoord -from iris.fileformats.grib._load_convert import validity_time_coord -from iris.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - self.fp = DimCoord(5, standard_name='forecast_period', units='hours') - self.fp_test_bounds = np.array([[1.0, 9.0]]) - self.unit = Unit('hours since epoch') - self.frt = DimCoord(10, standard_name='forecast_reference_time', - units=self.unit) - - def test_frt_shape(self): - frt = mock.Mock(shape=(2,)) - fp = mock.Mock(shape=(1,)) - emsg = 'scalar forecast reference time' - with self.assertRaisesRegexp(ValueError, emsg): - validity_time_coord(frt, fp) - - def test_fp_shape(self): - frt = mock.Mock(shape=(1,)) - fp = mock.Mock(shape=(2,)) - emsg = 'scalar forecast period' - with self.assertRaisesRegexp(ValueError, emsg): - validity_time_coord(frt, fp) - - def test(self): - coord = validity_time_coord(self.frt, self.fp) - self.assertIsInstance(coord, DimCoord) - self.assertEqual(coord.standard_name, 'time') - self.assertEqual(coord.units, self.unit) - self.assertEqual(coord.shape, (1,)) - point = self.frt.points[0] + self.fp.points[0] - self.assertEqual(coord.points[0], point) - self.assertFalse(coord.has_bounds()) - - def test_bounded(self): - self.fp.bounds = self.fp_test_bounds - coord = validity_time_coord(self.frt, self.fp) - self.assertIsInstance(coord, DimCoord) - self.assertEqual(coord.standard_name, 'time') - self.assertEqual(coord.units, self.unit) - self.assertEqual(coord.shape, (1,)) - point = self.frt.points[0] + self.fp.points[0] - self.assertEqual(coord.points[0], point) - self.assertTrue(coord.has_bounds()) - bounds = self.frt.points[0] + self.fp_test_bounds - self.assertArrayAlmostEqual(coord.bounds, bounds) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index daa508891a..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/load_convert/test_vertical_coords.py +++ /dev/null @@ -1,176 +0,0 @@ -# (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 . -""" -Test function :func:`iris.fileformats.grib._load_convert.vertical_coords`. - -""" - -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.exceptions import TranslationError -from iris.fileformats.grib._load_convert import \ - _MDI as MISSING_LEVEL, \ - _TYPE_OF_FIXED_SURFACE_MISSING as MISSING_SURFACE, \ - vertical_coords -from iris.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - self.metadata = {'factories': [], 'references': [], - 'standard_name': None, - 'long_name': None, 'units': None, 'attributes': {}, - 'cell_methods': [], 'dim_coords_and_dims': [], - '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 - with mock.patch(this, side_effect=func) as hybrid_factories: - vertical_coords(section, metadata) - self.assertTrue(hybrid_factories.called) - self.assertEqual(metadata['factories'], [factory]) - - def test_no_first_fixed_surface(self): - metadata = deepcopy(self.metadata) - section = {'NV': 0, - 'typeOfFirstFixedSurface': MISSING_SURFACE, - 'scaledValueOfFirstFixedSurface': MISSING_LEVEL} - vertical_coords(section, metadata) - self.assertEqual(metadata, self.metadata) - - def _check(self, value, msg): - this = 'iris.fileformats.grib._load_convert.options' - with mock.patch('warnings.warn') as warn: - with mock.patch(this) as options: - for request_warning in [False, True]: - options.warn_on_unsupported = request_warning - metadata = deepcopy(self.metadata) - section = {'NV': 0, - 'typeOfFirstFixedSurface': 0, - 'scaledValueOfFirstFixedSurface': value} - # The call being tested. - vertical_coords(section, metadata) - self.assertEqual(metadata, self.metadata) - if request_warning: - self.assertEqual(len(warn.mock_calls), 1) - args, _ = warn.call_args - self.assertIn(msg, args[0]) - else: - self.assertEqual(len(warn.mock_calls), 0) - - def test_unknown_first_fixed_surface_with_missing_scaled_value(self): - self._check(MISSING_LEVEL, 'surface with missing scaled value') - - def test_unknown_first_fixed_surface_with_scaled_value(self): - self._check(0, 'surface with scaled value') - - def test_pressure_with_no_second_fixed_surface(self): - metadata = deepcopy(self.metadata) - section = {'NV': 0, - 'typeOfFirstFixedSurface': 100, # pressure / Pa - 'scaledValueOfFirstFixedSurface': 10, - 'scaleFactorOfFirstFixedSurface': 1, - 'typeOfSecondFixedSurface': MISSING_SURFACE} - vertical_coords(section, metadata) - coord = DimCoord(1.0, long_name='pressure', units='Pa') - expected = deepcopy(self.metadata) - expected['aux_coords_and_dims'].append((coord, None)) - self.assertEqual(metadata, expected) - - def test_height_with_no_second_fixed_surface(self): - metadata = deepcopy(self.metadata) - section = {'NV': 0, - 'typeOfFirstFixedSurface': 103, # height / m - 'scaledValueOfFirstFixedSurface': 100, - 'scaleFactorOfFirstFixedSurface': 2, - 'typeOfSecondFixedSurface': MISSING_SURFACE} - vertical_coords(section, metadata) - coord = DimCoord(1.0, long_name='height', units='m') - expected = deepcopy(self.metadata) - expected['aux_coords_and_dims'].append((coord, None)) - self.assertEqual(metadata, expected) - - def test_different_fixed_surfaces(self): - section = {'NV': 0, - 'typeOfFirstFixedSurface': 100, - 'scaledValueOfFirstFixedSurface': None, - 'scaleFactorOfFirstFixedSurface': None, - 'typeOfSecondFixedSurface': 0} - emsg = 'different types of first and second fixed surface' - with self.assertRaisesRegexp(TranslationError, emsg): - vertical_coords(section, None) - - def test_same_fixed_surfaces_missing_second_scaled_value(self): - section = {'NV': 0, - 'typeOfFirstFixedSurface': 100, - 'scaledValueOfFirstFixedSurface': None, - 'scaleFactorOfFirstFixedSurface': None, - 'typeOfSecondFixedSurface': 100, - 'scaledValueOfSecondFixedSurface': MISSING_LEVEL} - emsg = 'missing scaled value of second fixed surface' - with self.assertRaisesRegexp(TranslationError, emsg): - vertical_coords(section, None) - - def test_pressure_with_second_fixed_surface(self): - metadata = deepcopy(self.metadata) - section = {'NV': 0, - 'typeOfFirstFixedSurface': 100, - 'scaledValueOfFirstFixedSurface': 10, - 'scaleFactorOfFirstFixedSurface': 1, - 'typeOfSecondFixedSurface': 100, - 'scaledValueOfSecondFixedSurface': 30, - 'scaleFactorOfSecondFixedSurface': 1} - vertical_coords(section, metadata) - coord = DimCoord(2.0, long_name='pressure', units='Pa', - bounds=[1.0, 3.0]) - expected = deepcopy(self.metadata) - expected['aux_coords_and_dims'].append((coord, None)) - self.assertEqual(metadata, expected) - - def test_height_with_second_fixed_surface(self): - metadata = deepcopy(self.metadata) - section = {'NV': 0, - 'typeOfFirstFixedSurface': 103, - 'scaledValueOfFirstFixedSurface': 1000, - 'scaleFactorOfFirstFixedSurface': 2, - 'typeOfSecondFixedSurface': 103, - 'scaledValueOfSecondFixedSurface': 3000, - 'scaleFactorOfSecondFixedSurface': 2} - vertical_coords(section, metadata) - coord = DimCoord(20.0, long_name='height', units='m', - bounds=[10.0, 30.0]) - expected = deepcopy(self.metadata) - expected['aux_coords_and_dims'].append((coord, None)) - self.assertEqual(metadata, expected) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/message/__init__.py b/lib/iris/tests/unit/fileformats/grib/message/__init__.py deleted file mode 100644 index 3162608b42..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/message/__init__.py +++ /dev/null @@ -1,20 +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 :mod:`iris.fileformats.grib.message` 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/unit/fileformats/grib/message/test_GribMessage.py b/lib/iris/tests/unit/fileformats/grib/message/test_GribMessage.py deleted file mode 100644 index 7e0516b339..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/message/test_GribMessage.py +++ /dev/null @@ -1,288 +0,0 @@ -# (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 `iris.fileformats.grib.message.GribMessage` class. - -""" - -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 - -from abc import ABCMeta, abstractmethod - -import numpy as np -import numpy.ma as ma - -from iris._lazy_data import as_concrete_data, is_lazy_data -from iris.exceptions import TranslationError -from iris.fileformats.grib.message import GribMessage -from iris.tests import mock -from iris.tests.unit.fileformats.grib import _make_test_message - - -SECTION_6_NO_BITMAP = {'bitMapIndicator': 255, 'bitmap': None} - - -@tests.skip_data -class Test_messages_from_filename(tests.IrisTest): - def test(self): - filename = tests.get_data_path(('GRIB', '3_layer_viz', - '3_layer.grib2')) - messages = list(GribMessage.messages_from_filename(filename)) - self.assertEqual(len(messages), 3) - - def test_release_file(self): - filename = tests.get_data_path(('GRIB', '3_layer_viz', - '3_layer.grib2')) - my_file = open(filename) - self.patch('__builtin__.open', mock.Mock(return_value=my_file)) - messages = list(GribMessage.messages_from_filename(filename)) - self.assertFalse(my_file.closed) - del messages - self.assertTrue(my_file.closed) - - -class Test_sections(tests.IrisTest): - def test(self): - # Check that the `sections` attribute defers to the `sections` - # attribute on the underlying _RawGribMessage. - message = _make_test_message(mock.sentinel.SECTIONS) - self.assertIs(message.sections, mock.sentinel.SECTIONS) - - -class Test_data__masked(tests.IrisTest): - def setUp(self): - self.bitmap = np.array([0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1]) - self.shape = (3, 4) - self._section_3 = {'sourceOfGridDefinition': 0, - 'numberOfOctectsForNumberOfPoints': 0, - 'interpretationOfNumberOfPoints': 0, - 'gridDefinitionTemplateNumber': 0, - 'scanningMode': 0, - 'Nj': self.shape[0], - 'Ni': self.shape[1]} - - def test_no_bitmap(self): - values = np.arange(12) - message = _make_test_message({3: self._section_3, - 6: SECTION_6_NO_BITMAP, - 7: {'codedValues': values}}) - result = as_concrete_data(message.data) - expected = values.reshape(self.shape) - self.assertEqual(result.shape, self.shape) - self.assertArrayEqual(result, expected) - - def test_bitmap_present(self): - # Test the behaviour where bitmap and codedValues shapes - # are not equal. - input_values = np.arange(5) - output_values = np.array([-1, -1, 0, 1, -1, -1, -1, 2, -1, 3, -1, 4]) - message = _make_test_message({3: self._section_3, - 6: {'bitMapIndicator': 0, - 'bitmap': self.bitmap}, - 7: {'codedValues': input_values}}) - result = as_concrete_data(message.data) - expected = ma.masked_array(output_values, - np.logical_not(self.bitmap)) - expected = expected.reshape(self.shape) - self.assertMaskedArrayEqual(result, expected) - - def test_bitmap__shapes_mismatch(self): - # Test the behaviour where bitmap and codedValues shapes do not match. - # Too many or too few unmasked values in codedValues will cause this. - values = np.arange(6) - message = _make_test_message({3: self._section_3, - 6: {'bitMapIndicator': 0, - 'bitmap': self.bitmap}, - 7: {'codedValues': values}}) - with self.assertRaisesRegexp(TranslationError, 'do not match'): - as_concrete_data(message.data) - - def test_bitmap__invalid_indicator(self): - values = np.arange(12) - message = _make_test_message({3: self._section_3, - 6: {'bitMapIndicator': 100, - 'bitmap': None}, - 7: {'codedValues': values}}) - with self.assertRaisesRegexp(TranslationError, 'unsupported bitmap'): - as_concrete_data(message.data) - - -class Test_data__unsupported(tests.IrisTest): - def test_unsupported_grid_definition(self): - message = _make_test_message({3: {'sourceOfGridDefinition': 1}, - 6: SECTION_6_NO_BITMAP}) - with self.assertRaisesRegexp(TranslationError, 'source'): - message.data - - def test_unsupported_quasi_regular__number_of_octets(self): - message = _make_test_message( - {3: {'sourceOfGridDefinition': 0, - 'numberOfOctectsForNumberOfPoints': 1, - 'gridDefinitionTemplateNumber': 0}, - 6: SECTION_6_NO_BITMAP}) - with self.assertRaisesRegexp(TranslationError, 'quasi-regular'): - message.data - - def test_unsupported_quasi_regular__interpretation(self): - message = _make_test_message( - {3: {'sourceOfGridDefinition': 0, - 'numberOfOctectsForNumberOfPoints': 0, - 'interpretationOfNumberOfPoints': 1, - 'gridDefinitionTemplateNumber': 0}, - 6: SECTION_6_NO_BITMAP}) - with self.assertRaisesRegexp(TranslationError, 'quasi-regular'): - message.data - - def test_unsupported_template(self): - message = _make_test_message( - {3: {'sourceOfGridDefinition': 0, - 'numberOfOctectsForNumberOfPoints': 0, - 'interpretationOfNumberOfPoints': 0, - 'gridDefinitionTemplateNumber': 2}}) - with self.assertRaisesRegexp(TranslationError, 'template'): - message.data - - -# Abstract, mix-in class for testing the `data` attribute for various -# grid definition templates. -class Mixin_data__grid_template(six.with_metaclass(ABCMeta, object)): - @abstractmethod - def section_3(self, scanning_mode): - raise NotImplementedError() - - def test_unsupported_scanning_mode(self): - message = _make_test_message( - {3: self.section_3(1), - 6: SECTION_6_NO_BITMAP}) - with self.assertRaisesRegexp(TranslationError, 'scanning mode'): - message.data - - def _test(self, scanning_mode): - message = _make_test_message( - {3: self.section_3(scanning_mode), - 6: SECTION_6_NO_BITMAP, - 7: {'codedValues': np.arange(12)}}) - data = message.data - self.assertTrue(is_lazy_data(data)) - self.assertEqual(data.shape, (3, 4)) - self.assertEqual(data.dtype, np.floating) - self.assertArrayEqual(as_concrete_data(data), - np.arange(12).reshape(3, 4)) - - def test_regular_mode_0(self): - self._test(0) - - def test_regular_mode_64(self): - self._test(64) - - def test_regular_mode_128(self): - self._test(128) - - def test_regular_mode_64_128(self): - self._test(64 | 128) - - -def _example_section_3(grib_definition_template_number, scanning_mode): - return {'sourceOfGridDefinition': 0, - 'numberOfOctectsForNumberOfPoints': 0, - 'interpretationOfNumberOfPoints': 0, - 'gridDefinitionTemplateNumber': grib_definition_template_number, - 'scanningMode': scanning_mode, - 'Nj': 3, - 'Ni': 4} - - -@tests.iristest_timing_decorator -class Test_data__grid_template_0(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - return _example_section_3(0, scanning_mode) - - -@tests.iristest_timing_decorator -class Test_data__grid_template_1(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - return _example_section_3(1, scanning_mode) - - -@tests.iristest_timing_decorator -class Test_data__grid_template_5(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - return _example_section_3(5, scanning_mode) - - -@tests.iristest_timing_decorator -class Test_data__grid_template_12(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - return _example_section_3(12, scanning_mode) - - -@tests.iristest_timing_decorator -class Test_data__grid_template_30(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - section_3 = _example_section_3(30, scanning_mode) - # Dimensions are 'Nx' + 'Ny' instead of 'Ni' + 'Nj'. - section_3['Nx'] = section_3['Ni'] - section_3['Ny'] = section_3['Nj'] - del section_3['Ni'] - del section_3['Nj'] - return section_3 - - -@tests.iristest_timing_decorator -class Test_data__grid_template_40_regular(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - return _example_section_3(40, scanning_mode) - - -@tests.iristest_timing_decorator -class Test_data__grid_template_90(tests.IrisTest_nometa, - Mixin_data__grid_template): - def section_3(self, scanning_mode): - section_3 = _example_section_3(90, scanning_mode) - # Exceptionally, dimensions are 'Nx' + 'Ny' instead of 'Ni' + 'Nj'. - section_3['Nx'] = section_3['Ni'] - section_3['Ny'] = section_3['Nj'] - del section_3['Ni'] - del section_3['Nj'] - return section_3 - - -class Test_data__unknown_grid_template(tests.IrisTest): - def test(self): - message = _make_test_message( - {3: _example_section_3(999, 0), - 6: SECTION_6_NO_BITMAP, - 7: {'codedValues': np.arange(12)}}) - with self.assertRaisesRegexp(TranslationError, - 'template 999 is not supported'): - data = message.data - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/message/test_Section.py b/lib/iris/tests/unit/fileformats/grib/message/test_Section.py deleted file mode 100644 index 07cee31dff..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/message/test_Section.py +++ /dev/null @@ -1,98 +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 `iris.fileformats.grib.message.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 -# importing anything else. -import iris.tests as tests - -import gribapi -import numpy as np - -from iris.fileformats.grib.message import Section - - -@tests.skip_data -class Test___getitem__(tests.IrisTest): - def setUp(self): - filename = tests.get_data_path(('GRIB', 'uk_t', 'uk_t.grib2')) - with open(filename, 'rb') as grib_fh: - self.grib_id = gribapi.grib_new_from_file(grib_fh) - - def test_scalar(self): - section = Section(self.grib_id, None, ['Ni']) - self.assertEqual(section['Ni'], 47) - - def test_array(self): - section = Section(self.grib_id, None, ['codedValues']) - codedValues = section['codedValues'] - self.assertEqual(codedValues.shape, (1551,)) - self.assertArrayAlmostEqual(codedValues[:3], - [-1.78140259, -1.53140259, -1.28140259]) - - def test_typeOfFirstFixedSurface(self): - section = Section(self.grib_id, None, ['typeOfFirstFixedSurface']) - self.assertEqual(section['typeOfFirstFixedSurface'], 100) - - def test_numberOfSection(self): - n = 4 - section = Section(self.grib_id, n, ['numberOfSection']) - self.assertEqual(section['numberOfSection'], n) - - def test_invalid(self): - section = Section(self.grib_id, None, ['Ni']) - with self.assertRaisesRegexp(KeyError, 'Nii'): - section['Nii'] - - -@tests.skip_data -class Test__getitem___pdt_31(tests.IrisTest): - def setUp(self): - filename = tests.get_data_path(('GRIB', 'umukv', 'ukv_chan9.grib2')) - with open(filename, 'rb') as grib_fh: - self.grib_id = gribapi.grib_new_from_file(grib_fh) - self.keys = ['satelliteSeries', 'satelliteNumber', 'instrumentType', - 'scaleFactorOfCentralWaveNumber', - 'scaledValueOfCentralWaveNumber'] - - def test_array(self): - section = Section(self.grib_id, None, self.keys) - for key in self.keys: - value = section[key] - self.assertIsInstance(value, np.ndarray) - self.assertEqual(value.shape, (1,)) - - -@tests.skip_data -class Test_get_computed_key(tests.IrisTest): - def test_gdt40_computed(self): - fname = tests.get_data_path(('GRIB', 'gaussian', 'regular_gg.grib2')) - with open(fname, 'rb') as grib_fh: - self.grib_id = gribapi.grib_new_from_file(grib_fh) - section = Section(self.grib_id, None, []) - latitudes = section.get_computed_key('latitudes') - self.assertTrue(88.55 < latitudes[0] < 88.59) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py b/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py deleted file mode 100644 index 37b1e38470..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/message/test__DataProxy.py +++ /dev/null @@ -1,58 +0,0 @@ -# (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 `iris.fileformats.grib.message._DataProxy` class. - -""" - -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 -from numpy.random import randint - -from iris.exceptions import TranslationError -from iris.fileformats.grib.message import _DataProxy - - -class Test__bitmap(tests.IrisTest): - def test_no_bitmap(self): - section_6 = {'bitMapIndicator': 255, 'bitmap': None} - data_proxy = _DataProxy(0, 0, 0) - result = data_proxy._bitmap(section_6) - self.assertIsNone(result) - - def test_bitmap_present(self): - bitmap = randint(2, size=(12)) - section_6 = {'bitMapIndicator': 0, 'bitmap': bitmap} - data_proxy = _DataProxy(0, 0, 0) - result = data_proxy._bitmap(section_6) - self.assertArrayEqual(bitmap, result) - - def test_bitmap__invalid_indicator(self): - section_6 = {'bitMapIndicator': 100, 'bitmap': None} - data_proxy = _DataProxy(0, 0, 0) - with self.assertRaisesRegexp(TranslationError, 'unsupported bitmap'): - data_proxy._bitmap(section_6) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py b/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py deleted file mode 100644 index b7e05de87b..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/message/test__MessageLocation.py +++ /dev/null @@ -1,48 +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.message._MessageLocation` class. - -""" - -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 iris.fileformats.grib.message import _MessageLocation -from iris.tests import mock - - -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' - expected = mock.sentinel.message - with mock.patch(patch_target, return_value=expected) as rgm: - result = message_location() - rgm.assert_called_once_with(mock.sentinel.filename, - mock.sentinel.location) - self.assertIs(result, expected) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py b/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py deleted file mode 100644 index c192ee48d9..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/message/test__RawGribMessage.py +++ /dev/null @@ -1,67 +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.message._RawGribMessage` class. - -""" - -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 - -from iris.fileformats.grib.message import _RawGribMessage - - -@tests.skip_data -class Test(tests.IrisTest): - def setUp(self): - filename = tests.get_data_path(('GRIB', 'uk_t', 'uk_t.grib2')) - with open(filename, 'rb') as grib_fh: - grib_id = gribapi.grib_new_from_file(grib_fh) - self.message = _RawGribMessage(grib_id) - - def test_sections__set(self): - # Test that sections writes into the _sections attribute. - res = self.message.sections - self.assertNotEqual(self.message._sections, None) - - def test_sections__indexing(self): - res = self.message.sections[3]['scanningMode'] - expected = 64 - self.assertEqual(expected, res) - - def test__get_message_sections__section_numbers(self): - res = list(self.message.sections.keys()) - self.assertEqual(res, list(range(9))) - - def test_sections__numberOfSection_value(self): - # The key `numberOfSection` is repeated in every section meaning that - # if requested using gribapi it always defaults to its last value (7). - # This tests that the `_RawGribMessage._get_message_sections` - # override is functioning. - section_number = 4 - res = self.message.sections[section_number]['numberOfSection'] - self.assertEqual(res, section_number) - - -if __name__ == '__main__': - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py b/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py deleted file mode 100644 index 04ceccc5f3..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/__init__.py +++ /dev/null @@ -1,100 +0,0 @@ -# (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 the :mod:`iris.fileformats.grib.grib_save_rules` module. - -""" - -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 - - -class GdtTestMixin(object): - """Some handy common test capabilities for grib grid-definition tests.""" - TARGET_MODULE = 'iris.fileformats.grib._save_rules' - - def setUp(self): - # Patch the gribapi of the tested module. - self.mock_gribapi = self.patch(self.TARGET_MODULE + '.gribapi') - - # Fix the mock gribapi to record key assignments. - def grib_set_trap(grib, name, value): - # Record a key setting on the mock passed as the 'grib message id'. - grib.keys[name] = value - - self.mock_gribapi.grib_set = grib_set_trap - self.mock_gribapi.grib_set_long = grib_set_trap - self.mock_gribapi.grib_set_float = grib_set_trap - self.mock_gribapi.grib_set_double = grib_set_trap - self.mock_gribapi.grib_set_long_array = grib_set_trap - self.mock_gribapi.grib_set_array = grib_set_trap - - # Create a mock 'grib message id', with a 'keys' dict for settings. - self.mock_grib = mock.Mock(keys={}) - - # Initialise the test cube and its coords to something barely usable. - self.test_cube = self._make_test_cube() - - def _default_coord_system(self): - return iris.coord_systems.GeogCS(PP_DEFAULT_EARTH_RADIUS) - - def _default_x_points(self): - # Define simple, regular coordinate points. - return [1.0, 2.0, 3.0] - - def _default_y_points(self): - return [7.0, 8.0] # N.B. is_regular will *fail* on length-1 coords. - - def _make_test_cube(self, cs=None, x_points=None, y_points=None): - # Create a cube with given properties, or minimal defaults. - if cs is None: - cs = self._default_coord_system() - if x_points is None: - x_points = self._default_x_points() - if y_points is None: - y_points = self._default_y_points() - - x_coord = iris.coords.DimCoord(x_points, standard_name='longitude', - units='degrees', - coord_system=cs) - y_coord = iris.coords.DimCoord(y_points, standard_name='latitude', - units='degrees', - coord_system=cs) - test_cube = iris.cube.Cube(np.zeros((len(y_points), len(x_points)))) - test_cube.add_dim_coord(y_coord, 0) - test_cube.add_dim_coord(x_coord, 1) - return test_cube - - def _check_key(self, name, value): - # Test that a specific grib key assignment occurred. - msg_fmt = 'Expected grib setting "{}" = {}, got {}' - found = self.mock_grib.keys.get(name) - if found is None: - self.assertEqual(0, 1, msg_fmt.format(name, value, '((UNSET))')) - else: - self.assertArrayEqual(found, value, - msg_fmt.format(name, value, found)) 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 deleted file mode 100644 index 6d44f1a9ae..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test__missing_forecast_period.py +++ /dev/null @@ -1,111 +0,0 @@ -# (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._missing_forecast_period.` - -""" - -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 iris.coords import DimCoord -from iris.cube import Cube -from iris.fileformats.grib._save_rules import _missing_forecast_period - - -class TestNoForecastReferenceTime(tests.IrisTest): - def test_no_bounds(self): - t_coord = DimCoord(15, 'time', units='hours since epoch') - cube = Cube(23) - cube.add_aux_coord(t_coord) - - res = _missing_forecast_period(cube) - expected_rt = t_coord.units.num2date(15) - expected_rt_type = 3 - expected_fp = 0 - expected_fp_type = 1 - expected = (expected_rt, - expected_rt_type, - expected_fp, - expected_fp_type) - self.assertEqual(res, expected) - - def test_with_bounds(self): - t_coord = DimCoord(15, 'time', bounds=[14, 16], - units='hours since epoch') - cube = Cube(23) - cube.add_aux_coord(t_coord) - - res = _missing_forecast_period(cube) - expected_rt = t_coord.units.num2date(14) - expected_rt_type = 3 - expected_fp = 0 - expected_fp_type = 1 - expected = (expected_rt, - expected_rt_type, - expected_fp, - expected_fp_type) - self.assertEqual(res, expected) - - -class TestWithForecastReferenceTime(tests.IrisTest): - def test_no_bounds(self): - t_coord = DimCoord(3, 'time', units='days since epoch') - frt_coord = DimCoord(8, 'forecast_reference_time', - units='hours since epoch') - cube = Cube(23) - cube.add_aux_coord(t_coord) - cube.add_aux_coord(frt_coord) - - res = _missing_forecast_period(cube) - expected_rt = frt_coord.units.num2date(8) - expected_rt_type = 1 - expected_fp = 3 * 24 - 8 - expected_fp_type = 1 - expected = (expected_rt, - expected_rt_type, - expected_fp, - expected_fp_type) - self.assertEqual(res, expected) - - def test_with_bounds(self): - t_coord = DimCoord(3, 'time', bounds=[2, 4], units='days since epoch') - frt_coord = DimCoord(8, 'forecast_reference_time', - units='hours since epoch') - cube = Cube(23) - cube.add_aux_coord(t_coord) - cube.add_aux_coord(frt_coord) - - res = _missing_forecast_period(cube) - expected_rt = frt_coord.units.num2date(8) - expected_rt_type = 1 - expected_fp = 2 * 24 - 8 - expected_fp_type = 1 - expected = (expected_rt, - expected_rt_type, - expected_fp, - expected_fp_type) - self.assertEqual(res, expected) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 4b507cebc2..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test__non_missing_forecast_period.py +++ /dev/null @@ -1,65 +0,0 @@ -# (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 module-level functions. - -""" - -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.fileformats.grib._save_rules import _non_missing_forecast_period - - -class Test(tests.IrisTest): - def _cube(self, t_bounds=False): - time_coord = iris.coords.DimCoord(15, standard_name='time', - units='hours since epoch') - fp_coord = iris.coords.DimCoord(10, standard_name='forecast_period', - units='hours') - if t_bounds: - time_coord.bounds = [[8, 100]] - fp_coord.bounds = [[3, 95]] - cube = iris.cube.Cube([23]) - cube.add_dim_coord(time_coord, 0) - cube.add_aux_coord(fp_coord, 0) - return cube - - def test_time_point(self): - cube = self._cube() - rt, rt_meaning, fp, fp_meaning = _non_missing_forecast_period(cube) - self.assertEqual((rt_meaning, fp, fp_meaning), (1, 10, 1)) - - def test_time_bounds(self): - cube = self._cube(t_bounds=True) - rt, rt_meaning, fp, fp_meaning = _non_missing_forecast_period(cube) - self.assertEqual((rt_meaning, fp, fp_meaning), (1, 3, 1)) - - def test_time_bounds_in_minutes(self): - cube = self._cube(t_bounds=True) - cube.coord('forecast_period').convert_units('minutes') - rt, rt_meaning, fp, fp_meaning = _non_missing_forecast_period(cube) - self.assertEqual((rt_meaning, fp, fp_meaning), (1, 180, 0)) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_10_and_11.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_10_and_11.py deleted file mode 100644 index 1bece351b0..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test__product_definition_template_8_10_and_11.py +++ /dev/null @@ -1,210 +0,0 @@ -# (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_8_10_and_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 before -# importing anything else. -import iris.tests as tests - -from cf_units import Unit -import gribapi -import mock - -from iris.coords import CellMethod, DimCoord -from iris.fileformats.grib._save_rules import \ - _product_definition_template_8_10_and_11 -import iris.tests.stock as stock - - -class TestTypeOfStatisticalProcessing(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(23, 'time', bounds=[0, 100], - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord) - - @mock.patch.object(gribapi, 'grib_set') - def test_sum(self, mock_set): - cube = self.cube - cell_method = CellMethod(method='sum', coords=['time']) - cube.add_cell_method(cell_method) - - _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - "typeOfStatisticalProcessing", 1) - - @mock.patch.object(gribapi, 'grib_set') - def test_unrecognised(self, mock_set): - cube = self.cube - cell_method = CellMethod(method='95th percentile', coords=['time']) - cube.add_cell_method(cell_method) - - _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - "typeOfStatisticalProcessing", 255) - - @mock.patch.object(gribapi, 'grib_set') - def test_multiple_cell_method_coords(self, mock_set): - cube = self.cube - cell_method = CellMethod(method='sum', - coords=['time', 'forecast_period']) - cube.add_cell_method(cell_method) - with self.assertRaisesRegexp(ValueError, - 'Cannot handle multiple coordinate name'): - _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): - cube = self.cube - cell_method = CellMethod(method='mean', coords=['season']) - cube.add_cell_method(cell_method) - with self.assertRaisesRegexp( - ValueError, "Expected a cell method with a coordinate " - "name of 'time'"): - _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) - - -class TestTimeCoordPrerequisites(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') - - @mock.patch.object(gribapi, 'grib_set') - def test_multiple_points(self, mock_set): - # Add time coord with multiple points. - coord = DimCoord([23, 24, 25], 'time', - bounds=[[22, 23], [23, 24], [24, 25]], - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord, 0) - with self.assertRaisesRegexp( - ValueError, 'Expected length one time coordinate'): - _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): - # Add time coord with no bounds. - coord = DimCoord(23, 'time', - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord) - with self.assertRaisesRegexp( - ValueError, 'Expected time coordinate with two bounds, ' - 'got 0 bounds'): - _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): - # Add time coord with more than two bounds. - coord = DimCoord(23, 'time', bounds=[21, 22, 23], - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord) - with self.assertRaisesRegexp( - ValueError, 'Expected time coordinate with two bounds, ' - 'got 3 bounds'): - _product_definition_template_8_10_and_11(self.cube, - mock.sentinel.grib) - - -class TestEndOfOverallTimeInterval(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') - cell_method = CellMethod(method='sum', coords=['time']) - self.cube.add_cell_method(cell_method) - - @mock.patch.object(gribapi, 'grib_set') - def test_default_calendar(self, mock_set): - cube = self.cube - # End bound is 1972-04-26 10:27:07. - coord = DimCoord(23.0, 'time', bounds=[0.452, 20314.452], - units=Unit('hours since epoch')) - cube.add_aux_coord(coord) - - grib = mock.sentinel.grib - _product_definition_template_8_10_and_11(cube, grib) - - mock_set.assert_any_call( - grib, "yearOfEndOfOverallTimeInterval", 1972) - mock_set.assert_any_call( - grib, "monthOfEndOfOverallTimeInterval", 4) - mock_set.assert_any_call( - grib, "dayOfEndOfOverallTimeInterval", 26) - mock_set.assert_any_call( - grib, "hourOfEndOfOverallTimeInterval", 10) - mock_set.assert_any_call( - grib, "minuteOfEndOfOverallTimeInterval", 27) - mock_set.assert_any_call( - grib, "secondOfEndOfOverallTimeInterval", 7) - - @mock.patch.object(gribapi, 'grib_set') - def test_360_day_calendar(self, mock_set): - cube = self.cube - # End bound is 1972-05-07 10:27:07 - coord = DimCoord(23.0, 'time', bounds=[0.452, 20314.452], - units=Unit('hours since epoch', calendar='360_day')) - cube.add_aux_coord(coord) - - grib = mock.sentinel.grib - _product_definition_template_8_10_and_11(cube, grib) - - mock_set.assert_any_call( - grib, "yearOfEndOfOverallTimeInterval", 1972) - mock_set.assert_any_call( - grib, "monthOfEndOfOverallTimeInterval", 5) - mock_set.assert_any_call( - grib, "dayOfEndOfOverallTimeInterval", 7) - mock_set.assert_any_call( - grib, "hourOfEndOfOverallTimeInterval", 10) - mock_set.assert_any_call( - grib, "minuteOfEndOfOverallTimeInterval", 27) - mock_set.assert_any_call( - grib, "secondOfEndOfOverallTimeInterval", 7) - - -class TestNumberOfTimeRange(tests.IrisTest): - @mock.patch.object(gribapi, 'grib_set') - def test_other_cell_methods(self, mock_set): - cube = stock.lat_lon_cube() - # Rename cube to avoid warning about unknown discipline/parameter. - cube.rename('air_temperature') - coord = DimCoord(23, 'time', bounds=[0, 24], - units=Unit('hours since epoch')) - cube.add_aux_coord(coord) - # Add one time cell method and another unrelated one. - cell_method = CellMethod(method='mean', coords=['elephants']) - cube.add_cell_method(cell_method) - cell_method = CellMethod(method='sum', coords=['time']) - cube.add_cell_method(cell_method) - - _product_definition_template_8_10_and_11(cube, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, 'numberOfTimeRange', 1) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index e44e038789..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_data_section.py +++ /dev/null @@ -1,170 +0,0 @@ -# (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 -: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 -# 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 - - -GRIB_API = 'iris.fileformats.grib._save_rules.gribapi' -GRIB_MESSAGE = mock.sentinel.GRIB_MESSAGE - - -class TestMDI(tests.IrisTest): - def assertBitmapOff(self, grib_api): - # Check the use of a mask has been turned off via: - # gribapi.grib_set(grib_message, 'bitmapPresent', 0) - grib_api.grib_set.assert_called_once_with(GRIB_MESSAGE, - 'bitmapPresent', 0) - - def assertBitmapOn(self, grib_api, fill_value): - # Check the use of a mask has been turned on via: - # gribapi.grib_set(grib_message, 'bitmapPresent', 1) - # gribapi.grib_set_double(grib_message, 'missingValue', fill_value) - grib_api.grib_set.assert_called_once_with(GRIB_MESSAGE, - 'bitmapPresent', 1) - grib_api.grib_set_double.assert_called_once_with(GRIB_MESSAGE, - 'missingValue', - fill_value) - - def assertBitmapRange(self, grib_api, min_data, max_data): - # Check the use of a mask has been turned on via: - # gribapi.grib_set(grib_message, 'bitmapPresent', 1) - # gribapi.grib_set_double(grib_message, 'missingValue', ...) - # and that a suitable fill value has been chosen. - grib_api.grib_set.assert_called_once_with(GRIB_MESSAGE, - 'bitmapPresent', 1) - args, = grib_api.grib_set_double.call_args_list - (message, key, fill_value), kwargs = args - self.assertIs(message, GRIB_MESSAGE) - self.assertEqual(key, 'missingValue') - self.assertTrue(fill_value < min_data or fill_value > max_data, - 'Fill value {} is not outside data range ' - '{} to {}.'.format(fill_value, min_data, max_data)) - return fill_value - - def assertValues(self, grib_api, values): - # Check the correct data values have been set via: - # gribapi.grib_set_double_array(grib_message, 'values', ...) - args, = grib_api.grib_set_double_array.call_args_list - (message, key, values), kwargs = args - self.assertIs(message, GRIB_MESSAGE) - self.assertEqual(key, 'values') - self.assertArrayEqual(values, values) - self.assertEqual(kwargs, {}) - - def test_simple(self): - # Check the simple case of non-masked data with no scaling. - cube = iris.cube.Cube(np.arange(5)) - grib_message = mock.sentinel.GRIB_MESSAGE - with mock.patch(GRIB_API) as grib_api: - data_section(cube, grib_message) - # Check the use of a mask has been turned off. - self.assertBitmapOff(grib_api) - # Check the correct data values have been set. - self.assertValues(grib_api, np.arange(5)) - - def test_masked_with_finite_fill_value(self): - cube = iris.cube.Cube(np.ma.MaskedArray([1.0, 2.0, 3.0, 1.0, 2.0, 3.0], - mask=[0, 0, 0, 1, 1, 1], - fill_value=2000)) - grib_message = mock.sentinel.GRIB_MESSAGE - with mock.patch(GRIB_API) as grib_api: - data_section(cube, grib_message) - # Check the use of a mask has been turned on. - FILL = 2000 - self.assertBitmapOn(grib_api, FILL) - # Check the correct data values have been set. - self.assertValues(grib_api, [1, 2, 3, FILL, FILL, FILL]) - - def test_masked_with_nan_fill_value(self): - cube = iris.cube.Cube(np.ma.MaskedArray([1.0, 2.0, 3.0, 1.0, 2.0, 3.0], - mask=[0, 0, 0, 1, 1, 1], - fill_value=np.nan)) - grib_message = mock.sentinel.GRIB_MESSAGE - with mock.patch(GRIB_API) as grib_api: - data_section(cube, grib_message) - # Check the use of a mask has been turned on and a suitable fill - # value has been chosen. - FILL = self.assertBitmapRange(grib_api, 1, 3) - # Check the correct data values have been set. - self.assertValues(grib_api, [1, 2, 3, FILL, FILL, FILL]) - - def test_scaled(self): - # If the Cube's units don't match the units required by GRIB - # ensure the data values are scaled correctly. - cube = iris.cube.Cube(np.arange(5), - standard_name='geopotential_height', units='km') - grib_message = mock.sentinel.GRIB_MESSAGE - with mock.patch(GRIB_API) as grib_api: - data_section(cube, grib_message) - # Check the use of a mask has been turned off. - self.assertBitmapOff(grib_api) - # Check the correct data values have been set. - self.assertValues(grib_api, np.arange(5) * 1000) - - def test_scaled_with_finite_fill_value(self): - # When re-scaling masked data with a finite fill value, ensure - # the fill value and any filled values are also re-scaled. - cube = iris.cube.Cube(np.ma.MaskedArray([1.0, 2.0, 3.0, 1.0, 2.0, 3.0], - mask=[0, 0, 0, 1, 1, 1], - fill_value=2000), - standard_name='geopotential_height', units='km') - grib_message = mock.sentinel.GRIB_MESSAGE - with mock.patch(GRIB_API) as grib_api: - data_section(cube, grib_message) - # Check the use of a mask has been turned on. - FILL = 2000 * 1000 - self.assertBitmapOn(grib_api, FILL) - # Check the correct data values have been set. - self.assertValues(grib_api, [1000, 2000, 3000, FILL, FILL, FILL]) - - def test_scaled_with_nan_fill_value(self): - # When re-scaling masked data with a NaN fill value, ensure - # a fill value is chosen which allows for the scaling, and any - # filled values match the chosen fill value. - cube = iris.cube.Cube(np.ma.MaskedArray([-1.0, 2.0, -1.0, 2.0], - mask=[0, 0, 1, 1], - fill_value=np.nan), - standard_name='geopotential_height', units='km') - grib_message = mock.sentinel.GRIB_MESSAGE - with mock.patch(GRIB_API) as grib_api: - data_section(cube, grib_message) - # Check the use of a mask has been turned on and a suitable fill - # value has been chosen. - FILL = self.assertBitmapRange(grib_api, -1000, 2000) - # Check the correct data values have been set. - self.assertValues(grib_api, [-1000, 2000, FILL, FILL]) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 0721ecaaa7..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_float32_as_int32.py +++ /dev/null @@ -1,63 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Unit tests for `iris.fileformats.grib._save_rules.fixup_float32_as_int32`. - -""" - -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 iris.fileformats.grib._save_rules import fixup_float32_as_int32 - - -class Test(tests.IrisTest): - def test_positive_zero(self): - result = fixup_float32_as_int32(0.0) - self.assertEqual(result, 0) - - def test_negative_zero(self): - result = fixup_float32_as_int32(-0.0) - self.assertEqual(result, 0) - - def test_high_bit_clear_1(self): - # Start with the float32 value for the bit pattern 0x00000001. - result = fixup_float32_as_int32(1.401298464324817e-45) - self.assertEqual(result, 1) - - def test_high_bit_clear_2(self): - # Start with the float32 value for the bit pattern 0x00000002. - result = fixup_float32_as_int32(2.802596928649634e-45) - self.assertEqual(result, 2) - - def test_high_bit_set_1(self): - # Start with the float32 value for the bit pattern 0x80000001. - result = fixup_float32_as_int32(-1.401298464324817e-45) - self.assertEqual(result, -1) - - def test_high_bit_set_2(self): - # Start with the float32 value for the bit pattern 0x80000002. - result = fixup_float32_as_int32(-2.802596928649634e-45) - self.assertEqual(result, -2) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index 01e5d7dea9..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_fixup_int32_as_uint32.py +++ /dev/null @@ -1,55 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Unit tests for `iris.fileformats.grib._save_rules.fixup_int32_as_uint32`. - -""" - -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 iris.fileformats.grib._save_rules import fixup_int32_as_uint32 - - -class Test(tests.IrisTest): - def test_very_negative(self): - with self.assertRaises(ValueError): - fixup_int32_as_uint32(-0x80000000) - - def test_negative(self): - result = fixup_int32_as_uint32(-3) - self.assertEqual(result, 0x80000003) - - def test_zero(self): - result = fixup_int32_as_uint32(0) - self.assertEqual(result, 0) - - def test_positive(self): - result = fixup_int32_as_uint32(5) - self.assertEqual(result, 5) - - def test_very_positive(self): - with self.assertRaises(ValueError): - fixup_int32_as_uint32(0x80000000) - - -if __name__ == '__main__': - tests.main() 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 deleted file mode 100644 index bdd4019b28..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_section.py +++ /dev/null @@ -1,58 +0,0 @@ -# (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 -:meth:`iris.fileformats.grib._save_rules.grid_definition_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 -# importing anything else. -import iris.tests as tests - -from iris.coord_systems import Orthographic -from iris.exceptions import TranslationError -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): - def setUp(self): - GdtTestMixin.setUp(self) - - def test__fail_irregular_latlon(self): - test_cube = self._make_test_cube(x_points=(1, 2, 11, 12), - y_points=(4, 5, 6)) - with self.assertRaisesRegexp( - TranslationError, - 'irregular latlon grid .* not yet supported'): - grid_definition_section(test_cube, self.mock_grib) - - def test__fail_unsupported_coord_system(self): - cs = Orthographic(0, 0) - test_cube = self._make_test_cube(cs=cs) - with self.assertRaisesRegexp( - ValueError, - 'Grib saving is not supported for coordinate system:'): - grid_definition_section(test_cube, self.mock_grib) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index a4f581b78e..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_0.py +++ /dev/null @@ -1,95 +0,0 @@ -# (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 -:meth:`iris.fileformats.grib._save_rules.grid_definition_template_0`. - -""" - -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 - -from iris.coord_systems import GeogCS -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): - def setUp(self): - GdtTestMixin.setUp(self) - - def test__template_number(self): - grid_definition_template_0(self.test_cube, self.mock_grib) - self._check_key('gridDefinitionTemplateNumber', 0) - - def test__shape_of_earth_spherical(self): - cs = GeogCS(semi_major_axis=1.23) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_0(test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 1) - self._check_key('scaleFactorOfRadiusOfSphericalEarth', 0) - self._check_key('scaledValueOfRadiusOfSphericalEarth', 1.23) - - def test__shape_of_earth_flattened(self): - cs = GeogCS(semi_major_axis=1.456, - semi_minor_axis=1.123) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_0(test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 7) - self._check_key('scaleFactorOfEarthMajorAxis', 0) - self._check_key('scaledValueOfEarthMajorAxis', 1.456) - self._check_key('scaleFactorOfEarthMinorAxis', 0) - self._check_key('scaledValueOfEarthMinorAxis', 1.123) - - def test__grid_shape(self): - test_cube = self._make_test_cube(x_points=np.arange(13), - y_points=np.arange(6)) - grid_definition_template_0(test_cube, self.mock_grib) - self._check_key('Ni', 13) - self._check_key('Nj', 6) - - def test__grid_points(self): - test_cube = self._make_test_cube( - x_points=[1, 3, 5, 7], y_points=[4, 9]) - grid_definition_template_0(test_cube, self.mock_grib) - self._check_key("longitudeOfFirstGridPoint", 1000000) - self._check_key("longitudeOfLastGridPoint", 7000000) - self._check_key("latitudeOfFirstGridPoint", 4000000) - self._check_key("latitudeOfLastGridPoint", 9000000) - self._check_key("DxInDegrees", 2.0) - self._check_key("DyInDegrees", 5.0) - - def test__scanmode(self): - grid_definition_template_0(self.test_cube, self.mock_grib) - self._check_key('iScansPositively', 1) - self._check_key('jScansPositively', 1) - - def test__scanmode_reverse(self): - test_cube = self._make_test_cube(x_points=np.arange(7, 0, -1)) - grid_definition_template_0(test_cube, self.mock_grib) - self._check_key('iScansPositively', 0) - self._check_key('jScansPositively', 1) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index f8cdf181d4..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_1.py +++ /dev/null @@ -1,131 +0,0 @@ -# (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 -:meth:`iris.fileformats.grib._save_rules.grid_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 - -import numpy as np - -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 - - -class Test(tests.IrisTest, GdtTestMixin): - def setUp(self): - self.default_ellipsoid = GeogCS(PP_DEFAULT_EARTH_RADIUS) - GdtTestMixin.setUp(self) - - def _default_coord_system(self): - # Define an alternate, rotated coordinate system to test. - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - ellipsoid=self.default_ellipsoid) - return cs - - def test__template_number(self): - grid_definition_template_1(self.test_cube, self.mock_grib) - self._check_key('gridDefinitionTemplateNumber', 1) - - def test__shape_of_earth_spherical(self): - ellipsoid = GeogCS(1.23) - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - ellipsoid=ellipsoid) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_1(test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 1) - self._check_key('scaleFactorOfRadiusOfSphericalEarth', 0) - self._check_key('scaledValueOfRadiusOfSphericalEarth', 1.23) - - def test__shape_of_earth_flattened(self): - ellipsoid = GeogCS(semi_major_axis=1.456, semi_minor_axis=1.123) - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - ellipsoid=ellipsoid) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_1(test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 7) - self._check_key('scaleFactorOfEarthMajorAxis', 0) - self._check_key('scaledValueOfEarthMajorAxis', 1.456) - self._check_key('scaleFactorOfEarthMinorAxis', 0) - self._check_key('scaledValueOfEarthMinorAxis', 1.123) - - def test__grid_shape(self): - test_cube = self._make_test_cube(x_points=np.arange(13), - y_points=np.arange(6)) - grid_definition_template_1(test_cube, self.mock_grib) - self._check_key('Ni', 13) - self._check_key('Nj', 6) - - def test__grid_points(self): - test_cube = self._make_test_cube(x_points=[1, 3, 5, 7], - y_points=[4, 9]) - grid_definition_template_1(test_cube, self.mock_grib) - self._check_key("longitudeOfFirstGridPoint", 1000000) - self._check_key("longitudeOfLastGridPoint", 7000000) - self._check_key("latitudeOfFirstGridPoint", 4000000) - self._check_key("latitudeOfLastGridPoint", 9000000) - self._check_key("DxInDegrees", 2.0) - self._check_key("DyInDegrees", 5.0) - - def test__scanmode(self): - grid_definition_template_1(self.test_cube, self.mock_grib) - self._check_key('iScansPositively', 1) - self._check_key('jScansPositively', 1) - - def test__scanmode_reverse(self): - test_cube = self._make_test_cube(x_points=np.arange(7, 0, -1)) - grid_definition_template_1(test_cube, self.mock_grib) - self._check_key('iScansPositively', 0) - self._check_key('jScansPositively', 1) - - def test__rotated_pole(self): - cs = RotatedGeogCS(grid_north_pole_latitude=75.3, - grid_north_pole_longitude=54.321, - ellipsoid=self.default_ellipsoid) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_1(test_cube, self.mock_grib) - self._check_key("latitudeOfSouthernPole", -75300000) - self._check_key("longitudeOfSouthernPole", 234321000) - self._check_key("angleOfRotation", 0) - - def test__fail_rotated_pole_nonstandard_meridian(self): - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - north_pole_grid_longitude=22.5, - ellipsoid=self.default_ellipsoid) - test_cube = self._make_test_cube(cs=cs) - with self.assertRaisesRegexp( - TranslationError, - 'not yet support .* rotated prime meridian.'): - grid_definition_template_1(test_cube, self.mock_grib) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 879d3980d3..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_12.py +++ /dev/null @@ -1,187 +0,0 @@ -# (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 -:meth:`iris.fileformats.grib._save_rules.grid_definition_template_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 before -# importing anything else. -import iris.tests as tests - -import numpy as np - -import iris.coords -from iris.coord_systems import GeogCS, TransverseMercator -from iris.fileformats.grib._save_rules import grid_definition_template_12 -from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin - - -class FakeGribError(Exception): - pass - - -class Test(tests.IrisTest, GdtTestMixin): - def setUp(self): - self.default_ellipsoid = GeogCS(semi_major_axis=6377563.396, - semi_minor_axis=6356256.909) - self.test_cube = self._make_test_cube() - - GdtTestMixin.setUp(self) - - def _make_test_cube(self, cs=None, x_points=None, y_points=None): - # Create a cube with given properties, or minimal defaults. - if cs is None: - cs = self._default_coord_system() - if x_points is None: - x_points = self._default_x_points() - if y_points is None: - y_points = self._default_y_points() - - x_coord = iris.coords.DimCoord(x_points, 'projection_x_coordinate', - units='m', coord_system=cs) - y_coord = iris.coords.DimCoord(y_points, 'projection_y_coordinate', - units='m', coord_system=cs) - test_cube = iris.cube.Cube(np.zeros((len(y_points), len(x_points)))) - test_cube.add_dim_coord(y_coord, 0) - test_cube.add_dim_coord(x_coord, 1) - return test_cube - - def _default_coord_system(self): - # This defines an OSGB coord system. - cs = TransverseMercator(latitude_of_projection_origin=49.0, - longitude_of_central_meridian=-2.0, - false_easting=400000.0, - false_northing=-100000.0, - scale_factor_at_central_meridian=0.9996012717, - ellipsoid=self.default_ellipsoid) - return cs - - def test__template_number(self): - grid_definition_template_12(self.test_cube, self.mock_grib) - self._check_key('gridDefinitionTemplateNumber', 12) - - def test__shape_of_earth(self): - grid_definition_template_12(self.test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 7) - self._check_key('scaleFactorOfEarthMajorAxis', 0) - self._check_key('scaledValueOfEarthMajorAxis', 6377563.396) - self._check_key('scaleFactorOfEarthMinorAxis', 0) - self._check_key('scaledValueOfEarthMinorAxis', 6356256.909) - - def test__grid_shape(self): - test_cube = self._make_test_cube(x_points=np.arange(13), - y_points=np.arange(6)) - grid_definition_template_12(test_cube, self.mock_grib) - self._check_key('Ni', 13) - self._check_key('Nj', 6) - - def test__grid_points_exact(self): - test_cube = self._make_test_cube(x_points=[1, 3, 5, 7], - y_points=[4, 9]) - grid_definition_template_12(test_cube, self.mock_grib) - self._check_key("X1", 100) - self._check_key("X2", 700) - self._check_key("Y1", 400) - self._check_key("Y2", 900) - self._check_key("Di", 200) - self._check_key("Dj", 500) - - def test__grid_points_approx(self): - test_cube = self._make_test_cube(x_points=[1.001, 3.003, 5.005, 7.007], - y_points=[4, 9]) - grid_definition_template_12(test_cube, self.mock_grib) - self._check_key("X1", 100) - self._check_key("X2", 701) - self._check_key("Y1", 400) - self._check_key("Y2", 900) - self._check_key("Di", 200) - self._check_key("Dj", 500) - - def test__negative_grid_points_gribapi_broken(self): - self.mock_gribapi.GribInternalError = FakeGribError - - # Force the test to run the signed int --> unsigned int workaround. - def set(grib, key, value): - if key in ["X1", "X2", "Y1", "Y2"] and value < 0: - raise self.mock_gribapi.GribInternalError() - grib.keys[key] = value - self.mock_gribapi.grib_set = set - - test_cube = self._make_test_cube(x_points=[-1, 1, 3, 5, 7], - y_points=[-4, 9]) - grid_definition_template_12(test_cube, self.mock_grib) - self._check_key("X1", 0x80000064) - self._check_key("X2", 700) - self._check_key("Y1", 0x80000190) - self._check_key("Y2", 900) - - def test__negative_grid_points_gribapi_fixed(self): - test_cube = self._make_test_cube(x_points=[-1, 1, 3, 5, 7], - y_points=[-4, 9]) - grid_definition_template_12(test_cube, self.mock_grib) - self._check_key("X1", -100) - self._check_key("X2", 700) - self._check_key("Y1", -400) - self._check_key("Y2", 900) - - def test__template_specifics(self): - grid_definition_template_12(self.test_cube, self.mock_grib) - self._check_key("latitudeOfReferencePoint", 49000000.0) - self._check_key("longitudeOfReferencePoint", -2000000.0) - self._check_key("XR", 40000000.0) - self._check_key("YR", -10000000.0) - - def test__scale_factor_gribapi_broken(self): - # GRIBAPI expects a signed int for scaleFactorAtReferencePoint - # but it should accept a float, so work around this. - # See https://software.ecmwf.int/issues/browse/SUP-1100 - - def get_native_type(grib, key): - assert key == "scaleFactorAtReferencePoint" - return int - self.mock_gribapi.grib_get_native_type = get_native_type - grid_definition_template_12(self.test_cube, self.mock_grib) - self._check_key("scaleFactorAtReferencePoint", 1065346526) - - def test__scale_factor_gribapi_fixed(self): - - def get_native_type(grib, key): - assert key == "scaleFactorAtReferencePoint" - return float - self.mock_gribapi.grib_get_native_type = get_native_type - grid_definition_template_12(self.test_cube, self.mock_grib) - self._check_key("scaleFactorAtReferencePoint", 0.9996012717) - - def test__scanmode(self): - grid_definition_template_12(self.test_cube, self.mock_grib) - self._check_key('iScansPositively', 1) - self._check_key('jScansPositively', 1) - - def test__scanmode_reverse(self): - test_cube = self._make_test_cube(x_points=np.arange(7, 0, -1)) - grid_definition_template_12(test_cube, self.mock_grib) - self._check_key('iScansPositively', 0) - self._check_key('jScansPositively', 1) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_30.py b/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_30.py deleted file mode 100644 index d348fdd202..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_30.py +++ /dev/null @@ -1,137 +0,0 @@ -# (C) British Crown Copyright 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 -:meth:`iris.fileformats.grib._save_rules.grid_definition_template_30`. - -""" - -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.coords -from iris.coord_systems import GeogCS, LambertConformal -from iris.fileformats.grib._save_rules import grid_definition_template_30 -from iris.tests.unit.fileformats.grib.save_rules import GdtTestMixin - - -class FakeGribError(Exception): - pass - - -class Test(tests.IrisTest, GdtTestMixin): - def setUp(self): - self.default_ellipsoid = GeogCS(semi_major_axis=6377563.396, - semi_minor_axis=6356256.909) - self.test_cube = self._make_test_cube() - - GdtTestMixin.setUp(self) - - def _make_test_cube(self, cs=None, x_points=None, y_points=None): - # Create a cube with given properties, or minimal defaults. - if cs is None: - cs = self._default_coord_system() - if x_points is None: - x_points = self._default_x_points() - if y_points is None: - y_points = self._default_y_points() - - x_coord = iris.coords.DimCoord(x_points, 'projection_x_coordinate', - units='m', coord_system=cs) - y_coord = iris.coords.DimCoord(y_points, 'projection_y_coordinate', - units='m', coord_system=cs) - test_cube = iris.cube.Cube(np.zeros((len(y_points), len(x_points)))) - test_cube.add_dim_coord(y_coord, 0) - test_cube.add_dim_coord(x_coord, 1) - return test_cube - - def _default_coord_system(self): - return LambertConformal(central_lat=39.0, central_lon=-96.0, - false_easting=0.0, false_northing=0.0, - secant_latitudes=(33, 45), - ellipsoid=self.default_ellipsoid) - - def test__template_number(self): - grid_definition_template_30(self.test_cube, self.mock_grib) - self._check_key('gridDefinitionTemplateNumber', 30) - - def test__shape_of_earth(self): - grid_definition_template_30(self.test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 7) - self._check_key('scaleFactorOfEarthMajorAxis', 0) - self._check_key('scaledValueOfEarthMajorAxis', 6377563.396) - self._check_key('scaleFactorOfEarthMinorAxis', 0) - self._check_key('scaledValueOfEarthMinorAxis', 6356256.909) - - def test__grid_shape(self): - test_cube = self._make_test_cube(x_points=np.arange(13), - y_points=np.arange(6)) - grid_definition_template_30(test_cube, self.mock_grib) - self._check_key('Nx', 13) - self._check_key('Ny', 6) - - def test__grid_points(self): - test_cube = self._make_test_cube(x_points=[1e6, 3e6, 5e6, 7e6], - y_points=[4e6, 9e6]) - grid_definition_template_30(test_cube, self.mock_grib) - self._check_key("latitudeOfFirstGridPoint", 71676530) - self._check_key("longitudeOfFirstGridPoint", 287218188) - self._check_key("Dx", 2e9) - self._check_key("Dy", 5e9) - - def test__template_specifics(self): - grid_definition_template_30(self.test_cube, self.mock_grib) - self._check_key("LaD", 39e6) - self._check_key("LoV", 264e6) - self._check_key("Latin1", 33e6) - self._check_key("Latin2", 45e6) - self._check_key("latitudeOfSouthernPole", 0) - self._check_key("longitudeOfSouthernPole", 0) - - def test__scanmode(self): - grid_definition_template_30(self.test_cube, self.mock_grib) - self._check_key('iScansPositively', 1) - self._check_key('jScansPositively', 1) - - def test__scanmode_reverse(self): - test_cube = self._make_test_cube(x_points=np.arange(7e6, 0, -1e6)) - grid_definition_template_30(test_cube, self.mock_grib) - self._check_key('iScansPositively', 0) - self._check_key('jScansPositively', 1) - - def test_projection_centre(self): - grid_definition_template_30(self.test_cube, self.mock_grib) - self._check_key("projectionCentreFlag", 0) - - def test_projection_centre_south_pole(self): - cs = LambertConformal(central_lat=39.0, central_lon=-96.0, - false_easting=0.0, false_northing=0.0, - secant_latitudes=(-33, -45), - ellipsoid=self.default_ellipsoid) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_30(test_cube, self.mock_grib) - self._check_key("projectionCentreFlag", 1) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 5b55f1f4d7..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_grid_definition_template_5.py +++ /dev/null @@ -1,148 +0,0 @@ -# (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 -:meth:`iris.fileformats.grib._save_rules.grid_definition_template_5`. - -""" - -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 - -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 - - -class Test(tests.IrisTest, GdtTestMixin): - def setUp(self): - GdtTestMixin.setUp(self) - - def _default_coord_system(self): - # Define an alternate, rotated coordinate system to test." - self.default_ellipsoid = GeogCS(PP_DEFAULT_EARTH_RADIUS) - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - ellipsoid=self.default_ellipsoid) - return cs - - def _default_x_points(self): - # Define an irregular x-coord, to force template 3.5 instead of 3.1. - return [1.0, 2.0, 5.0] - - def test__template_number(self): - grid_definition_template_5(self.test_cube, self.mock_grib) - self._check_key('gridDefinitionTemplateNumber', 5) - - def test__shape_of_earth_spherical(self): - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - ellipsoid=GeogCS(52431.0)) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_5(test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 1) - self._check_key('scaleFactorOfRadiusOfSphericalEarth', 0) - self._check_key('scaledValueOfRadiusOfSphericalEarth', 52431.0) - - def test__shape_of_earth_flattened(self): - ellipsoid = GeogCS(semi_major_axis=1456.0, semi_minor_axis=1123.0) - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - ellipsoid=ellipsoid) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_5(test_cube, self.mock_grib) - self._check_key('shapeOfTheEarth', 7) - self._check_key('scaleFactorOfEarthMajorAxis', 0) - self._check_key('scaledValueOfEarthMajorAxis', 1456.0) - self._check_key('scaleFactorOfEarthMinorAxis', 0) - self._check_key('scaledValueOfEarthMinorAxis', 1123.0) - - def test__grid_shape(self): - test_cube = self._make_test_cube(x_points=np.arange(13), - y_points=np.arange(6)) - grid_definition_template_5(test_cube, self.mock_grib) - self._check_key('Ni', 13) - self._check_key('Nj', 6) - - def test__scanmode(self): - grid_definition_template_5(self.test_cube, self.mock_grib) - self._check_key('iScansPositively', 1) - self._check_key('jScansPositively', 1) - - def test__scanmode_reverse(self): - test_cube = self._make_test_cube(y_points=[5.0, 2.0]) - grid_definition_template_5(test_cube, self.mock_grib) - self._check_key('iScansPositively', 1) - self._check_key('jScansPositively', 0) - - def test__rotated_pole(self): - cs = RotatedGeogCS(grid_north_pole_latitude=75.3, - grid_north_pole_longitude=54.321, - ellipsoid=self.default_ellipsoid) - test_cube = self._make_test_cube(cs=cs) - grid_definition_template_5(test_cube, self.mock_grib) - self._check_key("latitudeOfSouthernPole", -75300000) - self._check_key("longitudeOfSouthernPole", 234321000) - self._check_key("angleOfRotation", 0) - - def test__fail_rotated_pole_nonstandard_meridian(self): - cs = RotatedGeogCS(grid_north_pole_latitude=90.0, - grid_north_pole_longitude=0.0, - north_pole_grid_longitude=22.5, - ellipsoid=self.default_ellipsoid) - test_cube = self._make_test_cube(cs=cs) - with self.assertRaisesRegexp( - TranslationError, - 'not yet support .* rotated prime meridian.'): - grid_definition_template_5(test_cube, self.mock_grib) - - def test__grid_points(self): - x_floats = np.array([11.0, 12.0, 167.0]) - # TODO: reduce Y to 2 points, when gribapi nx=ny limitation is gone. - y_floats = np.array([20.0, 21.0, 22.0]) - test_cube = self._make_test_cube(x_points=x_floats, y_points=y_floats) - grid_definition_template_5(test_cube, self.mock_grib) - x_longs = np.array(np.round(1e6 * x_floats), dtype=int) - y_longs = np.array(np.round(1e6 * y_floats), dtype=int) - self._check_key("longitudes", x_longs) - self._check_key("latitudes", y_longs) - - def test__true_winds_orientation(self): - self.test_cube.rename('eastward_wind') - grid_definition_template_5(self.test_cube, self.mock_grib) - flags = self.mock_grib.keys['resolutionAndComponentFlags'] & 255 - flags_expected = 0b00000000 - self.assertEqual(flags, flags_expected) - - def test__grid_winds_orientation(self): - self.test_cube.rename('x_wind') - grid_definition_template_5(self.test_cube, self.mock_grib) - flags = self.mock_grib.keys['resolutionAndComponentFlags'] & 255 - flags_expected = 0b00001000 - self.assertEqual(flags, flags_expected) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 728c82d157..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_identification.py +++ /dev/null @@ -1,82 +0,0 @@ -# (C) British Crown Copyright 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 `iris.fileformats.grib.grib_save_rules.identification`.""" - -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 iris.fileformats.grib -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' - - -class Test(TestGribSimple): - @tests.skip_data - def test_no_realization(self): - cube = stock.simple_pp() - grib = mock.Mock() - mock_gribapi = mock.Mock(spec=gribapi) - with mock.patch(GRIB_API, mock_gribapi): - identification(cube, grib) - - mock_gribapi.assert_has_calls( - [mock.call.grib_set_long(grib, "typeOfProcessedData", 2)]) - - @tests.skip_data - def test_realization_0(self): - cube = stock.simple_pp() - realisation = iris.coords.AuxCoord((0,), standard_name='realization', - units='1') - cube.add_aux_coord(realisation) - - grib = mock.Mock() - mock_gribapi = mock.Mock(spec=gribapi) - with mock.patch(GRIB_API, mock_gribapi): - identification(cube, grib) - - mock_gribapi.assert_has_calls( - [mock.call.grib_set_long(grib, "typeOfProcessedData", 3)]) - - @tests.skip_data - def test_realization_n(self): - cube = stock.simple_pp() - realisation = iris.coords.AuxCoord((2,), standard_name='realization', - units='1') - cube.add_aux_coord(realisation) - - grib = mock.Mock() - mock_gribapi = mock.Mock(spec=gribapi) - with mock.patch(GRIB_API, mock_gribapi): - identification(cube, grib) - - mock_gribapi.assert_has_calls( - [mock.call.grib_set_long(grib, "typeOfProcessedData", 4)]) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index 7c6b49007c..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_1.py +++ /dev/null @@ -1,76 +0,0 @@ -# (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 -: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 -import mock - -from iris.coords import DimCoord -from iris.fileformats.grib._save_rules import product_definition_template_1 -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 deleted file mode 100644 index 694562c3dd..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_10.py +++ /dev/null @@ -1,74 +0,0 @@ -# (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 -import mock - -from iris.coords import DimCoord -from iris.fileformats.grib._save_rules import product_definition_template_10 -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 deleted file mode 100644 index d40eeb9676..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_11.py +++ /dev/null @@ -1,68 +0,0 @@ -# (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_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 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_11 -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(23, 'time', bounds=[0, 100], - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord) - coord = DimCoord(4, 'realization', units='1') - self.cube.add_aux_coord(coord) - - @mock.patch.object(gribapi, 'grib_set') - def test_realization(self, mock_set): - cube = self.cube - cell_method = CellMethod(method='sum', coords=['time']) - cube.add_cell_method(cell_method) - - product_definition_template_11(cube, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - "productDefinitionTemplateNumber", 11) - mock_set.assert_any_call(mock.sentinel.grib, - "perturbationNumber", 4) - mock_set.assert_any_call(mock.sentinel.grib, - "numberOfForecastsInEnsemble", 255) - mock_set.assert_any_call(mock.sentinel.grib, - "typeOfEnsembleForecast", 255) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index dbbef7d0e3..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_40.py +++ /dev/null @@ -1,61 +0,0 @@ -# (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_40` - -""" - -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_40 -from iris.tests import mock -import iris.tests.stock as stock - - -class TestChemicalConstituentIdentifier(tests.IrisTest): - def setUp(self): - self.cube = stock.lat_lon_cube() - # Rename cube to avoid warning about unknown discipline/parameter. - self.cube.rename('atmosphere_mole_content_of_ozone') - coord = DimCoord(24, 'time', - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord) - self.cube.attributes['WMO_constituent_type'] = 0 - - @mock.patch.object(gribapi, 'grib_set') - def test_constituent_type(self, mock_set): - cube = self.cube - - product_definition_template_40(cube, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - "productDefinitionTemplateNumber", 40) - mock_set.assert_any_call(mock.sentinel.grib, - "constituentType", 0) - - -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 deleted file mode 100644 index 3fbe619489..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_product_definition_template_8.py +++ /dev/null @@ -1,60 +0,0 @@ -# (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_8` - -""" - -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_8 -from iris.tests import mock -import iris.tests.stock as stock - - -class TestProductDefinitionIdentifier(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(23, 'time', bounds=[0, 100], - units=Unit('days since epoch', calendar='standard')) - self.cube.add_aux_coord(coord) - - @mock.patch.object(gribapi, 'grib_set') - def test_product_definition(self, mock_set): - cube = self.cube - cell_method = CellMethod(method='sum', coords=['time']) - cube.add_cell_method(cell_method) - - product_definition_template_8(cube, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - "productDefinitionTemplateNumber", 8) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index a2dcfeddd2..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_reference_time.py +++ /dev/null @@ -1,66 +0,0 @@ -# (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 `iris.fileformats.grib.grib_save_rules.reference_time`. - -""" - -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 - -from iris.fileformats.grib import load_cubes -from iris.fileformats.grib._save_rules import reference_time -from iris.tests import mock - - -class Test(tests.IrisTest): - def _test(self, cube): - grib = mock.Mock() - mock_gribapi = mock.Mock(spec=gribapi) - with mock.patch('iris.fileformats.grib._save_rules.gribapi', - mock_gribapi): - reference_time(cube, grib) - - mock_gribapi.assert_has_calls( - [mock.call.grib_set_long(grib, "significanceOfReferenceTime", 1), - 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. - fname = tests.get_data_path(('GRIB', 'global_t', 'global.grib2')) - [cube] = load_cubes(fname) - cube.remove_coord("forecast_period") - self._test(cube) - - -if __name__ == "__main__": - tests.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 deleted file mode 100644 index da4ac29238..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_fixed_surfaces.py +++ /dev/null @@ -1,82 +0,0 @@ -# (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.set_fixed_surfaces`. - -""" - -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.coords -import iris.cube -from iris.fileformats.grib._save_rules import set_fixed_surfaces - - -class Test(tests.IrisTest): - def test_bounded_altitude_feet(self): - cube = iris.cube.Cube([0]) - cube.add_aux_coord(iris.coords.AuxCoord( - 1500.0, long_name='altitude', units='ft', - bounds=np.array([1000.0, 2000.0]))) - grib = gribapi.grib_new_from_samples("GRIB2") - set_fixed_surfaces(cube, grib) - self.assertEqual( - gribapi.grib_get_double(grib, "scaledValueOfFirstFixedSurface"), - 304.0) - self.assertEqual( - gribapi.grib_get_double(grib, "scaledValueOfSecondFixedSurface"), - 609.0) - self.assertEqual( - gribapi.grib_get_long(grib, "typeOfFirstFixedSurface"), - 102) - self.assertEqual( - gribapi.grib_get_long(grib, "typeOfSecondFixedSurface"), - 102) - - def test_theta_level(self): - cube = iris.cube.Cube([0]) - cube.add_aux_coord(iris.coords.AuxCoord( - 230.0, standard_name='air_potential_temperature', - units='K', attributes={'positive': 'up'}, - bounds=np.array([220.0, 240.0]))) - grib = gribapi.grib_new_from_samples("GRIB2") - set_fixed_surfaces(cube, grib) - self.assertEqual( - gribapi.grib_get_double(grib, "scaledValueOfFirstFixedSurface"), - 220.0) - self.assertEqual( - gribapi.grib_get_double(grib, "scaledValueOfSecondFixedSurface"), - 240.0) - self.assertEqual( - gribapi.grib_get_long(grib, "typeOfFirstFixedSurface"), - 107) - self.assertEqual( - gribapi.grib_get_long(grib, "typeOfSecondFixedSurface"), - 107) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index a94df1572a..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_increment.py +++ /dev/null @@ -1,99 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Unit tests for -:func:`iris.fileformats.grib._save_rules.set_time_increment` - -""" - -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 - -from iris.coords import CellMethod -from iris.fileformats.grib._save_rules import set_time_increment -from iris.tests import mock - - -class Test(tests.IrisTest): - @mock.patch.object(gribapi, 'grib_set') - def test_no_intervals(self, mock_set): - cell_method = CellMethod('sum', 'time') - set_time_increment(cell_method, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 255) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 0) - - @mock.patch.object(gribapi, 'grib_set') - def test_area(self, mock_set): - cell_method = CellMethod('sum', 'area', '25 km') - set_time_increment(cell_method, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 255) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 0) - - @mock.patch.object(gribapi, 'grib_set') - def test_multiple_intervals(self, mock_set): - cell_method = CellMethod('sum', 'time', ('1 hour', '24 hour')) - set_time_increment(cell_method, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 255) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 0) - - @mock.patch.object(gribapi, 'grib_set') - def test_hr(self, mock_set): - cell_method = CellMethod('sum', 'time', '23 hr') - set_time_increment(cell_method, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 1) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 23) - - @mock.patch.object(gribapi, 'grib_set') - def test_hour(self, mock_set): - cell_method = CellMethod('sum', 'time', '24 hour') - set_time_increment(cell_method, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 1) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 24) - - @mock.patch.object(gribapi, 'grib_set') - def test_hours(self, mock_set): - cell_method = CellMethod('sum', 'time', '25 hours') - set_time_increment(cell_method, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 1) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 25) - - @mock.patch.object(gribapi, 'grib_set') - def test_fractional_hours(self, mock_set): - cell_method = CellMethod('sum', 'time', '25.9 hours') - with mock.patch('warnings.warn') as warn: - set_time_increment(cell_method, mock.sentinel.grib) - warn.assert_called_once_with('Truncating floating point timeIncrement ' - '25.9 to integer value 25') - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeIncrement', 1) - mock_set.assert_any_call(mock.sentinel.grib, 'timeIncrement', 25) - - -if __name__ == "__main__": - tests.main() 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 deleted file mode 100644 index e1a9d8f1a5..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/save_rules/test_set_time_range.py +++ /dev/null @@ -1,108 +0,0 @@ -# (C) British Crown Copyright 2014 - 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 . -""" -Unit tests for -:func:`iris.fileformats.grib._save_rules.set_time_range` - -""" - -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 warnings - -from cf_units import Unit -import gribapi - -from iris.coords import DimCoord -from iris.fileformats.grib._save_rules import set_time_range -from iris.tests import mock - - -class Test(tests.IrisTest): - def setUp(self): - self.coord = DimCoord(0, 'time', - units=Unit('hours since epoch', - calendar='standard')) - - def test_no_bounds(self): - with self.assertRaisesRegexp(ValueError, 'Expected time coordinate ' - 'with two bounds, got 0 bounds'): - set_time_range(self.coord, mock.sentinel.grib) - - def test_three_bounds(self): - self.coord.bounds = [0, 1, 2] - with self.assertRaisesRegexp(ValueError, 'Expected time coordinate ' - 'with two bounds, got 3 bounds'): - set_time_range(self.coord, mock.sentinel.grib) - - def test_non_scalar(self): - coord = DimCoord([0, 1], 'time', bounds=[[0, 1], [1, 2]], - units=Unit('hours since epoch', calendar='standard')) - with self.assertRaisesRegexp(ValueError, 'Expected length one time ' - 'coordinate, got 2 points'): - set_time_range(coord, mock.sentinel.grib) - - @mock.patch.object(gribapi, 'grib_set') - def test_hours(self, mock_set): - lower = 10 - upper = 20 - self.coord.bounds = [lower, upper] - set_time_range(self.coord, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeRange', 1) - mock_set.assert_any_call(mock.sentinel.grib, - 'lengthOfTimeRange', upper - lower) - - @mock.patch.object(gribapi, 'grib_set') - def test_days(self, mock_set): - lower = 4 - upper = 6 - self.coord.bounds = [lower, upper] - self.coord.units = Unit('days since epoch', calendar='standard') - set_time_range(self.coord, mock.sentinel.grib) - mock_set.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeRange', 1) - mock_set.assert_any_call(mock.sentinel.grib, - 'lengthOfTimeRange', - (upper - lower) * 24) - - @mock.patch.object(gribapi, 'grib_set') - def test_fractional_hours(self, mock_set_long): - lower = 10.0 - upper = 20.9 - self.coord.bounds = [lower, upper] - with warnings.catch_warnings(record=True) as warn: - warnings.simplefilter("always") - set_time_range(self.coord, mock.sentinel.grib) - self.assertEqual(len(warn), 1) - msg = 'Truncating floating point lengthOfTimeRange 10\.8?9+ ' \ - 'to integer value 10' - six.assertRegex(self, str(warn[0].message), msg) - mock_set_long.assert_any_call(mock.sentinel.grib, - 'indicatorOfUnitForTimeRange', 1) - mock_set_long.assert_any_call(mock.sentinel.grib, - 'lengthOfTimeRange', int(upper - lower)) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py b/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py deleted file mode 100644 index 696b5c8834..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test_GribWrapper.py +++ /dev/null @@ -1,141 +0,0 @@ -# (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 `iris.fileformats.grib.GribWrapper` class. - -""" - -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 - -from iris._lazy_data import as_concrete_data, is_lazy_data -from iris.exceptions import TranslationError -from iris.fileformats.grib import GribWrapper -import iris.fileformats.grib as grib -from iris.tests import mock - -_message_length = 1000 - - -def _mock_grib_get_long(grib_message, key): - lookup = dict(totalLength=_message_length, - numberOfValues=200, - jPointsAreConsecutive=0, - Ni=20, - Nj=10, - edition=1) - try: - result = lookup[key] - except KeyError: - msg = 'Mock grib_get_long unknown key: {!r}'.format(key) - raise AttributeError(msg) - return result - - -def _mock_grib_get_string(grib_message, key): - return grib_message - - -def _mock_grib_get_native_type(grib_message, key): - result = int - if key == 'gridType': - result = str - 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) - - -@tests.skip_data -class Test_deferred_data(tests.IrisTest): - def test_regular_data(self): - filename = tests.get_data_path(('GRIB', 'gaussian', - 'regular_gg.grib1')) - messages = list(grib._load_generate(filename)) - self.assertTrue(is_lazy_data(messages[0]._data)) - - def test_reduced_data(self): - filename = tests.get_data_path(('GRIB', 'reduced', - 'reduced_ll.grib1')) - messages = list(grib._load_generate(filename)) - self.assertTrue(is_lazy_data(messages[0]._data)) - - -class Test_deferred_proxy_args(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) - tell_tale = np.arange(1, 5) * _message_length - self.expected = tell_tale - _message_length - self.grib_fh = mock.Mock(tell=mock.Mock(side_effect=tell_tale)) - self.dtype = np.float64 - self.path = self.grib_fh.name - self.lookup = _mock_grib_get_long - - def test_regular_proxy_args(self): - grib_message = 'regular_ll' - shape = (self.lookup(grib_message, 'Nj'), - self.lookup(grib_message, 'Ni')) - for offset in self.expected: - with mock.patch('iris.fileformats.grib.GribDataProxy') as mock_gdp: - gw = GribWrapper(grib_message, self.grib_fh) - mock_gdp.assert_called_once_with(shape, self.dtype, - self.path, offset) - - def test_reduced_proxy_args(self): - grib_message = 'reduced_gg' - shape = (self.lookup(grib_message, 'numberOfValues')) - for offset in self.expected: - with mock.patch('iris.fileformats.grib.GribDataProxy') as mock_gdp: - gw = GribWrapper(grib_message, self.grib_fh) - mock_gdp.assert_called_once_with((shape,), self.dtype, - self.path, offset) - - -if __name__ == '__main__': - tests.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 deleted file mode 100644 index 98306ca749..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test__load_generate.py +++ /dev/null @@ -1,79 +0,0 @@ -# (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 - -from iris.exceptions import TranslationError -from iris.fileformats.grib import _load_generate, GribWrapper -from iris.fileformats.grib.message import GribMessage -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_load_cubes.py b/lib/iris/tests/unit/fileformats/grib/test_load_cubes.py deleted file mode 100644 index eb39d79dbf..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test_load_cubes.py +++ /dev/null @@ -1,65 +0,0 @@ -# (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 `iris.fileformats.grib.load_cubes` 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 -import iris.fileformats.grib -from iris.fileformats.grib import load_cubes -from iris.fileformats.rules import Loader -from iris.tests import mock - - -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) - rules_load.assert_called_once_with(files, callback, loader) - self.assertIs(result, expected_result) - - -@tests.skip_data -class Test_load_cubes(tests.IrisTest): - - def test_reduced_raw(self): - # Loading a GRIB message defined on a reduced grid without - # interpolating to a regular grid. - gribfile = tests.get_data_path( - ("GRIB", "reduced", "reduced_gg.grib2")) - grib_generator = load_cubes(gribfile) - self.assertCML(next(grib_generator)) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/grib/test_save_grib2.py b/lib/iris/tests/unit/fileformats/grib/test_save_grib2.py deleted file mode 100644 index d922331a7a..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test_save_grib2.py +++ /dev/null @@ -1,64 +0,0 @@ -# (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 deleted file mode 100644 index 637bdff352..0000000000 --- a/lib/iris/tests/unit/fileformats/grib/test_save_messages.py +++ /dev/null @@ -1,73 +0,0 @@ -# (C) British Crown Copyright 2015 - 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_messages` 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 gribapi - -import iris.fileformats.grib -from iris.tests import mock - - -class TestSaveMessages(tests.IrisTest): - def setUp(self): - # Create a test object to stand in for a real PPField. - self.grib_message = gribapi.grib_new_from_samples("GRIB2") - - def test_save(self): - if six.PY3: - open_func = 'builtins.open' - else: - open_func = '__builtin__.open' - m = mock.mock_open() - with mock.patch(open_func, m, create=True): - # sending a MagicMock object to gribapi raises an AssertionError - # as the gribapi code does a type check - # this is deemed acceptable within the scope of this unit test - with self.assertRaises((AssertionError, TypeError)): - 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): - if six.PY3: - open_func = 'builtins.open' - else: - open_func = '__builtin__.open' - m = mock.mock_open() - with mock.patch(open_func, m, create=True): - # sending a MagicMock object to gribapi raises an AssertionError - # as the gribapi code does a type check - # this is deemed acceptable within the scope of this unit test - with self.assertRaises((AssertionError, TypeError)): - iris.fileformats.grib.save_messages( - [self.grib_message], 'foo.grib2', append=True) - self.assertTrue(mock.call('foo.grib2', 'ab') in m.mock_calls) - - -if __name__ == "__main__": - tests.main()