diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt new file mode 100644 index 0000000000..e1113c838c --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-06_relax_geostationary.txt @@ -0,0 +1,6 @@ +* :class:`iris.coord_systems.Geostationary` can now accept creation arguments of + `false_easting=None` or `false_northing=None`, equivalent to values of 0. + Previously these kwargs could be omitted, but could not be set to `None`. + This also enables loading netcdf data on a Geostationary grid, where either of these + keys is not present as a grid-mapping variable property : Previously, loading any + such data caused an exception. diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 8e8b58fa46..cc41b27b34 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -714,8 +714,8 @@ def __init__( longitude_of_projection_origin, perspective_point_height, sweep_angle_axis, - false_easting=0, - false_northing=0, + false_easting=None, + false_northing=None, ellipsoid=None, ): @@ -768,9 +768,13 @@ def __init__( self.perspective_point_height = float(perspective_point_height) #: X offset from planar origin in metres. + if false_easting is None: + false_easting = 0 self.false_easting = float(false_easting) #: Y offset from planar origin in metres. + if false_northing is None: + false_northing = 0 self.false_northing = float(false_northing) #: The axis along which the satellite instrument sweeps - 'x' or 'y'. diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index c33c8baf54..8c6e0f6659 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -12,7 +12,9 @@ from contextlib import contextmanager from itertools import repeat import os.path +from os.path import join as path_join import shutil +from subprocess import check_call import tempfile from unittest import mock import warnings @@ -592,5 +594,79 @@ def test_standard_name_roundtrip(self): self.assertEqual(detection_limit_cube.standard_name, standard_name) +class TestLoadMinimalGeostationary(tests.IrisTest): + """ + Check we can load data with a geostationary grid-mapping, even when the + 'false-easting' and 'false_northing' properties are missing. + + """ + + _geostationary_problem_cdl = """ +netcdf geostationary_problem_case { +dimensions: + y = 2 ; + x = 3 ; +variables: + short radiance(y, x) ; + radiance:standard_name = "toa_outgoing_radiance_per_unit_wavelength" ; + radiance:units = "W m-2 sr-1 um-1" ; + radiance:coordinates = "y x" ; + radiance:grid_mapping = "imager_grid_mapping" ; + short y(y) ; + y:units = "rad" ; + y:axis = "Y" ; + y:long_name = "fixed grid projection y-coordinate" ; + y:standard_name = "projection_y_coordinate" ; + short x(x) ; + x:units = "rad" ; + x:axis = "X" ; + x:long_name = "fixed grid projection x-coordinate" ; + x:standard_name = "projection_x_coordinate" ; + int imager_grid_mapping ; + imager_grid_mapping:grid_mapping_name = "geostationary" ; + imager_grid_mapping:perspective_point_height = 35786023. ; + imager_grid_mapping:semi_major_axis = 6378137. ; + imager_grid_mapping:semi_minor_axis = 6356752.31414 ; + imager_grid_mapping:latitude_of_projection_origin = 0. ; + imager_grid_mapping:longitude_of_projection_origin = -75. ; + imager_grid_mapping:sweep_angle_axis = "x" ; + +data: + + // coord values, just so these can be dim-coords + y = 0, 1 ; + x = 0, 1, 2 ; + +} +""" + + @classmethod + def setUpClass(cls): + # Create a temp directory for transient test files. + cls.temp_dir = tempfile.mkdtemp() + cls.path_test_cdl = path_join(cls.temp_dir, "geos_problem.cdl") + cls.path_test_nc = path_join(cls.temp_dir, "geos_problem.nc") + # Create a reference file from the CDL text. + with open(cls.path_test_cdl, "w") as f_out: + f_out.write(cls._geostationary_problem_cdl) + # Call 'ncgen' to make an actual netCDF file from the CDL. + command = "ncgen -o {} {}".format(cls.path_test_nc, cls.path_test_cdl) + check_call(command, shell=True) + + @classmethod + def tearDownClass(cls): + # Destroy the temp directory. + shutil.rmtree(cls.temp_dir) + + def test_geostationary_no_false_offsets(self): + # Check we can load the test data and coordinate system properties are correct. + cube = iris.load_cube(self.path_test_nc) + # Check the coordinate system properties has the correct default properties. + cs = cube.coord_system() + self.assertIsInstance(cs, iris.coord_systems.Geostationary) + self.assertEqual(cs.false_easting, 0.0) + self.assertEqual(cs.false_northing, 0.0) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_geostationary_coordinate_system.py b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_geostationary_coordinate_system.py index bb045769d8..8a296cc896 100644 --- a/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_geostationary_coordinate_system.py +++ b/lib/iris/tests/unit/fileformats/pyke_rules/compiled_krb/fc_rules_cf_fc/test_build_geostationary_coordinate_system.py @@ -22,43 +22,43 @@ class TestBuildGeostationaryCoordinateSystem(tests.IrisTest): - def _test(self, inverse_flattening=False): + def _test(self, inverse_flattening=False, replace_props=None, remove_props=None): """ Generic test that can check vertical perspective validity with or without inverse flattening. """ - cf_grid_var_kwargs = { - 'spec': [], + # Make a dictionary of the non-ellipsoid properties to be added to both a test + # coord-system, and a test grid-mapping cf_var. + non_ellipsoid_kwargs = { 'latitude_of_projection_origin': 0.0, 'longitude_of_projection_origin': 2.0, 'perspective_point_height': 2000000.0, 'sweep_angle_axis': 'x', 'false_easting': 100.0, - 'false_northing': 200.0, - 'semi_major_axis': 6377563.396} + 'false_northing': 200.0} + # Make specified adjustments to the non-ellipsoid properties. + if remove_props: + for key in remove_props: + non_ellipsoid_kwargs.pop(key, None) + if replace_props: + for key, value in replace_props.items(): + non_ellipsoid_kwargs[key] = value + + # Make a dictionary of ellipsoid properties, to be added to both a test + # ellipsoid and the grid-mapping cf_var. ellipsoid_kwargs = {'semi_major_axis': 6377563.396} if inverse_flattening: ellipsoid_kwargs['inverse_flattening'] = 299.3249646 else: ellipsoid_kwargs['semi_minor_axis'] = 6356256.909 - cf_grid_var_kwargs.update(ellipsoid_kwargs) - - cf_grid_var = mock.Mock(**cf_grid_var_kwargs) - ellipsoid = iris.coord_systems.GeogCS(**ellipsoid_kwargs) + cf_grid_var_kwargs = non_ellipsoid_kwargs.copy() + cf_grid_var_kwargs.update(ellipsoid_kwargs) + cf_grid_var = mock.Mock(spec=[], **cf_grid_var_kwargs) cs = build_geostationary_coordinate_system(None, cf_grid_var) - expected = Geostationary( - latitude_of_projection_origin=cf_grid_var. - latitude_of_projection_origin, - longitude_of_projection_origin=cf_grid_var. - longitude_of_projection_origin, - perspective_point_height=cf_grid_var.perspective_point_height, - sweep_angle_axis=cf_grid_var.sweep_angle_axis, - false_easting=cf_grid_var.false_easting, - false_northing=cf_grid_var.false_northing, - ellipsoid=ellipsoid) - + ellipsoid = iris.coord_systems.GeogCS(**ellipsoid_kwargs) + expected = Geostationary(ellipsoid=ellipsoid, **non_ellipsoid_kwargs) self.assertEqual(cs, expected) def test_valid(self): @@ -66,3 +66,13 @@ def test_valid(self): def test_inverse_flattening(self): self._test(inverse_flattening=True) + + def test_false_offsets_missing(self): + self._test(remove_props=['false_easting', 'false_northing']) + + def test_false_offsets_none(self): + self._test(replace_props={'false_easting':None, 'false_northing':None}) + + +if __name__ == "__main__": + tests.main()