Skip to content

Commit 97d7941

Browse files
committed
Merge pull request SciTools#1477 from dkillick/grib_save_gdt12
Add GRIB save GDT12 (Transverse Mercator)
2 parents 4b38568 + c0decef commit 97d7941

File tree

4 files changed

+271
-23
lines changed

4 files changed

+271
-23
lines changed

lib/iris/fileformats/grib/__init__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -960,15 +960,13 @@ def save_grib2(cube, target, append=False, **kwargs):
960960
else:
961961
raise ValueError("Can only save grib to filename or writable")
962962

963-
# discover the lat and lon coords (this bit is common to the pp and grib savers...)
964-
lat_coords = filter(lambda coord: "latitude" in coord.name(), cube.coords())
965-
lon_coords = filter(lambda coord: "longitude" in coord.name(), cube.coords())
966-
if len(lat_coords) != 1 or len(lon_coords) != 1:
967-
raise TranslationError("Did not find one (and only one) "
968-
"latitude or longitude coord")
963+
x_coords = cube.coords(axis='x', dim_coords=True)
964+
y_coords = cube.coords(axis='y', dim_coords=True)
965+
if len(x_coords) != 1 or len(y_coords) != 1:
966+
raise TranslationError("Did not find one (and only one) x or y coord")
969967

970968
# Save each latlon slice2D in the cube
971-
for slice2D in cube.slices([lat_coords[0], lon_coords[0]]):
969+
for slice2D in cube.slices([y_coords[0], x_coords[0]]):
972970

973971
# Save this slice to the grib file
974972
grib_message = gribapi.grib_new_from_samples("GRIB2")

lib/iris/fileformats/grib/_save_rules.py

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import iris
3636
import iris.exceptions
3737
import iris.unit
38+
from iris.coord_systems import GeogCS, RotatedGeogCS, TransverseMercator
3839
from iris.fileformats.rules import is_regular, regular_step
3940
from iris.fileformats.grib import grib_phenom_translation as gptx
4041
from iris.fileformats.grib._load_convert import (_STATISTIC_TYPE_NAMES,
@@ -92,6 +93,19 @@ def fixup_int32_as_uint32(value):
9293
return value
9394

9495

96+
def ensure_set_int32_value(grib, key, value):
97+
"""
98+
Ensure the workaround function :func:`fixup_int32_as_uint32` is applied as
99+
necessary to problem keys.
100+
101+
"""
102+
try:
103+
gribapi.grib_set(grib, key, value)
104+
except gribapi.GribInternalError:
105+
value = fixup_int32_as_uint32(value)
106+
gribapi.grib_set(grib, key, value)
107+
108+
95109
###############################################################################
96110
#
97111
# Constants
@@ -104,13 +118,16 @@ def fixup_int32_as_uint32(value):
104118
# Reference Regulation 92.1.6
105119
_DEFAULT_DEGREES_UNITS = 1.0e-6
106120

121+
# Units scaling for Grid Definition Template 3.12
122+
_TRANSVERSE_MERCATOR_DEGREES_UNITS = 1.0e-2
107123

108124
###############################################################################
109125
#
110126
# Identification Section 1
111127
#
112128
###############################################################################
113129

130+
114131
def centre(cube, grib):
115132
# TODO: read centre from cube
116133
gribapi.grib_set_long(grib, "centre", 74) # UKMO
@@ -159,28 +176,35 @@ def identification(cube, grib):
159176
#
160177
###############################################################################
161178

162-
def shape_of_the_earth(cube, grib):
163179

180+
def shape_of_the_earth(cube, grib):
164181
# assume latlon
165182
cs = cube.coord(dimensions=[0]).coord_system
166183

167-
# Turn them all missing to start with (255 for byte, -1 for long)
184+
# Initially set shape_of_earth keys to missing (255 for byte, -1 for long).
168185
gribapi.grib_set_long(grib, "scaleFactorOfRadiusOfSphericalEarth", 255)
169186
gribapi.grib_set_long(grib, "scaledValueOfRadiusOfSphericalEarth", -1)
170187
gribapi.grib_set_long(grib, "scaleFactorOfEarthMajorAxis", 255)
171188
gribapi.grib_set_long(grib, "scaledValueOfEarthMajorAxis", -1)
172189
gribapi.grib_set_long(grib, "scaleFactorOfEarthMinorAxis", 255)
173190
gribapi.grib_set_long(grib, "scaledValueOfEarthMinorAxis", -1)
174191

175-
ellipsoid = cs
176-
if isinstance(cs, iris.coord_systems.RotatedGeogCS):
192+
if isinstance(cs, GeogCS):
193+
ellipsoid = cs
194+
else:
177195
ellipsoid = cs.ellipsoid
196+
if ellipsoid is None:
197+
msg = "Could not determine shape of the earth from coord system "\
198+
"of horizontal grid."
199+
raise iris.exceptions.TranslationError(msg)
178200

201+
# Spherical earth.
179202
if ellipsoid.inverse_flattening == 0.0:
180203
gribapi.grib_set_long(grib, "shapeOfTheEarth", 1)
181204
gribapi.grib_set_long(grib, "scaleFactorOfRadiusOfSphericalEarth", 0)
182205
gribapi.grib_set_long(grib, "scaledValueOfRadiusOfSphericalEarth",
183206
ellipsoid.semi_major_axis)
207+
# Oblate spheroid earth.
184208
else:
185209
gribapi.grib_set_long(grib, "shapeOfTheEarth", 7)
186210
gribapi.grib_set_long(grib, "scaleFactorOfEarthMajorAxis", 0)
@@ -223,9 +247,8 @@ def latlon_first_last(x_coord, y_coord, grib):
223247
def dx_dy(x_coord, y_coord, grib):
224248
x_step = regular_step(x_coord)
225249
y_step = regular_step(y_coord)
226-
# TODO: THIS USED BE "Dx" and "Dy"!!! DID THE API CHANGE AGAIN???
227-
gribapi.grib_set_double(grib, "DxInDegrees", float(abs(x_step)))
228-
gribapi.grib_set_double(grib, "DyInDegrees", float(abs(y_step)))
250+
gribapi.grib_set(grib, "DxInDegrees", float(abs(x_step)))
251+
gribapi.grib_set(grib, "DyInDegrees", float(abs(y_step)))
229252

230253

231254
def scanning_mode_flags(x_coord, y_coord, grib):
@@ -235,7 +258,7 @@ def scanning_mode_flags(x_coord, y_coord, grib):
235258
int(y_coord.points[1] - y_coord.points[0] > 0))
236259

