Skip to content

Commit 4b38568

Browse files
committed
Merge pull request SciTools#1475 from rhattersley/grib2-gdt12
Add load support for GRIB2 GDT 12.
2 parents 948e76f + c4675bd commit 4b38568

File tree

8 files changed

+401
-25
lines changed

8 files changed

+401
-25
lines changed

lib/iris/fileformats/grib/_load_convert.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,40 @@ def _hindcast_fix(forecast_time):
214214
return forecast_time
215215

216216

217+
def fixup_float32_from_int32(value):
218+
"""
219+
Workaround for use when reading an IEEE 32-bit floating-point value
220+
which the ECMWF GRIB API has erroneously treated as a 4-byte signed
221+
integer.
222+
223+
"""
224+
# Convert from two's complement to sign-and-magnitude.
225+
# NB. The bit patterns 0x00000000 and 0x80000000 will both be
226+
# returned by the ECMWF GRIB API as an integer 0. Because they
227+
# correspond to positive and negative zero respectively it is safe
228+
# to treat an integer 0 as a positive zero.
229+
if value < 0:
230+
value = 0x80000000 - value
231+
value_as_uint32 = np.array(value, dtype='u4')
232+
value_as_float32 = value_as_uint32.view(dtype='f4')
233+
return float(value_as_float32)
234+
235+
236+
def fixup_int32_from_uint32(value):
237+
"""
238+
Workaround for use when reading a signed, 4-byte integer which the
239+
ECMWF GRIB API has erroneously treated as an unsigned, 4-byte
240+
integer.
241+
242+
NB. This workaround is safe to use with values which are already
243+
treated as signed, 4-byte integers.
244+
245+
"""
246+
if value >= 0x80000000:
247+
value = 0x80000000 - value
248+
return value
249+
250+
217251
###############################################################################
218252
#
219253
# Identification Section 1
@@ -644,6 +678,83 @@ def grid_definition_template_5(section, metadata):
644678
'grid_latitude', 'grid_longitude', cs)
645679

646680

