Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 6 additions & 2 deletions lib/iris/coord_systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):

Expand Down Expand Up @@ -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'.
Expand Down
76 changes: 76 additions & 0 deletions lib/iris/tests/integration/test_netcdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -22,47 +22,57 @@


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):
self._test(inverse_flattening=False)

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()