diff --git a/iris_grib/_load_convert.py b/iris_grib/_load_convert.py
index b499759d4..b85dbd36c 100644
--- a/iris_grib/_load_convert.py
+++ b/iris_grib/_load_convert.py
@@ -1630,14 +1630,38 @@ def coord_timedelta(coord, value):
return coord
-def generating_process(section):
+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.')
- warnings.warn('Unable to translate forecast generating '
- 'process identifier.')
+ if include_forecast_process:
+ warnings.warn('Unable to translate forecast generating '
+ 'process identifier.')
def data_cutoff(hoursAfterDataCutoff, minutesAfterDataCutoff):
@@ -1781,28 +1805,7 @@ def product_definition_template_0(section, metadata, rt_coord):
data_cutoff(section['hoursAfterDataCutoff'],
section['minutesAfterDataCutoff'])
- 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))
+ time_coords(section, metadata, rt_coord)
# Check for vertical coordinates.
vertical_coords(section, metadata)
@@ -2050,29 +2053,7 @@ def product_definition_template_15(section, metadata, frt_coord):
method=cell_method_name)]
-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'.
-
- """
- if options.warn_on_unsupported:
- warnings.warn('Unable to translate type of generating process.')
- warnings.warn('Unable to translate observation generating '
- 'process identifier.')
-
+def satellite_common(section, metadata):
# Number of contributing spectral bands.
NB = section['NB']
@@ -2106,8 +2087,62 @@ def product_definition_template_31(section, metadata, rt_coord):
# Add the central wave number coordinate to the metadata aux coords.
metadata['aux_coords_and_dims'].append((coord, None))
- # Add the observation time coordinate.
- metadata['aux_coords_and_dims'].append((rt_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):
@@ -2193,6 +2228,8 @@ def product_definition_section(section, metadata, discipline, tablesVersion,
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:
diff --git a/iris_grib/tests/unit/load_convert/test_generating_process.py b/iris_grib/tests/unit/load_convert/test_generating_process.py
index b6fa9d3cd..97fac5ac6 100644
--- a/iris_grib/tests/unit/load_convert/test_generating_process.py
+++ b/iris_grib/tests/unit/load_convert/test_generating_process.py
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2014 - 2016, Met Office
+# (C) British Crown Copyright 2014 - 2017, Met Office
#
# This file is part of iris-grib.
#
@@ -38,18 +38,30 @@ def test_nowarn(self):
generating_process(None)
self.assertEqual(self.warn_patch.call_count, 0)
- def test_warn(self):
+ def _check_warnings(self, with_forecast=True):
module = 'iris_grib._load_convert'
self.patch(module + '.options.warn_on_unsupported', True)
- generating_process(None)
+ 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]
- expected_msgs = ['Unable to translate type of generating process',
- 'Unable to translate background generating process',
- 'Unable to translate forecast generating process']
- for expect_msg in expected_msgs:
- matches = [msg for msg in got_msgs if expect_msg in msg]
- self.assertEqual(len(matches), 1)
- got_msgs.remove(matches[0])
+ 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__':
diff --git a/iris_grib/tests/unit/load_convert/test_product_definition_template_10.py b/iris_grib/tests/unit/load_convert/test_product_definition_template_10.py
index c749476bd..0e4de1af3 100644
--- a/iris_grib/tests/unit/load_convert/test_product_definition_template_10.py
+++ b/iris_grib/tests/unit/load_convert/test_product_definition_template_10.py
@@ -31,7 +31,7 @@
from iris.coords import DimCoord
from iris_grib._load_convert import product_definition_template_10
-from iris.tests.unit.fileformats.grib.load_convert import empty_metadata
+from iris_grib.tests.unit.load_convert import empty_metadata
class Test(tests.IrisGribTest):
diff --git a/iris_grib/tests/unit/load_convert/test_product_definition_template_31.py b/iris_grib/tests/unit/load_convert/test_product_definition_template_31.py
index f6611de69..10af448d6 100644
--- a/iris_grib/tests/unit/load_convert/test_product_definition_template_31.py
+++ b/iris_grib/tests/unit/load_convert/test_product_definition_template_31.py
@@ -1,4 +1,4 @@
-# (C) British Crown Copyright 2014 - 2016, Met Office
+# (C) British Crown Copyright 2014 - 2017, Met Office
#
# This file is part of iris-grib.
#
@@ -32,6 +32,7 @@
import warnings
from iris.coords import AuxCoord
+from iris_grib.tests.unit.load_convert import empty_metadata
from iris_grib._load_convert import product_definition_template_31
@@ -39,17 +40,13 @@
class Test(tests.IrisGribTest):
def setUp(self):
self.patch('warnings.warn')
- self.metadata = {'factories': [], 'references': [],
- 'standard_name': None,
- 'long_name': None, 'units': None, 'attributes': None,
- 'cell_methods': [], 'dim_coords_and_dims': [],
- 'aux_coords_and_dims': []}
+ self.satellite_common_patch = self.patch(
+ 'iris_grib._load_convert.satellite_common')
+ self.generating_process_patch = self.patch(
+ 'iris_grib._load_convert.generating_process')
- def _check(self, request_warning=False, value=10, factor=1):
+ def test(self):
# Prepare the arguments.
- def unscale(v, f):
- return v / 10.0 ** f
-
series = mock.sentinel.satelliteSeries
number = mock.sentinel.satelliteNumber
instrument = mock.sentinel.instrumentType
@@ -58,50 +55,19 @@ def unscale(v, f):
'satelliteSeries': series,
'satelliteNumber': number,
'instrumentType': instrument,
- 'scaleFactorOfCentralWaveNumber': factor,
- 'scaledValueOfCentralWaveNumber': value}
- metadata = deepcopy(self.metadata)
- this = 'iris_grib._load_convert.options'
- with mock.patch(this, warn_on_unsupported=request_warning):
- # The call being tested.
- product_definition_template_31(section, metadata, rt_coord)
- # Check the result.
- expected = deepcopy(self.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(unscale(value, factor),
- standard_name=standard_name,
- units='m-1')
- expected['aux_coords_and_dims'].append((coord, None))
- expected['aux_coords_and_dims'].append((rt_coord, None))
- self.assertEqual(metadata, expected)
- if request_warning:
- warn_msgs = [arg[1][0] for arg in warnings.warn.mock_calls]
- expected_msgs = ['type of generating process',
- 'observation generating process identifier']
- 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(request_warning=False)
+ 'scaleFactorOfCentralWaveNumber': 1,
+ 'scaledValueOfCentralWaveNumber': 12}
- def test_pdt_warn(self):
- self._check(request_warning=True)
+ # Call the function.
+ metadata = empty_metadata()
+ product_definition_template_31(section, metadata, rt_coord)
- def test_wavelength_array(self):
- value = np.array([1, 10, 100, 1000])
- for i in range(value.size):
- factor = np.ones(value.shape) * i
- self._check(value=value, factor=factor)
+ # 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__':
diff --git a/iris_grib/tests/unit/load_convert/test_product_definition_template_32.py b/iris_grib/tests/unit/load_convert/test_product_definition_template_32.py
new file mode 100644
index 000000000..e54760a16
--- /dev/null
+++ b/iris_grib/tests/unit/load_convert/test_product_definition_template_32.py
@@ -0,0 +1,85 @@
+# (C) British Crown Copyright 2017, Met Office
+#
+# This file is part of iris-grib.
+#
+# iris-grib 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-grib 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-grib. If not, see .
+"""
+Tests for `iris_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_grib.tests first so that some things can be initialised
+# before importing anything else.
+import iris_grib.tests as tests
+
+from copy import deepcopy
+import mock
+import numpy as np
+import warnings
+
+from iris.coords import AuxCoord, DimCoord
+from iris_grib.tests.unit.load_convert import empty_metadata
+
+from iris_grib._load_convert import product_definition_template_32
+
+MDI = 0xffffffff
+
+
+class Test(tests.IrisGribTest):
+ def setUp(self):
+ self.patch('warnings.warn')
+ self.generating_process_patch = self.patch(
+ 'iris_grib._load_convert.generating_process')
+ self.satellite_common_patch = self.patch(
+ 'iris_grib._load_convert.satellite_common')
+ self.time_coords_patch = self.patch(
+ 'iris_grib._load_convert.time_coords')
+ self.data_cutoff_patch = self.patch(
+ 'iris_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/iris_grib/tests/unit/load_convert/test_satellite_common.py b/iris_grib/tests/unit/load_convert/test_satellite_common.py
new file mode 100644
index 000000000..bb82c94a7
--- /dev/null
+++ b/iris_grib/tests/unit/load_convert/test_satellite_common.py
@@ -0,0 +1,84 @@
+# (C) British Crown Copyright 2014 - 2017, Met Office
+#
+# This file is part of iris-grib.
+#
+# iris-grib 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-grib 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-grib. If not, see .
+"""
+Tests for `iris_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_grib.tests first so that some things can be initialised
+# before importing anything else.
+import iris_grib.tests as tests
+
+from copy import deepcopy
+import mock
+import numpy as np
+import warnings
+
+from iris.coords import AuxCoord
+from iris_grib.tests.unit.load_convert import empty_metadata
+
+from iris_grib._load_convert import satellite_common
+
+
+class Test(tests.IrisGribTest):
+ 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()