Skip to content

Commit 2122d96

Browse files
committed
Merge pull request SciTools#1482 from pp-mo/grib_gdt5_saveload
Fix grib gdt5 load and add save+load roundtrip test.
2 parents 646b9b3 + bf477fc commit 2122d96

File tree

5 files changed

+134
-43
lines changed

5 files changed

+134
-43
lines changed

lib/iris/fileformats/grib/_load_convert.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ def grid_definition_template_4_and_5(section, metadata, y_name, x_name, cs):
546546
547547
"""
548548
# Determine the (variable) units of resolution.
549-
key = 'basicAngleOfTheInitialProductDomain'
549+
key = 'basicAngleOfTheInitialProductionDomain'
550550
basicAngleOfTheInitialProductDomain = section[key]
551551
subdivisionsOfBasicAngle = section['subdivisionsOfBasicAngle']
552552

lib/iris/fileformats/grib/_message.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def data(self):
101101
'unsupported quasi-regular grid.')
102102

103103
template = grid_section['gridDefinitionTemplateNumber']
104-
if template in (0, 1, 90):
104+
if template in (0, 1, 5, 90):
105105
# We can ignore the first two bits (i-neg, j-pos) because
106106
# that is already captured in the coordinate values.
107107
if grid_section['scanningMode'] & 0x3f:
@@ -370,7 +370,8 @@ def _get_key_value(self, key):
370370
vector_keys = ('codedValues', 'pv', 'satelliteSeries',
371371
'satelliteNumber', 'instrumentType',
372372
'scaleFactorOfCentralWaveNumber',
373-
'scaledValueOfCentralWaveNumber')
373+
'scaledValueOfCentralWaveNumber',
374+
'longitudes', 'latitudes')
374375
if key in vector_keys:
375376
res = gribapi.grib_get_array(self._message_id, key)
376377
elif key == 'bitmap':

lib/iris/tests/integration/test_grib2.py

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from subprocess import check_output
3030

3131
from iris import FUTURE, load_cube, save
32+
from iris.coords import CellMethod
3233
from iris.coord_systems import RotatedGeogCS
3334
from iris.fileformats.pp import EARTH_RADIUS as UM_DEFAULT_EARTH_RADIUS
3435
from iris.util import is_regular
@@ -100,9 +101,10 @@ def test_cell_method(self):
100101
self.assertEqual(cell_method.coord_names, ('time',))
101102

102103

103-
class TestSaveGdt5(tests.IrisTest):
104-
def test_simple(self):
105-
# Fetch some sample UKV data (rotated variable grid)
104+
@tests.skip_data
105+
class TestGDT5(tests.IrisTest):
106+
def test_save_load(self):
107+
# Load sample UKV data (variable-resolution rotated grid).
106108
path = tests.get_data_path(('PP', 'ukV1', 'ukVpmslont.pp'))
107109
cube = load_cube(path)
108110

@@ -123,29 +125,92 @@ def test_simple(self):
123125
self.assertIsInstance(x_coord.coord_system, RotatedGeogCS)
124126
self.assertFalse(is_regular(x_coord))
125127

126-
# Write the data to a temporary file, and capture dump output.
127-
# TODO: replace the dump with an Iris load, when we support loading.
128+
# Write to temporary file, check grib_dump output, and load back in.
128129
with self.temp_filename('ukv_sample.grib2') as temp_file_path:
129130
save(cube, temp_file_path)
130-
# Get a dump of the output.
131+
132+
# Get a grib_dump of the output file.
131133
dump_text = check_output(('grib_dump -O -wcount=1 ' +
132134
temp_file_path),
133135
shell=True)
134-
# Check that various aspects of the result are as expected.
135-
expect_strings = (
136-
'editionNumber = 2',
137-
'gridDefinitionTemplateNumber = 5',
138-
'Ni = {:d}'.format(nn),
139-
'Nj = {:d}'.format(nn),
140-
'shapeOfTheEarth = 1',
141-
'scaledValueOfRadiusOfSphericalEarth = {:d}'.format(
142-
int(UM_DEFAULT_EARTH_RADIUS)),
143-
'resolutionAndComponentFlags = 0',
144-
'latitudeOfSouthernPole = -37500000',
145-
'longitudeOfSouthernPole = 357500000',
146-
'angleOfRotation = 0')
147-
for expect in expect_strings:
148-
self.assertIn(expect, dump_text)
136+
137+
# Check that various aspects of the saved file are as expected.
138+
expect_strings = (
139+
'editionNumber = 2',
140+
'gridDefinitionTemplateNumber = 5',
141+
'Ni = {:d}'.format(cube.shape[-1]),
142+
'Nj = {:d}'.format(cube.shape[-2]),
143+
'shapeOfTheEarth = 1',
144+
'scaledValueOfRadiusOfSphericalEarth = {:d}'.format(
145+
int(UM_DEFAULT_EARTH_RADIUS)),
146+
'resolutionAndComponentFlags = 0',
147+
'latitudeOfSouthernPole = -37500000',
148+
'longitudeOfSouthernPole = 357500000',
149+
'angleOfRotation = 0')
150+
for expect in expect_strings:
151+
self.assertIn(expect, dump_text)
152+
153+
# Load the Grib file back into a new cube.
154+
with FUTURE.context(strict_grib_load=True):
155+
cube_loaded_from_saved = load_cube(temp_file_path)
156+
# Also load data, before the temporary file gets deleted.
157+
cube_loaded_from_saved.data
158+
159+
# The re-loaded result will not match the original in every respect:
160+
# * cube attributes are discarded
161+
# * horizontal coordinates are rounded to an integer representation
162+
# * bounds on horizontal coords are lost
163+
# Thus the following "equivalence tests" are rather piecemeal..
164+
165+
# Check those re-loaded properties which should match the original.
166+
for test_cube in (cube, cube_loaded_from_saved):
167+
self.assertEqual(test_cube.standard_name,
168+
'air_pressure_at_sea_level')
169+
self.assertEqual(test_cube.units, 'Pa')
170+
self.assertEqual(test_cube.shape, (744, 744))
171+
self.assertEqual(test_cube.cell_methods, ())
172+
173+
# Check no cube attributes on the re-loaded cube.
174+
# Note: this does *not* match the original, but is as expected.
175+
self.assertEqual(cube_loaded_from_saved.attributes, {})
176+
177+
# Now remaining to check: coordinates + data...
178+
179+
# Check they have all the same coordinates.
180+
co_names = [coord.name() for coord in cube.coords()]
181+
co_names_reload = [coord.name()
182+
for coord in cube_loaded_from_saved.coords()]
183+
self.assertEqual(sorted(co_names_reload), sorted(co_names))
184+
185+
# Check all the coordinates.
186+
for coord_name in co_names:
187+
try:
188+
co_orig = cube.coord(coord_name)
189+
co_load = cube_loaded_from_saved.coord(coord_name)
190+
191+
# Check shape.
192+
self.assertEqual(co_load.shape, co_orig.shape,
193+
'Shape of re-loaded "{}" coord is {} '
194+
'instead of {}'.format(coord_name,
195+
co_load.shape,
196+
co_orig.shape))
197+
198+
# Check coordinate points equal, within a tolerance.
199+
self.assertArrayAllClose(co_load.points, co_orig.points,
200+
rtol=1.0e-6)
201+
202+
# Check all coords are unbounded.
203+
# (NOTE: this is not so for the original X and Y coordinates,
204+
# but Grib does not store those bounds).
205+
self.assertIsNone(co_load.bounds)
206+
207+
except AssertionError as err:
208+
self.assertTrue(False,
209+
'Failed on coordinate "{}" : {}'.format(
210+
coord_name, str(err)))
211+
212+
# Check that main data array also matches.
213+
self.assertArrayAllClose(cube.data, cube_loaded_from_saved.data)
149214

150215

151216
if __name__ == '__main__':

lib/iris/tests/unit/fileformats/grib/load_convert/test_grid_definition_template_4_and_5.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def _check(self, section, request_warning,
8888

8989
def test_resolution_default_0(self):
9090
for request_warn in [False, True]:
91-
section = {'basicAngleOfTheInitialProductDomain': 0,
91+
section = {'basicAngleOfTheInitialProductionDomain': 0,
9292
'subdivisionsOfBasicAngle': 0,
9393
'resolutionAndComponentFlags': 0,
9494
'longitudes': self.data * RESOLUTION,
@@ -98,7 +98,7 @@ def test_resolution_default_0(self):
9898

9999
def test_resolution_default_mdi(self):
100100
for request_warn in [False, True]:
101-
section = {'basicAngleOfTheInitialProductDomain': MDI,
101+
section = {'basicAngleOfTheInitialProductionDomain': MDI,
102102
'subdivisionsOfBasicAngle': MDI,
103103
'resolutionAndComponentFlags': 0,
104104
'longitudes': self.data * RESOLUTION,
@@ -109,7 +109,7 @@ def test_resolution_default_mdi(self):
109109
def test_resolution(self):
110110
angle = 10
111111
for request_warn in [False, True]:
112-
section = {'basicAngleOfTheInitialProductDomain': 1,
112+
section = {'basicAngleOfTheInitialProductionDomain': 1,
113113
'subdivisionsOfBasicAngle': angle,
114114
'resolutionAndComponentFlags': 0,
115115
'longitudes': self.data * angle,
@@ -120,7 +120,7 @@ def test_resolution(self):
120120
def test_uv_resolved_warn(self):
121121
angle = 100
122122
for warn in [False, True]:
123-
section = {'basicAngleOfTheInitialProductDomain': 1,
123+
section = {'basicAngleOfTheInitialProductionDomain': 1,
124124
'subdivisionsOfBasicAngle': angle,
125125
'resolutionAndComponentFlags': 0x08,
126126
'longitudes': self.data * angle,
@@ -131,7 +131,7 @@ def test_uv_resolved_warn(self):
131131
def test_j_consecutive(self):
132132
angle = 1000
133133
for request_warn in [False, True]:
134-
section = {'basicAngleOfTheInitialProductDomain': 1,
134+
section = {'basicAngleOfTheInitialProductionDomain': 1,
135135
'subdivisionsOfBasicAngle': angle,
136136
'resolutionAndComponentFlags': 0,
137137
'longitudes': self.data * angle,

lib/iris/tests/unit/fileformats/grib/message/test__GribMessage.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -189,26 +189,51 @@ def test_regular_mode_64_128(self):
189189
self._test(64 | 128)
190190

191191

192+
def _example_section_3(grib_definition_template_number, scanning_mode):
193+
return {'sourceOfGridDefinition': 0,
194+
'numberOfOctectsForNumberOfPoints': 0,
195+
'interpretationOfNumberOfPoints': 0,
196+
'gridDefinitionTemplateNumber': grib_definition_template_number,
197+
'scanningMode': scanning_mode,
198+
'Nj': 3,
199+
'Ni': 4}
200+
201+
192202
class Test_data__grid_template_0(tests.IrisTest, Mixin_data__grid_template):
193203
def section_3(self, scanning_mode):
194-
return {'sourceOfGridDefinition': 0,
195-
'numberOfOctectsForNumberOfPoints': 0,
196-
'interpretationOfNumberOfPoints': 0,
197-
'gridDefinitionTemplateNumber': 0,
198-
'scanningMode': scanning_mode,
199-
'Nj': 3,
200-
'Ni': 4}
204+
return _example_section_3(0, scanning_mode)
205+
206+
207+
class Test_data__grid_template_1(tests.IrisTest, Mixin_data__grid_template):
208+
def section_3(self, scanning_mode):
209+
return _example_section_3(1, scanning_mode)
210+
211+
212+
class Test_data__grid_template_5(tests.IrisTest, Mixin_data__grid_template):
213+
def section_3(self, scanning_mode):
214+
return _example_section_3(5, scanning_mode)
201215

202216

203217
class Test_data__grid_template_90(tests.IrisTest, Mixin_data__grid_template):
204218
def section_3(self, scanning_mode):
205-
return {'sourceOfGridDefinition': 0,
206-
'numberOfOctectsForNumberOfPoints': 0,
207-
'interpretationOfNumberOfPoints': 0,
208-
'gridDefinitionTemplateNumber': 90,
209-
'scanningMode': scanning_mode,
210-
'Ny': 3,
211-
'Nx': 4}
219+
section_3 = _example_section_3(90, scanning_mode)
220+
# Exceptionally, dimensions are 'Nx' + 'Ny' instead of 'Ni' + 'Nj'.
221+
section_3['Nx'] = section_3['Ni']
222+
section_3['Ny'] = section_3['Nj']
223+
del section_3['Ni']
224+
del section_3['Nj']
225+
return section_3
226+
227+
228+
class Test_data__unknown_grid_template(tests.IrisTest):
229+
def test(self):
230+
message = _make_test_message(
231+
{3: _example_section_3(999, 0),
232+
6: SECTION_6_NO_BITMAP,
233+
7: {'codedValues': np.arange(12)}})
234+
with self.assertRaisesRegexp(TranslationError,
235+
'template 999 is not supported'):
236+
data = message.data
212237

213238

214239
if __name__ == '__main__':

0 commit comments

Comments
 (0)