237260

238-
def latlon_grid_common(cube, grib):
261+
def horizontal_grid_common(cube, grib):
239262
# Grib encoding of the sequences of X and Y points.
240263
y_coord = cube.coord(dimensions=[0])
241264
x_coord = cube.coord(dimensions=[1])
@@ -312,7 +335,7 @@ def grid_definition_template_0(cube, grib):
312335
"""
313336
# Constant resolution, aka 'regular' true lat-lon grid.
314337
gribapi.grib_set_long(grib, "gridDefinitionTemplateNumber", 0)
315-
latlon_grid_common(cube, grib)
338+
horizontal_grid_common(cube, grib)
316339
latlon_points_regular(cube, grib)
317340

318341

@@ -333,7 +356,7 @@ def grid_definition_template_1(cube, grib):
333356
rotated_pole(cube, grib)
334357

335358
# Encode the lat/lon points.
336-
latlon_grid_common(cube, grib)
359+
horizontal_grid_common(cube, grib)
337360
latlon_points_regular(cube, grib)
338361

339362

@@ -351,19 +374,84 @@ def grid_definition_template_5(cube, grib):
351374
# Without this, setting "gridDefinitionTemplateNumber" = 5 causes an
352375
# immediate error.
353376
# See: https://software.ecmwf.int/issues/browse/SUP-1095
354-
# This is acceptable, as the subsequent call to 'latlon_grid_common' will
355-
# set these to the correct horizontal dimensions (by calling 'grid_dims').
377+
# This is acceptable, as the subsequent call to 'horizontal_grid_common'
378+
# will set these to the correct horizontal dimensions
379+
# (by calling 'grid_dims').
356380
gribapi.grib_set(grib, "Ni", 1)
357381
gribapi.grib_set(grib, "Nj", 1)
358382
gribapi.grib_set(grib, "gridDefinitionTemplateNumber", 5)
359383

360384
# Record details of the rotated coordinate system.
361385
rotated_pole(cube, grib)
362386
# Encode the lat/lon points.
363-
latlon_grid_common(cube, grib)
387+
horizontal_grid_common(cube, grib)
364388
latlon_points_irregular(cube, grib)
365389

366390

391+
def grid_definition_template_12(cube, grib):
392+
"""
393+
Set keys within the provided grib message based on
394+
Grid Definition Template 3.12.
395+
396+
Template 3.12 is used to represent a Transverse Mercator grid.
397+
398+
"""
399+
gribapi.grib_set(grib, "gridDefinitionTemplateNumber", 12)
400+
401+
# Retrieve some information from the cube.
402+
y_coord = cube.coord(dimensions=[0])
403+
x_coord = cube.coord(dimensions=[1])
404+
cs = y_coord.coord_system
405+
406+
# Set some keys specific to GDT12.
407+
# Encode the horizontal points.
408+
x_step = regular_step(x_coord)
409+
y_step = regular_step(y_coord)
410+
gribapi.grib_set(grib, "Di",
411+
float(abs(x_step)) / _TRANSVERSE_MERCATOR_DEGREES_UNITS)
412+
gribapi.grib_set(grib, "Dj",
413+
float(abs(y_step)) / _TRANSVERSE_MERCATOR_DEGREES_UNITS)
414+
horizontal_grid_common(cube, grib)
415+
416+
# GRIBAPI expects unsigned ints in X1, X2, Y1, Y2 but it should accept
417+
# signed ints, so work around this.
418+
# See https://software.ecmwf.int/issues/browse/SUP-1101
419+
ensure_set_int32_value(
420+
grib, "Y1",
421+
int(y_coord.points[0] / _TRANSVERSE_MERCATOR_DEGREES_UNITS))
422+
ensure_set_int32_value(
423+
grib, "Y2",
424+
int(y_coord.points[-1] / _TRANSVERSE_MERCATOR_DEGREES_UNITS))
425+
ensure_set_int32_value(
426+
grib, "X1",
427+
int(x_coord.points[0] / _TRANSVERSE_MERCATOR_DEGREES_UNITS))
428+
ensure_set_int32_value(
429+
grib, "X2",
430+
int(x_coord.points[-1] / _TRANSVERSE_MERCATOR_DEGREES_UNITS))
431+
432+
# Lat and lon of reference point are measured in millionths of a degree.
433+
gribapi.grib_set(grib, "latitudeOfReferencePoint",
434+
cs.latitude_of_projection_origin / _DEFAULT_DEGREES_UNITS)
435+
gribapi.grib_set(grib, "longitudeOfReferencePoint",
436+
cs.longitude_of_central_meridian / _DEFAULT_DEGREES_UNITS)
437+
438+
# False easting and false northing are measured in units of (10^-2)m.
439+
gribapi.grib_set(grib, "XR",
440+
cs.false_easting / _TRANSVERSE_MERCATOR_DEGREES_UNITS)
441+
gribapi.grib_set(grib, "YR",
442+
cs.false_northing / _TRANSVERSE_MERCATOR_DEGREES_UNITS)
443+
444+
# GRIBAPI expects a signed int for scaleFactorAtReferencePoint
445+
# but it should accept a float, so work around this.
446+
# See https://software.ecmwf.int/issues/browse/SUP-1100
447+
value = cs.scale_factor_at_central_meridian
448+
key_type = gribapi.grib_get_native_type(grib,
449+
"scaleFactorAtReferencePoint")
450+
if key_type is not float:
451+
value = fixup_float32_as_int32(value)
452+
gribapi.grib_set(grib, "scaleFactorAtReferencePoint", value)
453+
454+
367455
def grid_definition_section(cube, grib):
368456
"""
369457
Set keys within the grid definition section of the provided grib message,
@@ -375,22 +463,26 @@ def grid_definition_section(cube, grib):
375463
cs = x_coord.coord_system # N.B. already checked same cs for x and y.
376464
regular_x_and_y = is_regular(x_coord) and is_regular(y_coord)
377465