681+
def grid_definition_template_12(section, metadata):
682+
"""
683+
Translate template representing transverse Mercator.
684+
685+
Updates the metadata in-place with the translations.
686+
687+
Args:
688+
689+
* section:
690+
Dictionary of coded key/value pairs from section 3 of the message.
691+
692+
* metadata:
693+
:class:`collections.OrderedDict` of metadata.
694+
695+
"""
696+
major, minor, radius = ellipsoid_geometry(section)
697+
geog_cs = ellipsoid(section['shapeOfTheEarth'], major, minor, radius)
698+
699+
lat = section['latitudeOfReferencePoint'] * _GRID_ACCURACY_IN_DEGREES
700+
lon = section['longitudeOfReferencePoint'] * _GRID_ACCURACY_IN_DEGREES
701+
scale = section['scaleFactorAtReferencePoint']
702+
# Catch bug in ECMWF GRIB API (present at 1.12.1) where the scale
703+
# is treated as a signed, 4-byte integer.
704+
if isinstance(scale, int):
705+
scale = fixup_float32_from_int32(scale)
706+
CM_TO_M = 0.01
707+
easting = section['XR'] * CM_TO_M
708+
northing = section['YR'] * CM_TO_M
709+
cs = icoord_systems.TransverseMercator(lat, lon, easting, northing,
710+
scale, geog_cs)
711+
712+
# Deal with bug in ECMWF GRIB API (present at 1.12.1) where these
713+
# values are treated as unsigned, 4-byte integers.
714+
x1 = fixup_int32_from_uint32(section['x1'])
715+
y1 = fixup_int32_from_uint32(section['y1'])
716+
x2 = fixup_int32_from_uint32(section['x2'])
717+
y2 = fixup_int32_from_uint32(section['y2'])
718+
719+
# Rather unhelpfully this grid definition template seems to be
720+
# overspecified, and thus open to inconsistency.
721+
last_x = x1 + (section['Ni'] - 1) * section['Di']
722+
last_y = y1 + (section['Nj'] - 1) * section['Dj']
723+
if (last_x != x2 or last_y != y2):
724+
raise TranslationError('Inconsistent grid definition')
725+
726+
x1 = x1 * CM_TO_M
727+
dx = section['Di'] * CM_TO_M
728+
x_points = x1 + np.arange(section['Ni']) * dx
729+
y1 = y1 * CM_TO_M
730+
dy = section['Dj'] * CM_TO_M
731+
y_points = y1 + np.arange(section['Nj']) * dy
732+
733+
# This has only been tested with +x/+y scanning, so raise an error
734+
# for other permutations.
735+
scan = scanning_mode(section['scanningMode'])
736+
if scan.i_negative:
737+
raise TranslationError('Unsupported -x scanning')
738+
if not scan.j_positive:
739+
raise TranslationError('Unsupported -y scanning')
740+
741+
# Create the X and Y coordinates.
742+
y_coord = DimCoord(y_points, 'projection_y_coordinate', units='m',
743+
coord_system=cs)
744+
x_coord = DimCoord(x_points, 'projection_x_coordinate', units='m',
745+
coord_system=cs)
746+
747+
# Determine the lat/lon dimensions.
748+
y_dim, x_dim = 0, 1
749+
scan = scanning_mode(section['scanningMode'])
750+
if scan.j_consecutive:
751+
y_dim, x_dim = 1, 0
752+
753+
# Add the X and Y coordinates to the metadata dim coords.
754+
metadata['dim_coords_and_dims'].append((y_coord, y_dim))
755+
metadata['dim_coords_and_dims'].append((x_coord, x_dim))
756+
757+
647758
def grid_definition_template_90(section, metadata):
648759
"""
649760
Translate template representing space view.
@@ -792,6 +903,9 @@ def grid_definition_section(section, metadata):
792903
elif template == 5:
793904
# Process variable resolution rotated latitude/longitude.
794905
grid_definition_template_5(section, metadata)
906+
elif template == 12:
907+
# Process transverse Mercator.
908+
grid_definition_template_12(section, metadata)
795909
elif template == 90:
796910
# Process space view.
797911
grid_definition_template_90(section, metadata)

lib/iris/fileformats/grib/_message.py

Lines changed: 1 addition & 1 deletion
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, 5, 90):
104+
if template in (0, 1, 5, 12, 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:

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,19 @@
1717
"""Unit tests for the :mod:`iris.fileformats.grib._load_convert` package."""
1818

1919
from __future__ import (absolute_import, division, print_function)
20+
21+
from collections import OrderedDict
22+
23+
24+
def empty_metadata():
25+
metadata = OrderedDict()
26+
metadata['factories'] = []
27+
metadata['references'] = []
28+
metadata['standard_name'] = None
29+
metadata['long_name'] = None
30+
metadata['units'] = None
31+
metadata['attributes'] = {}
32+
metadata['cell_methods'] = []
33+
metadata['dim_coords_and_dims'] = []
34+
metadata['aux_coords_and_dims'] = []
35+
return metadata
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# (C) British Crown Copyright 2014, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
"""
18+
Unit tests for `iris.fileformats.grib._load_convert.fixup_float32_from_int32`.
19+
20+
"""
21+
22+
from __future__ import (absolute_import, division, print_function)
23+
24+
# Import iris.tests first so that some things can be initialised before
25+
# importing anything else.
26+
import iris.tests as tests
27+
28+
from iris.fileformats.grib._load_convert import fixup_float32_from_int32
29+
30+
31+
class Test(tests.IrisTest):
32+
def test_negative(self):
33+
result = fixup_float32_from_int32(-0x3f000000)
34+
self.assertEqual(result, -0.5)
35+
36+
def test_zero(self):
37+
result = fixup_float32_from_int32(0)
38+
self.assertEqual(result, 0)
39+
40+
def test_positive(self):
41+
result = fixup_float32_from_int32(0x3f000000)
42+
self.assertEqual(result, 0.5)
43+
44+
45+
if __name__ == '__main__':
46+
tests.main()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# (C) British Crown Copyright 2014, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
"""
18+
Unit tests for `iris.fileformats.grib._load_convert.fixup_int32_from_uint32`.
19+
20+
"""
21+
22+
from __future__ import (absolute_import, division, print_function)
23+
24+
# Import iris.tests first so that some things can be initialised before
25+
# importing anything else.
26+
import iris.tests as tests
27+
28+
from iris.fileformats.grib._load_convert import fixup_int32_from_uint32
29+
30+
31+
class Test(tests.IrisTest):
32+
def test_negative(self):
33+
result = fixup_int32_from_uint32(0x80000005)
34+
self.assertEqual(result, -5)
35+
36+
def test_negative_zero(self):
37+
result = fixup_int32_from_uint32(0x80000000)
38+
self.assertEqual(result, 0)
39+
40+
def test_zero(self):
41+
result = fixup_int32_from_uint32(0)
42+
self.assertEqual(result, 0)
43+
44+
def test_positive(self):
45+
result = fixup_int32_from_uint32(200000)
46+
self.assertEqual(result, 200000)
47+
48+
def test_already_negative(self):
49+
# If we *already* have a negative value the fixup routine should
50+
# leave it alone.
51+
result = fixup_int32_from_uint32(-7)
52+
self.assertEqual(result, -7)
53+
54+
55+
if __name__ == '__main__':
56+
tests.main()

0 commit comments

Comments
 (0)