Skip to content

Commit

Permalink
Product Definition Template 32 (synthetic satellite data) (#77)
Browse files Browse the repository at this point in the history
* Product definition template 32
  • Loading branch information
djkirkham authored and marqh committed Mar 30, 2017
1 parent f82e491 commit 0981f32
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 113 deletions.
137 changes: 87 additions & 50 deletions iris_grib/_load_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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']

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
32 changes: 22 additions & 10 deletions iris_grib/tests/unit/load_convert/test_generating_process.py
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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__':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -32,24 +32,21 @@
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


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
Expand All @@ -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__':
Expand Down
Loading

0 comments on commit 0981f32

Please sign in to comment.