378-
if isinstance(cs, iris.coord_systems.GeogCS):
466+
if isinstance(cs, GeogCS):
379467
if not regular_x_and_y:
380468
raise iris.exceptions.TranslationError(
381469
'Saving an irregular latlon grid to GRIB (PDT3.4) is not '
382470
'yet supported.')
383471

384472
grid_definition_template_0(cube, grib)
385473

386-
elif isinstance(cs, iris.coord_systems.RotatedGeogCS):
474+
elif isinstance(cs, RotatedGeogCS):
387475
# Rotated coordinate system cases.
388476
# Choose between GDT 3.1 and 3.5 according to coordinate regularity.
389477
if regular_x_and_y:
390478
grid_definition_template_1(cube, grib)
391479
else:
392480
grid_definition_template_5(cube, grib)
393481

482+
elif isinstance(cs, TransverseMercator):
483+
# Transverse Mercator coordinate system (template 3.12).
484+
grid_definition_template_12(cube, grib)
485+
394486
else:
395487
raise ValueError('Grib saving is not supported for coordinate system: '
396488
'{:s}'.format(cs))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ def grib_set_trap(grib, name, value):
3939
# Record a key setting on the mock passed as the 'grib message id'.
4040
grib.keys[name] = value
4141

42+
self.mock_gribapi.grib_set = grib_set_trap
4243
self.mock_gribapi.grib_set_long = grib_set_trap
4344
self.mock_gribapi.grib_set_float = grib_set_trap
4445
self.mock_gribapi.grib_set_double = grib_set_trap
4546
self.mock_gribapi.grib_set_long_array = grib_set_trap
46-
self.mock_gribapi.grib_set = grib_set_trap
4747
self.mock_gribapi.grib_set_array = grib_set_trap
4848

4949
# Create a mock 'grib message id', with a 'keys' dict for settings.

0 commit comments

Comments
 (0)