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()